Translations
This page shows how to make the text of a plugin or theme translatable, and how its translations are found. Read it before you write text people will see: labels, messages, emails and admin pages.
Kestello's own text works the same way. Its template is core/resources/lang/kestello.pot; translators use Translating Kestello.
Write translatable text
Wrap every sentence people see in __(), written in English, with your plugin's slug as the domain.
In PHP:
use function Kestello\__;
use function Kestello\trans_choice;
__('Your order was placed.', domain: 'acme-shop');
__('Hello, {name}.', ['name' => $user->username], 'acme-shop');
trans_choice('{count, plural, one {# item} other {# items}} in your cart.', $count, domain: 'acme-shop');In a Vue page or component:
<script setup>
import { useI18n } from '@kestello/sdk';
const { __ } = useI18n('acme-shop');
</script>
<template>
<KCard :title="__('Orders')" :description="__('{n, plural, one {# order} other {# orders}} today.', { n: count })" />
</template>Rules that keep text translatable:
- Whole sentences. Never build a sentence from pieces: word order differs between languages.
- Named placeholders.
{name}, not%s. Translators move them where their language needs them. - ICU plurals for anything counted:
{n, plural, one {# order} other {# orders}}. Languages have up to six plural forms, and the translation adds the ones it needs. - No HTML in the text when you can avoid it. Put the markup around the sentence.
WARNING
Vue ends a text interpolation at the first pair of closing braces, so an ICU plural written inside one breaks the template. Use the message in a bound attribute, as above, or in a computed.
Text Kestello translates for you
Labels you give to Kestello are looked up in your domain first, then in core's, so you don't wrap them:
- field and block labels, help text and descriptions (
->label(),->help(),->description()), - admin menu items, pages, widgets and columns you register,
- the
title,descriptionand enum labels in your settings schema.
The translation template
The template lists every translatable sentence, with the file it comes from. Write it into your plugin's resources/lang/ folder:
php bin/cms i18n:extract acme-shop /var/www/site/content/plugins/acme-shop/resources/lang/acme-shop.pot (42 messages)The slug form needs an installed site. In a plugin's own repository, name the folder and domain instead:
php bin/cms i18n:extract --path=. --domain=acme-shop--check writes nothing and fails when the template is out of date, which is useful in CI. Commit the template, and send it to your translators.
Language packs
Translations live in the plugin's or theme's resources/lang/ folder, one file per language, named with the locale. Any of these formats works:
| File | Format |
|---|---|
sr.po | A gettext catalogue, as translation programs save it. |
sr.json | {"Your order was placed.": "Vaša porudžbina je primljena."} |
sr.php | A PHP file that returns the same pairs as an array: return ['Your order was placed.' => 'Vaša porudžbina je primljena.']; |
A sentence is looked up for the site's language and region (sr_Latn_RS), then the language (sr), then the English text. The browser gets the same translations, so one pack covers PHP and Vue: admin pages load every domain's, and site pages load the active plugins' and theme's before they are drawn (and before server-side rendering, so both show the same text). Core's own site blocks, such as the form, bring their text already translated.
If something goes wrong
The text stays in English. Check the domain: __('Orders') without a domain looks in core's translations, not yours. Then check the file name is a locale (sr.json, not serbian.json) and that the key matches the English text exactly, including punctuation.
"Finding "acme-shop" needs an installed site. Use --path=<its folder> --domain=acme-shop instead." You ran i18n:extract acme-shop in a checkout that was never installed. Use the --path form.
A sentence is missing from the template. The extractor finds __(), trans_choice() and the label methods above, written with a plain string. A sentence built at runtime (__($text)) can't be found; write the string where it is used.
Next
- Editor setup: completion for Kestello's classes in your editor.
- Admin pages.