Internationalization (i18n)
Piko provides internationalization support through a dedicated, framework-agnostic package that can also be used in standalone PHP projects:
composer require piko/i18n
This installs the Piko\I18n component, which loads translations from PHP files and exposes a simple API for translating strings.
Translation files
Translations are stored in plain PHP files that return an array of key–value pairs:
- Key: the source string (usually in your default language).
- Value: the translated string.
- File name: the current language code followed by
.php(e.g.en.php,fr.php).
Example directory structure:
app/
├── messages/
│ ├── en.php
│ └── fr.php
Example of a translation file (fr.php):
<?php
return [
'Translation test' => 'Test de traduction',
'Hello {name}' => 'Bonjour {name}',
];
Piko\I18n will look for files named <language>.php inside the directory configured for each domain (see below).
Using I18n in a standalone PHP application
The piko/i18n package can be used without the rest of the Piko framework. At minimum you need:
- A
messagesdirectory containing one file per language (as shown above). - An instance of
Piko\I18nconfigured with your domains and base language.
Example project structure:
project-root/
├── messages/
│ ├── en.php
│ └── fr.php
├── composer.json
└── index.php
messages/fr.php:
<?php
return [
'Translation test' => 'Test de traduction',
'Hello {name}' => 'Bonjour {name}',
];
index.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Piko\I18n;
use function Piko\I18n\__;
// Create the I18n instance
$i18n = new I18n([
// domain => directory containing <language>.php files
'app' => __DIR__ . '/messages',
], 'fr'); // default language is "fr"
// Optional: register this instance for the __() helper
I18n::setInstance($i18n);
// Direct usage
echo $i18n->translate('app', 'Hello {name}', ['name' => 'John']) . "\n";
// Outputs: Bonjour John
// Using the helper
echo __('app', 'Translation test') . "\n";
// Outputs: Test de traduction
You can omit I18n::setInstance() if you do not plan to use the __() helper and only call $i18n->translate() directly.
Registering the I18n component in a Piko application
Declare the component in the application configuration. The key of the components array must match the fully-qualified class name used when retrieving the component:
<?php
return [
// ...
'components' => [
Piko\I18n::class => [
// Domain => path (can use aliases such as @app)
'translations' => [
'app' => '@app/messages',
// 'user' => '@app/messages/user',
],
// Optional: default language (defaults to 'en')
// 'language' => 'fr',
],
],
];
Notes:
- Each domain (e.g.
app,user) maps to a directory containing one file per language. - Piko aliases such as
@appare resolved viaPiko::getAlias(), so@app/messagesmust point to a real directory in your project (typicallyapp/messages). - The
languageproperty controls which<language>.phpfile is loaded.
Basic usage
Access the I18n component from your application and call translate():
<?php
/** @var Piko\Application $app */
$i18n = $app->getComponent(Piko\I18n::class);
// Simple translation
echo $i18n->translate('app', 'Translation test') . '<br>'; // "Test de traduction" (if language = 'fr')
// Translation with parameters
echo $i18n->translate('app', 'Hello {name}', ['name' => 'John']) . '<br>'; // "Bonjour John"
Behavior:
- If the domain is not configured or the key is missing,
translate()returns the original$textunchanged. - Placeholders in the form
{name}are replaced using the values from$params. - If
$textisnull,translate()returnsnull.
Using the __() helper function
The package provides a global helper function Piko\I18n\__() as a proxy to I18n::translate():
use function Piko\I18n\__;
Before using __(), you must register the active I18n instance so the helper knows which component to use. Otherwise, calling __() will throw a RuntimeException.
A typical bootstrap/entry script looks like this:
<?php
use Piko\ModularApplication;
use Piko\I18n;
use function Piko\I18n\__;
require __DIR__ . '/../vendor/autoload.php';
$config = require __DIR__ . '/../config/app.php';
$app = new ModularApplication($config);
// Register the I18n instance for the __() helper
$I18n = $app->getComponent(I18n::class);
I18n::setInstance($I18n);
// Now you can safely use __()
echo __('app', 'Translation test') . '<br>';
// Test de traduction
echo __('app', 'Hello {name}', ['name' => 'John']) . '<br>';
// Bonjour John
$app->run();
The helper behaves exactly like I18n::translate():
__('app', 'Hello {name}', ['name' => 'John']);
// is equivalent to
$I18n->translate('app', 'Hello {name}', ['name' => 'John']);
For details, see the Piko\I18n\__ API documentation.
Dynamic language selection in a Piko application
Multi-language applications typically need to switch the language dynamically based on the current request.
Piko\I18n does not impose a specific strategy, but it is designed to be used from middleware. A common approach is:
- Read the language code from the first segment of the request URI (e.g.
/fr/blog/...). - Strip that segment from the URI so routing continues to work as usual.
- Update the
I18ncomponent’slanguageproperty. - Prefix all generated URLs with the active language so links stay consistent.
Example I18nMiddleware
The middleware below implements this strategy and integrates with the router’s AfterUriBuiltEvent to ensure generated URLs are prefixed with the current language:
<?php
namespace app\lib;
use Piko\Application;
use Piko\I18n;
use Piko\Router;
use Piko\Router\AfterUriBuiltEvent;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class I18nMiddleware implements MiddlewareInterface
{
/** @var I18n */
private $i18n;
public function __construct(Application $app)
{
$router = $app->getComponent(Router::class);
assert($router instanceof Router);
$i18n = $app->getComponent(I18n::class);
assert($i18n instanceof I18n);
// Prefix all generated URLs with the current language
$router->on(
AfterUriBuiltEvent::class,
fn (AfterUriBuiltEvent $event) => $event->uri = '/' . $i18n->language . $event->uri
);
$this->i18n = $i18n;
}
/**
* {@inheritDoc}
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$uri = $request->getUri();
$uriPath = $uri->getPath();
/*
* Extract the language from the URI path.
* Example: with "/fr/blog" it will extract "fr",
* set the I18n language to "fr" and rewrite the path to "/blog".
*/
if (preg_match('/^\/([a-z]{2})\//', $uriPath, $match)) {
$lang = $match[1];
$uriPath = preg_replace('/^\/' . $lang . '\//', '/', $uriPath);
// Update the request URI so routing ignores the language prefix
$request = $request->withUri($uri->withPath($uriPath));
// Update the I18n language so subsequent translations use this locale
$this->i18n->language = $lang;
}
return $handler->handle($request);
}
}
Wiring the middleware
Register the middleware in the entry script so it runs for every request:
<?php
use Piko\ModularApplication;
use app\lib\I18nMiddleware;
require __DIR__ . '/../vendor/autoload.php';
$config = require __DIR__ . '/../config/app.php';
$app = new ModularApplication($config);
$app->pipe(new I18nMiddleware($app));
$app->run();
With this setup:
- A request to
/fr/blogwill be internally routed as/blogwithI18n::$language === 'fr'. - Calls to
Router::getUrl()will emit URLs prefixed with/fr(or the currently active language). - The same mechanism works regardless of the number of languages, as long as your URIs start with a two-letter language code.