Překlady#
Systém má dvě na sobě nezávislé překladové vrstvy. Většina zmatků kolem vícejazyčnosti pochází z toho, že se zamění.
| Vrstva | Kde žije | Co překládá | Když text chybí |
|---|---|---|---|
| NEON | app/Locale/ |
texty rozhraní — popisky, tlačítka, hlášky | vypíše se holý klíč, stránka jede dál |
| Databáze | tabulky *_texts |
obsah — názvy stránek, kategorií, produktů a jejich href |
podle místa buď fallback na hlavní jazyk, nebo 404 celé cesty |
Závazná konvence pro NEON je ve skillu skills/cs-translations/SKILL.md.
NEON: kde soubory jsou#
app/Locale/<modul>/<locale>/<doména>.<LOCALE>.neon
│ │ │
│ │ └─ front | admin | scripts
│ └─ cs_cz | en_us | sk_sk
└─ base bazaar blog comcat discussion eshop general
invoicer results store system
Stav změřený k 2026-08-15: 11 modulů × 3 jazyky = 63 souborů, v každém jazyce
stejných 21 (find app/Locale -name "*.neon" | wc -l). Z toho 27 domény front,
30 admin, 3 scripts a 3 soubory modulu results bez prefixu domény.
Jak vzniká message ID#
Soubor bazaar/cs_cz/front.cs_CZ.neon:
→ message ID front.bazaar.components.responseForm.popup.header: doména z názvu
souboru plus stromová cesta uvnitř NEONu. Klíče se skládají stromově, nikdy ploše
s tečkami v názvu.
Pole formuláře musí mít label: jako podklíč, ne holý řetězec
name: "Název" udělá z form.fields.name řetězec, takže form.fields.name.label
už nemá kam sáhnout a translator vrátí holý klíč. Vždycky
form.fields.name.label: — i když je pod tím jediný podklíč.
Kde se doména bere a kdo překládá šablonu#
Tohle je nejčastější zdroj „proč se mi to nepřeložilo“. Tři místa, tři různá chování —
ověřitelná v TranslatorInitTrait::setTranslatorDomain(),
Admin\BasePresenter::setupTemplateTranslator() a BaseComponent::setup():
| Kde | Odkud je doména | Šablona překládá krátké klíče |
|---|---|---|
| Front presenter | odvodí se z namespace — App\UI\Front\Blog\Presenters + ArticlePresenter → front.blog.presenters.article |
ne — šablona potřebuje {translator $translatorDomain} |
| Admin presenter | ručně z vlastnosti $translatorDomain na presenteru |
ano, base ji nastaví do šablony |
| Komponenta (Front i Admin) | ručně z vlastnosti $translatorDomain |
ano, setup() ji nastaví do šablony |
Ve Front presenteru je deklarovaná $translatorDomain jen dokumentace
startup() ji přepíše hodnotou odvozenou z namespace. Kdo přesune presenter do
jiného namespace, přesune tím i doménu — a všechny jeho klíče přestanou existovat.
Projeví se to vypsáním syrových klíčů na stránce, ne chybou.
{translator} musí být UVNITŘ {block} / {snippet}, ne před ním
Mimo blok Latte hlásí Unexpected end, expecting {/translator} — a hlásí to na
úplně jiném řádku, než kde je příčina.
{translator} se do {include} nepřenáší
Každá vložená sekce si ho musí otevřít znovu. Rodičovská šablona přeložená je, vložená sekce vedle ní vypíše klíče — vypadá to jako chybějící překlad.
V šabloně KOMPONENTY {translator} naopak nepoužívat
Doménu tam nastavil už BaseComponent::setup(). Přidané makro skončí chybou
o neukončeném translatoru.
Globální klíč se zapíše absolutně dvěma lomítky
{_'//admin.system.form.status.active'} doménu obejde a sáhne na plný message ID.
V administraci je to použité na 125 místech pro sdílené texty layoutu.
Postup: přidávám nový text do kódu#
- Najdi správný soubor — modul podle toho, čí je ten kód, doména podle sekce
(
front/admin/scripts). - Zkontroluj, jestli klíč už neexistuje.
messages.success,close,backa spol. bývají hotové; duplikovat je znamená mít později dva různé texty pro tutéž věc. - Přidej klíč stromově k logicky příbuzným, hodnotu do dvojitých uvozovek, odsazení mezerami (NEON, ne taby).
- Doplň týž klíč do všech tří jazyků —
cs_cz,en_us,sk_sk. Nepřeložený klíč doplň aspoň českým textem; chybějící klíč je horší než nepřeložený. - Použij ho v kódu podle kontextu:
| Kontext | Zápis |
|---|---|
| Latte | {_'popup.header'} (s doménou), {_'//plny.message.id'} (absolutně) |
| Formulář | holý klíč rovnou do addText('name', 'name.label') |
setOption('description', …) |
jen klíč, překládá se až v Latte |
| PHP mimo formulář | $this->translator->translate($this->translatorDomain . '.popup.loadError') |
| Flash v komponentě | holý klíč bez domény — {_$flash->message} doménu přidá |
- Ověř, že NEON je platný:
php -r "Nette\Neon\Neon::decode(file_get_contents('…'));"nebo prostě načtení stránky.
Popisek formuláře předaný už s doménou se prefixuje podruhé
Při useDomainTranslator() translator doménu přidává sám. $t('name.label')
v labelu proto vyrobí
front.blog.presenters.contact.front.blog.presenters.contact.name.label — klíč,
který neexistuje, takže se vypíše syrový. Vypadá to jako chybějící překlad,
ale chyba je v tom, že tam je dvakrát.
setOption('description', …) přes translator vůbec neprojde
Nette tuhle hodnotu nepřekládá. Když do ní dáte přeložený text, v Latte se
{_…} pokusí přeložit už hotovou větu — a vrátí ji nezměněnou jen náhodou.
Do PHP patří klíč, překlad do šablony.
Postup: přidávám nový jazyk#
- Založ jazyk a lokalizaci v administraci (Systém → Jazyky).
- NEON soubory: nová složka
app/Locale/<modul>/<locale>/a v ní kopie všech souborů daného modulu s příponou nové locale (front.sk_SK.neon). - Zaregistruj krátký kód do mapy v
TranslatorInitTrait::LOCALE_MAPa vapp/Core/Middlewares/LocaleMiddleWare.php— obojí mapujesk→sk_SK. - Smaž
temp/cache/nette.configurator,nette.searchatranslation. Nová locale (nový soubor, ne jen nový klíč) vyžaduje přestavbu DI kontejneru. - Databázová vrstva začíná
cms_system_page_texts— bez řádku pro nový jazyk nefunguje žádná pojmenovaná front stránka, i kdyby kategorie pod ní přeložené byly. Je to jen desítky řádků a blokuje všechno ostatní. - Pak teprve modulové
*_texts— kategorie e-shopu, inzerce, katalogu firem, blogu. V systému je jich 84 rodin (find app/UI/Api -name "*Text.php" -path "*Entities*" | wc -l). - Projdi hlavní typy stránek v novém jazyce — rozcestník, výpis, detail, formulář. Ne jen homepage.
Krátký kód locale poslaný do setLocale() vrátí ČEŠTINU
translator.neon má jeden sdílený fallback řetězec
[cs_CZ, cs, en_US, en, sk_SK, sk] pro všechny jazyky, takže cs_CZ v něm vyhraje
vždycky. Živě ověřeno: setLocale('en') vracelo český text i s kompletní sadou
anglických souborů. Proto ta mapa v kroku 3 — bez ní by celý překlad byl
neviditelný a nic by na to neupozornilo.
Bez smazání temp/cache se nový jazyk projeví nepředvídatelně
Ne chybou — částečně. Něco přeložené je, něco ne, a podle toho, co se zrovna přegeneruje. Hledá se to hodiny.
Postup: text se nepřekládá#
Od nejlevnějšího:
- Vypisuje se holý message ID? Pak klíč v NEONu chybí nebo je jinde, než kde ho hledáte. Přečtěte si vypsaný klíč — je v něm celá cesta i doména.
- Je doména v klíči dvakrát? Viz varování výše — text je přeložený v PHP i v Latte.
- Je to šablona presenteru? Front presenter potřebuje
{translator}, a to i v každém{include}. - Je to šablona komponenty? Tam
{translator}naopak být nesmí. - Změnil se namespace presenteru? S ním se změnila i doména.
- Je to text z databáze, ne z NEONu? Názvy kategorií, stránek a produktů jsou
*_textsřádky — v NEONu je nenajdete. - Po zásahu do souborů smažte
temp/cache/translation/. Nette regeneruje skoro všechno samo, katalog překladů ne.
Databázová vrstva a její pasti#
Texty obsahu jsou v tabulkách *_texts s vazbou na jazyk. Chování při chybějícím
překladu se liší podle toho, k čemu ten text slouží:
| Text | Chybí-li |
|---|---|
název (name, title) |
fallback na jiný jazyk |
href použitý v routě |
routa URL nesestaví → No route for … |
řádek v cms_system_page_texts |
404 celé cesty /<locale>/<cokoliv> |
Jedna nepřeložená kategorie shodí CELOU cizojazyčnou stránku
Route translator vrátí pro chybějící překlad null, RestrictedTranslatableRoute
pak URL nesestaví a Nette hlásí Invalid link: No route for … — což shodí celou
stránku, ne jen ten jeden odkaz. Řeší to
AbstractTranslator::fromCacheWithLanguageFallback(): chybí-li překlad pro
požadovaný jazyk, vezme se jiný (hlavní první). Zapojeno u kategorií e-shopu,
inzerce, katalogu firem i blogu — u nového modulu na to musíte myslet sami.
Prázdný překlad PŘEBIJE fallback
Fallback se píše řetězcem ??, a ten přeskočí jen null. Prázdný řetězec je
hodnota, takže řádek s name = '' vyhraje nad hlavním jazykem a prvek se
vykreslí úplně bez názvu. Držte invariant „řádek existuje ⟺ má neprázdný
název": prázdný nezakládejte a existující smažte.
Tabulka může mít dva překlady téhož v jednom jazyce
Šesti tabulkám *_texts chyběl primární klíč (revize 2026-09-03), takže
databáze pouštěla duplicitní řádky — a ON DUPLICATE KEY UPDATE nad nimi místo
aktualizace tiše vkládal další. Který z nich se přečte, nic negarantovalo.
Typicky to vypadalo jako předchozí past: vedle vyplněného řádku ležel prázdný.
Na existující databázi to prověřte podle kapitoly
Kontrola databáze — validateClass chybějící
klíč nenajde, protože neporovnává indexy.
Řádek může existovat, a přesto být česky
Zkopírovaný a nepřeložený text se tváří jako hotový a filtr nepřeložených ho nenajde. Pokrytí se proto měří obsahem, ne počtem řádků.
Filtr nepřeložených v administraci skrývá jiné věci než formulář
Prázdný filtr neznamená „přeloženo“. Znamená „nic neodpovídá tomu, co filtr hledá" — a to je něco jiného.
Kam sáhnout#
| Chci | Kde |
|---|---|
| přidat nebo najít klíč rozhraní | app/Locale/<modul>/<locale>/<doména>.<LOCALE>.neon |
| nastavení translatoru, fallback, whitelist | config/Shared/translator.neon |
| mapování krátkého kódu na locale | app/Core/Middlewares/LocaleMiddleWare.php, app/Core/Traits/Shared/Presenters/TranslatorInitTrait.php |
| odvozování domény Front presenteru | TranslatorInitTrait::setTranslatorDomain() |
| doména v šabloně administrace | app/Core/Base/Admin/BasePresenter.php → setupTemplateTranslator() |
| doména v komponentě | app/Core/Base/BaseComponent.php → setup() |
| fallback překladů v routách | app/Core/Routers/Translators/AbstractTranslator.php |
| texty výsledkových kódů | app/Locale/results/ + app/Core/Utils/Results/ResultCode.php |
| závazná konvence | skills/cs-translations/SKILL.md |
Navazující kapitoly: Routování · Formuláře · Cache · Konvence kódu