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 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
{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á
Flash před přesměrováním do jiné domény (Scripts → front) hotový překlad jako řetězec: $this->flashMessage($this->translator->translate($this->translatorDomain . '.…'), …)
  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.

Flash, který vypíše cizí presenter, nesmí nést klíč ani objekt

Odkaz z e-mailu typicky vede na presenter, který přesměruje jinam — potvrzení newsletteru z domény scripts na frontovou hlavní stránku. Hlášku pak vypisuje cílová šablona přes {_$flash->message} se svou doménou, takže klíč ze scripts nenajde. Objekt App\Core\Utils\Controls\FlashMessage neprojde vůbec — Translator::translate() bere jen řetězec, i když je objekt Stringable. Pošli proto hotový text; druhý překlad ho díky returnOriginalMessage: true vrátí beze změny. Vzor: NewsletterAddressPresenter (Scripts) a RegistrationPresenter (Front).

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 sk → sk_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.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:

  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.

Obsahové bloky (sekce stránek, reklamy v AdvertList) mají přísnější pravidlo než názvy: cizí jazyk nedoplňují. Překlad vybírá TranslationFallback::resolve($entita, $langId, $defaultLangId):

  1. překlad jazyka stránky, pokud existuje a má activeTranslation = 1,
  2. jinak aktivní překlad výchozího jazyka lokalizace,
  3. jinak null — blok se nevykreslí.

getTranslation() neaktivní překlad nevyřadí

Entita vrátí překlad i s activeTranslation = 0. Kdo si překlad bloku vybírá sám přes getTranslation(), zobrazí text, který správce v administraci vypnul. Nový obsahový blok proto ber přes TranslationFallback; entita potřebuje getTranslation(int) (TranslationCollectionTrait), překlad isActiveTranslation() (TranslationLanguageTrait).

Tabulka může mít dva překlady téhož v jednom jazyce

Šesti tabulkám *_texts chyběl primární klíč, 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.

Data z databáze ve volbách formuláře#

Volby selectu načtené z databáze (názvy sazeb, typů, jazyků) jsou už přeložená data, ne klíče. Doménový translator formuláře by je přesto hledal jako klíče: vypíše je beze změny, ale Tracy u každé volby hlásí chybějící překlad. Jsou dvě cesty:

Cesta Co se stane
obalit text volby Contributte\Translation\Wrappers\NotTranslate translator ho vrátí beze změny (i přes prefixovaný translator); funguje se zapnutým i vypnutým translatorem, protože je Stringable
setTranslator(null) na kontrolce nepřekládají se volby ani prompt; popisek se dál překládá

Vzor obalených voleb: InvoicerSelectDataTrait v administraci Fakturace.

Po setTranslator(null) se klíč v setPrompt() vypíše syrový

Prompt přelož ručně, třeba ->setPrompt($this->translator->translate($this->translatorDomain . '.form.fields.x.prompt')). Jinak uvidí admin v prázdné volbě selectu holý klíč.

Jazyk administrace#

Administrace jazyk uživatele (User::$languageId) nečte vůbec. Rozhoduje adresa, v tomto pořadí:

Přednost Zdroj Příklad
1 persistentní parametr lang /admin/system-nastaveni/prehled?lang=en
2 jazyk lokalizace z předpony adresy (loc, výchozí lokalizace předponu nemá) /en/admin/system-nastaveni/prehled
3 neznámá lokalizace en

Přepínač v hlavičce (LocaleSwitcher) nabídne aktivní lokalizace, pro jejichž jazyk existuje aspoň jeden soubor app/Locale/<modul>/<locale>/admin.<LOCALE>.neon; víc lokalizací téhož jazyka se slije do jedné (vyhraje hlavní). Odkaz vede na loc = <klíč lokalizace> a zároveň ruší lang.

Odkaz s lang přebije předponu na všech dalších stránkách

lang je persistentní, takže se veze v každém odkazu dál a přepínač pak zdánlivě nefunguje. Jazyk administrace měňte předponou (linkParams()), ne parametrem lang.

Návrat po uložení formuláře (resolveBackUrl()) nebere jako předchozí stránku tutéž stránku v jiném jazyce (isSamePageIgnoringLocale()), jinak by „zpět“ po přepnutí jazyka vedlo na editaci s původní předponou.

Postup: přidávám jazyk administrace

  1. Přeložte admin.<LOCALE>.neon aspoň v jednom modulu; bez souboru přepínač jazyk nenabídne.
  2. Lokalizace s tímto jazykem musí být aktivní.
  3. Ověřte přepnutí na editaci záznamu a uložení: návrat vede na výpis v novém jazyce.

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
výběr překladu obsahového bloku app/Core/Utils/Helpers/TranslationFallback.php
texty výsledkových kódů app/Locale/results/ + app/Core/Utils/Results/ResultCode.php
jazyk administrace a přepínač app/UI/Admin/System/Models/Helpers/AdminLocaleOptions.php, app/UI/Admin/System/Components/LocaleSwitcherComponent/
závazná konvence skills/cs-translations/SKILL.md

Navazující kapitoly: Routování · Formuláře · Cache · Konvence kódu

Nastavení cookies