Internationalization
Internationalization prepares an application for more than one locale. Localization is the locale-specific part: translated text, plural forms, date and number formats, and view templates for a locale such as en-US or de.
In a Yii application, the usual parts are:
- locale selection for the current request;
- translated application messages;
- plural and parameter formatting;
- localized view templates for pages that need different markup.
This tutorial assumes a new yiisoft/app project.
Install packages
yiisoft/app already includes yiisoft/i18n and yiisoft/translator. Install PHP-file message storage:
composer require yiisoft/translator-message-phpInstall the extractor as a development dependency:
composer require --dev yiisoft/translator-extractorThe examples use ICU message formatting for plural forms. Make sure the PHP intl extension is enabled. For simple placeholders only, use Yiisoft\Translator\SimpleMessageFormatter in the configuration below.
Configure application locale
The application template keeps basic application settings in config/common/application.php. Set the source locale there:
<?php
declare(strict_types=1);
return [
'charset' => 'UTF-8',
'locale' => 'en-US',
'name' => 'My Project',
];The default layout uses this value in the <html lang=""> attribute through ApplicationParams. When your application chooses locale per request, set the selected locale on the view as shown in Choosing locale per request.
Configure translator defaults in config/common/params.php:
'yiisoft/translator' => [
'locale' => 'en-US',
'fallbackLocale' => 'en-US',
'defaultCategory' => 'app',
],locale is the default translation locale. fallbackLocale is used when a translation is missing for the current locale. defaultCategory is the category used when a call to translate() does not specify one.
Configure message storage
Create config/common/di/translator.php:
<?php
declare(strict_types=1);
use Yiisoft\Aliases\Aliases;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\Message\Php\MessageSource;
/** @var array $params */
return [
'translation.app' => [
'definition' => static function (Aliases $aliases) use ($params): CategorySource {
$messageSource = new MessageSource($aliases->get('@root/messages'));
return new CategorySource(
$params['yiisoft/translator']['defaultCategory'],
$messageSource,
new IntlMessageFormatter(),
$messageSource,
);
},
'tags' => ['translation.categorySource'],
],
];This registers the app category. The MessageSource reads PHP arrays from the messages/ directory and writes extracted messages back to the same directory.
Create the source and target message files:
messages/
├── de/
│ └── app.php
└── en-US/
└── app.phpmessages/en-US/app.php:
<?php
return [
'home.title' => 'Home',
'home.welcome' => 'Welcome to Yii3',
'cart.items' => '{count, plural, =0{No items} one{# item} other{# items}}',
];messages/de/app.php:
<?php
return [
'home.title' => 'Home',
'home.welcome' => 'Willkommen bei Yii3',
'cart.items' => '{count, plural, =0{Keine Artikel} one{# Artikel} other{# Artikel}}',
];Translate messages
Inject Yiisoft\Translator\TranslatorInterface where you prepare data for a response. For the default home page action, update src/Web/HomePage/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\HomePage;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
private TranslatorInterface $translator,
) {}
public function __invoke(): ResponseInterface
{
return $this->viewRenderer->render(__DIR__ . '/template', [
'itemCount' => 3,
'translator' => $this->translator,
]);
}
}Use the translator in src/Web/HomePage/template.php:
<?php
declare(strict_types=1);
use App\Shared\ApplicationParams;
use Yiisoft\Html\Html;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\View\WebView;
/**
* @var WebView $this
* @var ApplicationParams $applicationParams
* @var int $itemCount
* @var TranslatorInterface $translator
*/
$this->setTitle($translator->translate('home.title'));
?>
<div class="text-center">
<h1><?= Html::encode($translator->translate('home.welcome')) ?></h1>
<p><?= Html::encode($translator->translate('cart.items', ['count' => $itemCount])) ?></p>
</div>Escape translated text before output. Store trusted HTML separately or render it from a view template.
Choosing locale per request
For a web application, choose locale early in the middleware stack and apply it to both the translator and the view. The example below uses the lang query parameter and a fixed allow list:
<?php
declare(strict_types=1);
namespace App\Web\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Translator\TranslatorInterface;
use Yiisoft\View\WebView;
final readonly class LocaleMiddleware implements MiddlewareInterface
{
private const DEFAULT_LOCALE = 'en-US';
private const SUPPORTED_LOCALES = ['en-US', 'de'];
public function __construct(
private TranslatorInterface $translator,
private WebView $view,
) {}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$locale = $this->getLocale($request);
$this->translator->setLocale($locale);
$this->view->setLocale($locale);
return $handler->handle($request);
}
private function getLocale(ServerRequestInterface $request): string
{
$locale = $request->getQueryParams()['lang'] ?? null;
return is_string($locale) && in_array($locale, self::SUPPORTED_LOCALES, true)
? $locale
: self::DEFAULT_LOCALE;
}
}Register the middleware in config/common/routes.php or in the route group that needs localized output. Add middleware before the route action:
use App\Web\Middleware\LocaleMiddleware;
use Yiisoft\Router\Route;
return [
Route::get('/')
->middleware(LocaleMiddleware::class)
->action(App\Web\HomePage\Action::class)
->name('home'),
];Update src/Web/Shared/Layout/Main/layout.php to read the locale from the current view state:
<html lang="<?= Html::encode($this->getLocale()) ?>">Use the same approach when locale is stored in a session, a cookie, a user profile, or a route parameter.
yiisoft/i18n provides Yiisoft\I18n\Locale for working with BCP 47 locale codes:
use Yiisoft\I18n\Locale;
$locale = new Locale('de-DE');
echo $locale->language(); // de
echo $locale->region(); // DE
echo $locale->fallbackLocale()->asString(); // deThis is useful when you need to derive a language fallback or inspect locale parts before choosing a supported locale.
Localized view files
Use message translation for labels, headings, buttons, validation messages, and short paragraphs. Use localized view files when a page needs different markup or a different amount of text for each locale.
yiisoft/view looks for localized files in a locale subdirectory next to the original view file. For example, when src/Web/HomePage/template.php is rendered with locale de-DE, Yii checks:
src/Web/HomePage/de-DE/template.phpsrc/Web/HomePage/de/template.phpsrc/Web/HomePage/template.php
The fallback to the two-letter language code is automatic.
You can set the locale for a single render call:
return $this->viewRenderer
->withLocale('de-DE')
->render(__DIR__ . '/template');When the locale is set on WebView in middleware, the same locale is applied to views rendered by WebViewRenderer.
Extract messages
Configure the extractor so it can read existing messages and write new IDs to the same message storage.
Create config/common/di/translator-extractor.php:
<?php
declare(strict_types=1);
use Yiisoft\Aliases\Aliases;
use Yiisoft\Definitions\DynamicReference;
use Yiisoft\Translator\Message\Php\MessageSource;
use Yiisoft\TranslatorExtractor\CategorySource as ExtractorCategorySource;
use Yiisoft\TranslatorExtractor\Extractor;
/** @var array $params */
return [
Extractor::class => [
'__construct()' => [
[
DynamicReference::to([
'class' => ExtractorCategorySource::class,
'__construct()' => [
$params['yiisoft/translator']['defaultCategory'],
DynamicReference::to(
static fn (Aliases $aliases): MessageSource => new MessageSource($aliases->get('@root/messages')),
),
DynamicReference::to(
static fn (Aliases $aliases): MessageSource => new MessageSource($aliases->get('@root/messages')),
),
],
]),
],
],
],
];Rebuild config after adding the extractor configuration file:
composer yii-config-rebuildExtract messages from application source files:
./yii translator/extract src --languages=en-US,deThe command scans calls such as:
$translator->translate('home.welcome');
$translator->translate('cart.items', ['count' => $itemCount]);It writes missing IDs to:
messages/en-US/app.php
messages/de/app.phpAfter extraction, edit target language files and replace source IDs with translated text.
Use --category when a call belongs to another category:
./yii translator/extract src --languages=en-US,de --category=shopThe extractor excludes vendor/ by default. Use --only or --except when you need a narrower scan:
./yii translator/extract src --languages=en-US,de --only=**/*.php --except=**/runtime/**Working with categories
The default app category is enough for most application messages. Add categories when a package or a large feature owns its own message files.
For a shop category, create another tagged category source:
'translation.shop' => [
'definition' => static function (Aliases $aliases): CategorySource {
$messageSource = new MessageSource($aliases->get('@root/messages'));
return new CategorySource(
'shop',
$messageSource,
new IntlMessageFormatter(),
$messageSource,
);
},
'tags' => ['translation.categorySource'],
],Translate with the category name:
$translator->translate('checkout.submit', category: 'shop');The message files are stored as:
messages/de/shop.php
messages/en-US/shop.phpKeep message IDs stable. Changing an ID creates a new translation task for every locale.