Skip to content

Lab 11: Localization & Multi-language Support

In this lab, you will implement localization for your e-commerce application using the Symfony Translation component. You will create translation files, build a TranslationHelper service, implement a LocaleMiddleware for language detection with session persistence, and update your views to support multiple languages.


  • Session Middleware lab completed (SessionManager and SessionMiddleware must be implemented).
  • Flash Messages lab completed.
  • Working e-commerce application with views.
  • Basic understanding of JSON format.

This lab builds on the Session lab. You will use SessionManager to persist locale preferences across requests.


Step 1: Install Symfony Translation Component

Section titled “Step 1: Install Symfony Translation Component”

Open your terminal in the project root and run:

Terminal window
composer require symfony/translation

Create folders for your translation files in the project root:

Terminal window
mkdir lang
mkdir lang/en
mkdir lang/fr
/lang
/en (English translations)
/fr (French translations)

Create lang/en/messages.json with translations organized by section. Here is a minimal example:

lang/en/messages.json
{
"nav": {
"home": "Home",
"products": "Products",
"cart": "Cart"
},
"home": {
"welcome": "Welcome to our e-commerce store",
"featured_products": "Featured Products"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"errors": {
"404": {
"title": "404 - Page Not Found",
"message": "Oops! The page you are looking for does not exist."
}
}
}

Expand this file by adding translations for all pages in your application: more navigation items, product-related text, cart text, app-wide labels, and additional error messages. Use descriptive keys (e.g., products.add_to_cart not products.btn1).

You can also use placeholders for dynamic content:

{
"user": {
"greeting": "Hello, %name%! You have %count% messages."
}
}
trans('user.greeting', ['%name%' => 'Alice', '%count%' => 3])

Create lang/fr/messages.json by copying the structure from your English file and translating only the values. Keep all keys in English.

lang/fr/messages.json
{
"nav": {
"home": "Accueil",
"products": "Produits",
"cart": "Panier"
},
"home": {
"welcome": "Bienvenue dans notre boutique en ligne",
"featured_products": "Produits en Vedette"
},
"common": {
"save": "Enregistrer",
"cancel": "Annuler",
"delete": "Supprimer"
},
"errors": {
"404": {
"title": "404 - Page Non Trouvée",
"message": "Oups! La page que vous recherchez n'existe pas."
}
}
}

Both files must have identical keys. Only the values differ. Save with UTF-8 encoding to support French special characters (é, è, ê, à, ô, ç). You can validate your JSON syntax using JSONLint.


The TranslationHelper wraps Symfony’s Translator to provide a clean interface for your application. It loads translation files, manages the current locale, and provides methods to translate text.

The following are the key Translator methods you will need:

$translator = new Translator(string $locale);
$translator->setFallbackLocales(array $locales): void;
$translator->addLoader(string $format, LoaderInterface $loader): void;
$translator->addResource(string $format, string $resource, string $locale, string $domain): void;
$translator->trans(string $id, array $parameters, string $domain, string $locale): string;
$translator->setLocale(string $locale): void;

Create app/Helpers/TranslationHelper.php:

app/Helpers/TranslationHelper.php
<?php
declare(strict_types=1);
namespace App\Helpers;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\JsonFileLoader;
class TranslationHelper
{
private Translator $translator;
private string $currentLocale;
private string $defaultLocale;
private string $langPath;
private array $availableLocales;
/**
* @param string $langPath Path to language files directory
* @param string $defaultLocale Default locale (fallback)
* @param array $availableLocales List of available locales
*/
public function __construct(
string $langPath,
string $defaultLocale = 'en',
array $availableLocales = ['en', 'fr']
) {
// TODO: Initialize class properties, create a Translator instance,
// configure the fallback locale, register the JSON file loader,
// and load all translation files.
}
/**
* Load translation files for all available locales.
*/
private function loadTranslations(): void
{
// TODO: Loop through available locales, build the file path to each
// locale's messages.json, and register each existing file
// with the translator.
}
/**
* Translate a message.
*
* @param string $key Translation key (supports dot notation: 'home.welcome')
* @param array $parameters Parameters to replace in translation
* @param string|null $locale Override current locale for this translation
* @return string Translated message
*/
public function trans(string $key, array $parameters = [], ?string $locale = null): string
{
// TODO: Translate the given key using the current locale
// (or the override locale if provided).
}
/**
* Set the current locale.
*
* @param string $locale Locale code (e.g., 'en', 'fr')
* @throws \InvalidArgumentException If locale is not available
*/
public function setLocale(string $locale): void
{
// TODO: Validate that the locale is available, then update both
// the class property and the translator.
}
// TODO: Implement the following getter methods that return
// the corresponding class properties:
// - getLocale(): string
// - getDefaultLocale(): string
// - getAvailableLocales(): array
// - isLocaleAvailable(string $locale): bool
}

The LocaleMiddleware runs on every request and determines which language to use. It checks the URL query parameter first, then the session, and finally falls back to the default locale.

Create app/Middleware/LocaleMiddleware.php:

app/Middleware/LocaleMiddleware.php
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Helpers\SessionManager;
use App\Helpers\TranslationHelper;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
class LocaleMiddleware implements MiddlewareInterface
{
private TranslationHelper $translator;
public function __construct(TranslationHelper $translator)
{
$this->translator = $translator;
}
public function process(Request $request, RequestHandler $handler): ResponseInterface
{
// TODO: Detect the user's preferred locale by checking:
// 1. The URL query parameter (?lang=)
// 2. The session (saved from a previous request)
// 3. The default locale
// If a new locale was explicitly requested via URL and is valid,
// update the translator and save it to the session.
// If no URL parameter is present but a saved locale exists
// in the session, set it in the translator.
// Store the current locale as a request attribute
// and pass the request to the next handler.
}
}

Open config/constants.php and add a new constant called APP_LANG_PATH that combines APP_BASE_DIR_PATH with '/lang'. Place it after the APP_VIEWS_PATH constant.


Step 6.2: Register Services in DI Container

Section titled “Step 6.2: Register Services in DI Container”

Open config/container.php and add use statements for TranslationHelper and LocaleMiddleware.

Register TranslationHelper in the $definitions array:

TranslationHelper::class => function (ContainerInterface $container): TranslationHelper {
return new TranslationHelper(
APP_LANG_PATH,
'en',
['en', 'fr']
);
},

Register LocaleMiddleware in the $definitions array:

LocaleMiddleware::class => function (ContainerInterface $container): LocaleMiddleware {
return new LocaleMiddleware(
$container->get(TranslationHelper::class)
);
},

Open config/middleware.php and add LocaleMiddleware to the middleware stack. SessionMiddleware must be registered before LocaleMiddleware because the session must be started before locale can be read from or saved to it.

$app->add(SessionMiddleware::class); // Must come first
$app->add(LocaleMiddleware::class); // Comes after

Step 6.4: Create Global Translation Function

Section titled “Step 6.4: Create Global Translation Function”

Open config/functions.php and add a global trans() helper function at the end of the file. This makes views cleaner since you can write trans('home.welcome') instead of accessing the translator through the container.

config/functions.php
if (!function_exists('trans')) {
/**
* Translate a message using the application's translation helper.
*
* @param string $key Translation key (supports dot notation: 'home.welcome')
* @param array $parameters Parameters to replace in translation
* @param string|null $locale Override current locale for this translation
* @return string Translated message or the key itself if translation not found
*/
function trans(string $key, array $parameters = [], ?string $locale = null): string
{
global $translator;
if (!isset($translator)) {
return $key;
}
return $translator->trans($key, $parameters, $locale);
}
}

Open config/bootstrap.php and add the following code after the container is built (after $container = $containerBuilder->build();):

global $translator;
$translator = $container->get(\App\Helpers\TranslationHelper::class);

This makes the TranslationHelper instance available to the global trans() function.


Replace hardcoded text in your views with translation keys. Always wrap trans() output with hs() to prevent XSS.

Open your home page view (e.g., app/Views/homeView.php).

Before:

<?php
$page_title = 'Home';
ViewHelper::loadHeader($page_title);
?>
<h1>Welcome to Our E-Commerce Store</h1>
<p>Browse our products and enjoy a seamless shopping experience.</p>

After:

<?php
$page_title = trans('home.title');
ViewHelper::loadHeader($page_title);
?>
<h1><?= hs(trans('home.welcome')) ?></h1>
<p><?= hs(trans('home.description')) ?></p>

Before:

<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<a href="/cart">Cart</a>
</nav>

After:

<nav>
<a href="/"><?= hs(trans('nav.home')) ?></a>
<a href="/products"><?= hs(trans('nav.products')) ?></a>
<a href="/cart"><?= hs(trans('nav.cart')) ?></a>
</nav>

Apply this pattern to all views in your application: page titles, headings, labels, buttons, error messages, and any other hardcoded text.


You can add a language switcher to your header or navigation so users can change language without manually editing the URL:

<?php
global $translator;
$currentLocale = $translator->getLocale();
$availableLocales = $translator->getAvailableLocales();
?>
<?php foreach ($availableLocales as $locale): ?>
<?php if ($locale !== $currentLocale): ?>
<a href="?lang=<?= hs($locale) ?>">
<?= $locale === 'en' ? 'English' : 'Français' ?>
</a>
<?php endif; ?>
<?php endforeach; ?>

Visit http://localhost/[your-app]/ without any query parameter. The page should display in English.

Visit http://localhost/[your-app]/?lang=fr. The page should display in French. Navigate to other pages without the ?lang=fr parameter and verify the language persists (the locale was saved to the session).

Switch to French with ?lang=fr, then visit ?lang=en. The page should switch back to English and stay in English as you navigate. The URL parameter always takes priority over the session, and each selection updates the stored preference.

Visit http://localhost/[your-app]/?lang=es (Spanish is not supported). The page should fall back to the default language (English).

Temporarily use a non-existent key like trans('fake.key') in a view. It should display the key itself (“fake.key”) as a fallback.