Lab 11: Localization & Multi-language Support
Resources
Section titled “Resources”Overview
Section titled “Overview”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.
Prerequisites
Section titled “Prerequisites”- Session Middleware lab completed (
SessionManagerandSessionMiddlewaremust 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:
composer require symfony/translationStep 2: Create Directory Structure
Section titled “Step 2: Create Directory Structure”Create folders for your translation files in the project root:
mkdir langmkdir lang/enmkdir lang/fr/lang /en (English translations) /fr (French translations)Step 3: Create Translation Files
Section titled “Step 3: Create Translation Files”Step 3.1: Create English Translation File
Section titled “Step 3.1: Create English Translation File”Create lang/en/messages.json with translations organized by section. Here is a minimal example:
{ "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])Step 3.2: Create French Translation File
Section titled “Step 3.2: Create French Translation File”Create lang/fr/messages.json by copying the structure from your English file and translating only the values. Keep all keys in English.
{ "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 ↗.
Step 4: Create TranslationHelper Class
Section titled “Step 4: Create TranslationHelper Class”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:
<?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}Step 5: Create LocaleMiddleware
Section titled “Step 5: Create LocaleMiddleware”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:
<?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. }}Step 6: Update Configuration
Section titled “Step 6: Update Configuration”Step 6.1: Add Language Path Constant
Section titled “Step 6.1: Add Language Path Constant”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) );},Step 6.3: Register Middleware
Section titled “Step 6.3: Register Middleware”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 afterStep 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.
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); }}Step 6.5: Initialize Global Translator
Section titled “Step 6.5: Initialize Global Translator”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.
Step 7: Update Views to Use Translations
Section titled “Step 7: Update Views to Use Translations”Replace hardcoded text in your views with translation keys. Always wrap trans() output with hs() to prevent XSS.
Home Page
Section titled “Home Page”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>Navigation
Section titled “Navigation”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.
Optional: Language Switcher
Section titled “Optional: Language Switcher”You can add a language switcher to your header or navigation so users can change language without manually editing the URL:
<?phpglobal $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; ?>Testing Your Implementation
Section titled “Testing Your Implementation”Default Language
Section titled “Default Language”Visit http://localhost/[your-app]/ without any query parameter. The page should display in English.
Switch to French
Section titled “Switch to French”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).
Session Persistence
Section titled “Session Persistence”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.
Invalid Language Code
Section titled “Invalid Language Code”Visit http://localhost/[your-app]/?lang=es (Spanish is not supported). The page should fall back to the default language (English).
Missing Translation Key
Section titled “Missing Translation Key”Temporarily use a non-existent key like trans('fake.key') in a view. It should display the key itself (“fake.key”) as a fallback.