Translating a Theme
A theme keeps its own wording in its own locale files, one file per page. The operator can reword any of it from the panel, without editing a file.
Overview
No visible string is written into a template. Every label, button and accessible name comes from a locale key. The theme resolves it against its own files first, the platform's second.
The developer writes the defaults, one file per page. The operator rewrites them from the content editor. Those edits land in a generated tree a theme update never overwrites.
Structure
Where Text Lives
name and a description key here override the manifest's. The panel's theme list shows them translated.
What a Scope Is
A scope is the page's public identity, not its file path. For an ordinary view the two match; for a file-based marketing page they do not.
| View | Scope | Locale file |
|---|---|---|
views/home.tpl | home | locale/en/home.php |
views/account/settings.tpl | account/settings | locale/en/account/settings.php |
views/page/about.tpl | about | locale/en/about.php |
The last row matters. A file-based page is addressed at its bare slug, so the page/ segment is stripped before the scope resolves.
Step by Step
Add a Shared String
- Put the key in the common file of every installed language, not only yours. A key missing from a language falls through to English, a silent half-translation.
- Prefix by region when the key belongs to the chrome (
hdr_,ftr_). The common file is the only place a prefix helps. - Print it in the partial and open the page in both languages.
Add a Page's Own Strings
- Create the scope file, one per language, named after the page's public identity.
- Write short keys: the file is already the scope, so
hero_cta, nothome_hero_cta. Two pages reusing a short key never collide, because only one view scope is loaded per request. - Open the page. The scope is loaded before the view runs, and the page appears in the content editor.
Print It With Values
- Call the language function with the key.
- Pass a value by adding an attribute of the same name:
countreplaces{count}in the string. Any number of them, any names you like. - Pass values as they are. The function escapes each one, so a name or title a customer typed cannot add markup.
- Keep the placeholder in the value and the sentence order in the translation. Never build a sentence by concatenating two keys around a value: word order differs per language.
Print a Count
- Write the key's value in the plural, with
{n}where the number goes. - Add a sibling key per extra form:
<key>-one, and in Ukrainian, Russian or Polish also<key>-fewand<key>-many. - Print it with
plural=$n. The form that number needs is picked, and{n}is filled with it. - If a script changes the number later, also print
forms=1into a data attribute. Re-pick the word withwindow.wcpPlural()fromtemplates/admin/js/plural.js.
Add a Language
- Copy the English tree, common file and every scope file, into the new language code.
- Translate the values and leave the keys untouched. A renamed key is a missing key in that language only.
- Check the pages that carry counts and dates. Their placeholders move, and that is where a mechanical translation breaks first.
- Write the count siblings your language needs. A language without them shows the plural for every number.
Reference
Resolution Order
Every lookup walks this list and stops at the first hit. The last step makes an unresolved key visible instead of blank.
| Step | Source | Why it is there |
|---|---|---|
| 1 | Operator override, active language | Panel edits win over everything, even a theme update |
| 2 | Theme locale, active language (common + loaded scope) | The developer's default wording |
| 3 | Theme locale, English | An untranslated language still works |
| 4 | The platform's own language files | Path-style keys like website/index/logo-alt keep working from a theme |
| 5 | The key itself | A missing string shows as its key, found in review rather than production |
The Translation API
// $vars is a replacement map with the braces INCLUDED: ['{count}' => 3].
// This is what the template function calls; use it from PHP for the same result.
public function lang(string $key, array $vars = []): string;
// Every form of a count key, all from ONE language. The key's own value is 'other':
// ['one' => '{n} service', 'other' => '{n} services'].
public function lang_forms(string $key): array;
// The form $n reads with, $vars applied. '{n}' is the number unless $vars sets it.
public function lang_plural(string $key, int|float $n, array $vars = []): string;
// Load a view's scope before rendering it. Called for you on the request path and by the
// context-free render; call it yourself only when you render a view by hand.
public function loadViewContent(string $view): void;
// Another theme's locale, without booting it as the active theme (the panel's theme list
// reads a theme's own name and description this way). Falls back to English.
public static function localeFor(string $themeName, string $lang = ''): array;
// '' when the name is not a usable scope. A scope is [a-zA-Z0-9/_-] and may not climb out.
public static function cleanScope(string $scope): string;
// Every editable scope of a theme: 'common' first, then the page scopes, sorted.
public static function contentScopes(string $themeName): array;
// The developer's defaults of one scope in one language (string values only).
public static function scopeDefaults(string $themeName, string $lang, string $scope): array;
// The operator's overrides of the same scope. Empty when nothing was changed.
public static function scopeOverrides(string $themeName, string $lang, string $scope): array;
// Persist them. An EMPTY set deletes the file, which is how "reset to default" is stored:
// an override is an exception, never a copy of the default.
public static function writeScopeOverrides(string $themeName, string $lang, string $scope, array $values): bool;
The Template Function
count=$n replaces {count} in the value. The braces belong to the string, not to the call. Each value is escaped on the way in; a character reference already in it is not doubled.
-one, -few and -many siblings add the rest. {n} holds the number.
forms=1 the function returns every form as JSON, safe inside an attribute, for a script that re-labels a count.
raw is the only way to pass markup through one.
lang('key') takes the key, plural=n and forms=true. It has no placeholder attributes.
Example
One page's strings in two languages, and the view that prints them. The placeholder lives in the value; sentence order differs between the files.
return [
// Short keys: the FILE is the scope, so a page prefix would only repeat it.
'title' => 'Services',
'lede' => 'Everything running on your account, with its next renewal date.',
'count' => '{n} services',
'count-one' => '{n} service',
'empty_title' => 'No services yet',
'empty_text' => 'Your first order will show up here.',
'renew_cta' => 'Renew',
'aria_list' => 'Service list',
];
return [
// Same keys, translated values. A renamed key here is a missing key in Turkish only.
'title' => 'Hizmetler',
'lede' => 'Hesabınızda çalışan her şey ve bir sonraki yenileme tarihi.',
// Turkish reads the same for every number, so 'count' needs no sibling.
'count' => '{n} hizmet',
'empty_title' => 'Henüz hizmet yok',
'empty_text' => 'İlk siparişiniz burada görünecek.',
'renew_cta' => 'Yenile',
'aria_list' => 'Hizmet listesi',
];
<h1>{lang key='title'}</h1>
<p>{lang key='lede'}</p>
{if $services}
{* plural= picks the form for this number and fills {n}; any other attribute fills its own placeholder. *}
<p>{lang key='count' plural=$services|@count}</p>
<ul aria-label="{lang key='aria_list'}">
{foreach $services as $service}
<li>
{$service.name}
<a href="{link route='service-detail' p1=$service.id}">{lang key='renew_cta'}</a>
</li>
{/foreach}
</ul>
{else}
{* Accessible names are strings too: nothing visible OR announced is hardcoded. *}
<h2>{lang key='empty_title'}</h2>
<p>{lang key='empty_text'}</p>
{/if}
{* A platform key, in its path form: the same wording every theme shares. *}
<a href="{link route='home'}" aria-label="{lang key='website/index/logo-alt'}">{$company_name}</a>
Pitfalls
Put the file at the slug's own path, not under the view's folder. Left where the view is, it exists, it lints, and all its strings appear as keys.
Only the common scope and the current view's scope are loaded per request. A key borrowed from another page may resolve in development and print as its key in production. Shared wording belongs in the common file.
Written by the panel, kept out of version control, holding only the differences. Editing it by hand puts a value where the next panel save overwrites it. Shipping it in a theme package hands your test wording to every installation.
A check like {if $n == 1} knows two forms. Ukrainian and Russian have three for whole numbers, and each language decides where they fall. Use plural=.
Name only markup the template built from escaped variables. A name, title or message a customer typed never goes under raw.
Strings every theme shares already exist and are already translated. A copy inside a theme drifts, and it stops the operator's platform-wide wording change from reaching your theme.
Related Articles
Дякуємо за відгук!
Наша служба підтримки на зв’язку цілодобово з усього, чого ви не знайшли вище.