Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Pro vývojáře / Překlady

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:

bazaar:
    components:
        responseForm:
            popup:
                header: "Odpovědět na inzerát"

→ 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 namespaceApp\UI\Front\Blog\Presenters + ArticlePresenterfront.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
{block content}
{translator $translatorDomain}
    <h1>{_'headers.list'}</h1>
{/translator}
{/block}

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#

  1. Najdi správný soubor — modul podle toho, čí je ten kód, doména podle sekce (front / admin / scripts).
  2. Zkontroluj, jestli klíč už neexistuje. messages.success, close, back a spol. bývají hotové; duplikovat je znamená mít později dva různé texty pro tutéž věc.
  3. Přidej klíč stromově k logicky příbuzným, hodnotu do dvojitých uvozovek, odsazení mezerami (NEON, ne taby).
  4. 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ý.
  5. 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á
  1. 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#

  1. Založ jazyk a lokalizaci v administraci (Systém → Jazyky).
  2. 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).
  3. Zaregistruj krátký kód do mapy v TranslatorInitTrait::LOCALE_MAP a v app/Core/Middlewares/LocaleMiddleWare.php — obojí mapuje sksk_SK.
  4. Smaž temp/cache/nette.configurator, nette.search a translation. Nová locale (nový soubor, ne jen nový klíč) vyžaduje přestavbu DI kontejneru.
  5. 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í.
  6. 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).
  7. 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.neonjeden 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:

  1. 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.
  2. Je doména v klíči dvakrát? Viz varování výše — text je přeložený v PHP i v Latte.
  3. Je to šablona presenteru? Front presenter potřebuje {translator}, a to i v každém {include}.
  4. Je to šablona komponenty? Tam {translator} naopak být nesmí.
  5. Změnil se namespace presenteru? S ním se změnila i doména.
  6. Je to text z databáze, ne z NEONu? Názvy kategorií, stránek a produktů jsou *_texts řádky — v NEONu je nenajdete.
  7. 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ázevalidateClass 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.phpsetupTemplateTranslator()
doména v komponentě app/Core/Base/BaseComponent.phpsetup()
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