Lab: Localization & Multi-language Support
Resources
Section titled “Resources”- Symfony Translation Documentation ↗
- PHP Internationalization (i18n) ↗
- W3C Internationalization Best Practices ↗
Overview
Section titled “Overview”In this lab, you’ll implement a complete localization (i18n) system for your e-commerce application using Symfony Translation component. You’ll enable your application to support multiple languages, allowing users to view content in their preferred language.
What is Localization?
Localization (also called internationalization or i18n) is the process of adapting your application to support multiple languages and regional settings. Instead of hardcoding text like “Welcome” in your views, you’ll use translation keys like trans('home.welcome') that display different text based on the user’s language preference.
Real-World Applications:
- E-commerce sites serving multiple countries
- SaaS applications with global user base
- Government and educational websites
- Mobile applications with international users
Key Features of Symfony Translation
Section titled “Key Features of Symfony Translation”Symfony Translation is a powerful and flexible component that provides enterprise-grade internationalization (i18n) capabilities. Here are the main features you’ll be using in this lab:
1. Multi-Language Support
Section titled “1. Multi-Language Support”Easily support multiple languages by organizing translations in separate files. Switch between languages seamlessly at runtime.
// Englishtrans('home.welcome') // → "Welcome to our e-commerce store"
// French (when locale is set to 'fr')trans('home.welcome') // → "Bienvenue dans notre boutique en ligne"2. Dynamic Content with Placeholders
Section titled “2. Dynamic Content with Placeholders”Insert dynamic values into translations using placeholders, making your translations flexible and reusable.
// Translation: "Hello, %name%! You have %count% new messages."trans('user.greeting', ['%name%' => 'John', '%count%' => 5])// → "Hello, John! You have 5 new messages."3. Automatic Fallback Mechanism
Section titled “3. Automatic Fallback Mechanism”If a translation is missing in the requested language, Symfony automatically falls back to your default language, ensuring your application never displays broken text.
// If 'products.new_arrival' doesn't exist in Frenchtrans('products.new_arrival') // → Falls back to English version4. Multiple File Format Support
Section titled “4. Multiple File Format Support”Symfony supports various translation file formats. In this lab, we’ll use JSON for its simplicity and readability, but you can also use YAML, PHP arrays, XLIFF, and more.
// Clean, readable JSON format{ "nav": { "home": "Home", "products": "Products" }}5. Organized Translation Keys
Section titled “5. Organized Translation Keys”Use dot notation to organize translations hierarchically, keeping your translation files clean and maintainable.
trans('errors.404.title') // → "404 - Page Not Found"trans('errors.404.message') // → "Oops! The page..."trans('products.add_to_cart') // → "Add to Cart"trans('common.save') // → "Save"6. Locale Detection and Management
Section titled “6. Locale Detection and Management”Built-in support for locale management with easy switching between languages using query parameters, sessions, or custom logic.
// Switch to French$translator->setLocale('fr');
// Get current locale$translator->getLocale(); // → "fr"7. Translation Domains
Section titled “7. Translation Domains”Organize translations into domains (categories) to separate different parts of your application or share translations across projects.
// In this lab we use the 'messages' domain (default)trans('home.welcome', [], 'messages')8. Performance Optimized
Section titled “8. Performance Optimized”Symfony Translation uses caching to ensure translations are loaded efficiently, even with large translation files.
Learning Objectives
Section titled “Learning Objectives”- Install and configure Symfony Translation component
- Create and organize JSON translation files
- Build a TranslationHelper service to manage translations
- Implement LocaleMiddleware for language detection with session persistence
- Update views to use translation keys instead of hardcoded text
- Apply XSS protection when displaying translated content
- Understand fallback mechanisms for missing translations
Prerequisites
Section titled “Prerequisites”- Session Middleware lab completed (SessionManager and SessionMiddleware must be implemented)
- Flash Messages lab completed
- Understanding of middleware concept
- Working e-commerce application with views
- Basic understanding of JSON format
- Familiarity with dependency injection
Important: This lab builds on the Session lab. You will use the SessionManager class (with static methods) that you created in the Session lab to persist locale preferences across requests.
How Localization Works
Section titled “How Localization Works”Translation Flow:
- User visits your application (optionally with language parameter like
?lang=fr) - LocaleMiddleware detects language based on URL parameter, session, or default
- Application loads the appropriate translation file (
lang/fr/messages.jsonfor French) - Views use
trans('home.welcome')instead of hardcoded “Welcome” - TranslationHelper returns “Bienvenue” from French translations
- User sees content in French
Language Detection Priority:
- URL Parameter -
?lang=fr(user explicitly selects language - highest priority) - Session Value - Previously saved preference (persists across requests)
- Default Locale - English (fallback when no parameter or session value)
File Organization:
/lang /en messages.json → English translations /fr messages.json → French translationsTranslation Keys: Use dot notation to organize translations hierarchically:
{ "home": { "title": "Home", "welcome": "Welcome to our store" }, "nav": { "home": "Home", "products": "Products" }}Access with: trans('home.welcome') or trans('nav.home')
Lab Instructions
Section titled “Lab Instructions”The following steps will guide you through the process of implementing localization in your application. Please follow the steps in order and complete each step before moving on to the next one.
Step 1: Install Symfony Translation Component
Section titled “Step 1: Install Symfony Translation Component”Objective: Add Symfony Translation library to your project.
Open your terminal in the project root and run:
composer require symfony/translationWhat Gets Installed:
symfony/translation- Core translation functionalitysymfony/translation-contracts- Translation interfacessymfony/polyfill-mbstring- Multi-byte string supportsymfony/deprecation-contracts- Compatibility layer
Verify Installation:
Check that vendor/symfony/translation directory exists and composer.json includes the dependency.
Step 2: Create Directory Structure
Section titled “Step 2: Create Directory Structure”Objective: Set up folders for translation files.
Create the following directory structure in your project root:
mkdir langmkdir lang/enmkdir lang/frResult:
/lang /en (English translations) /fr (French translations)Why separate folders? Each language gets its own folder to keep translations organized and make it easy to add new languages later.
Step 3: Create Translation Files
Section titled “Step 3: Create Translation Files”Objective: Create JSON files with translations for English and French.
3.1: Create English Translation File
Section titled “3.1: Create English Translation File”Objective: Create a JSON file with English translations organized by sections.
Create lang/en/messages.json with the following structure:
Understanding Translation Groups:
Translations are organized using dot notation to create logical groups. Each group represents a section of your application:
app.*- Application-wide settings (title, name, etc.)nav.*- Navigation menu itemshome.*- Home page specific textproducts.*- Product-related textcart.*- Shopping cart textcommon.*- Reusable text across the app (buttons, actions)errors.*- Error messages grouped by error code
Your Task:
Create the file following this structure. Here’s a minimal example showing how to group translations:
{ "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." } }}Now expand this file by adding:
- More navigation items (login, logout, etc.)
- Product-related translations (add_to_cart, price, description, etc.)
- Cart translations (title, empty, quantity, total, etc.)
- App-wide translations (title, description, name)
- More common actions and error messages
Tips:
- Add translations for ALL pages in your application
- Keep keys descriptive (e.g.,
products.add_to_cartnotproducts.btn1) - Use nested groups for related errors (e.g.,
errors.404.title,errors.404.message) - Validate your JSON syntax using JSONLint ↗
3.2: Create French Translation File
Section titled “3.2: Create French Translation File”Objective: Translate the English file to French while keeping the same structure.
Your Task:
- Copy the entire structure from your
lang/en/messages.jsonfile - Translate ONLY the values to French (keep all keys in English)
- Ensure both files have identical structure
Example (translate the values):
{ "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." } }}Translation Hints:
- Use Google Translate or a French dictionary
- Common translations: Home → Accueil, Products → Produits, Cart → Panier, Welcome → Bienvenue, Save → Enregistrer, Delete → Supprimer
- Don’t forget French special characters: é, è, ê, à, ô, ç
Important:
- Both files must have identical keys (only values differ)
- Save with UTF-8 encoding to support special characters
- Validate JSON syntax using JSONLint ↗
Step 4: Create TranslationHelper Class
Section titled “Step 4: Create TranslationHelper Class”Objective: Build a service to manage translations using Symfony Translation component.
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.
Create app/Helpers/TranslationHelper.php:
<?php
declare(strict_types=1);
namespace App\Helpers;
use Symfony\Component\Translation\Translator;use Symfony\Component\Translation\Loader\JsonFileLoader;
/** * Translation Helper * * Provides translation functionality using Symfony Translation component. * Supports multiple locales with JSON-based translation files. */class TranslationHelper{ private Translator $translator; private string $currentLocale; private string $defaultLocale; private string $langPath; private array $availableLocales;
/** * Initialize the Translation Helper * * @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: Store all constructor parameters in their corresponding class properties // Hint: You have 4 properties to initialize (langPath, defaultLocale, currentLocale, availableLocales) // Note: currentLocale should start as the same value as defaultLocale
// TODO: Create a new Translator instance and store it in $this->translator // Hint: Pass the current locale to the Translator constructor
// TODO: Configure the translator to fall back to the default locale if a translation is missing // Hint: Use the setFallbackLocales() method with an array containing the default locale
// TODO: Register the JSON file loader with the translator // Hint: Use addLoader() method with 'json' as the format name
// TODO: Load all translation files // Hint: Call the loadTranslations() method }
/** * Load translation files for all available locales */ private function loadTranslations(): void { // TODO: Loop through each locale in availableLocales array
// TODO: For each locale, build the file path to messages.json // Hint: Use DIRECTORY_SEPARATOR constant for cross-platform compatibility // Example path: lang/en/messages.json
// TODO: Check if the file exists before trying to load it
// TODO: If file exists, register it with the translator using addResource() // Parameters: format ('json'), file path, locale code, domain ('messages') }
/** * 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: If no locale is provided, use the current locale // Hint: Use the null coalescing operator (??)
// TODO: Use the translator's trans() method to translate the key // Parameters: key, parameters, domain ('messages'), locale // Hint: Return the result }
/** * 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 requested locale is available // Hint: Check if $locale exists in $this->availableLocales array // If not available, throw an InvalidArgumentException with a descriptive message
// TODO: Update the currentLocale property with the new locale
// TODO: Also update the translator's locale // Hint: The translator has its own setLocale() method }
/** * Get the current locale * * @return string Current locale code */ public function getLocale(): string { // TODO: Return the current locale property }
/** * Get the default locale * * @return string Default locale code */ public function getDefaultLocale(): string { // TODO: Return the default locale property }
/** * Get all available locales * * @return array List of available locale codes */ public function getAvailableLocales(): array { // TODO: Return the available locales array property }
/** * Check if a locale is available * * @param string $locale Locale code to check * @return bool True if locale is available */ public function isLocaleAvailable(string $locale): bool { // TODO: Check if the given locale exists in the availableLocales array // Hint: Use in_array() function }}What This Class Does:
- Constructor: Initializes Symfony Translator, sets up fallback locale, loads all translation files
- loadTranslations(): Reads JSON files from
lang/{locale}/messages.jsonand registers them - trans(): Translates a key (e.g., “home.welcome”) to the current language
- setLocale(): Changes the active language (validates it’s available first)
- Getter methods: Provide access to current locale, available locales, etc.
Step 5: Create LocaleMiddleware with Session Persistence
Section titled “Step 5: Create LocaleMiddleware with Session Persistence”Objective: Build middleware to automatically detect and set the user’s preferred language with session persistence.
LocaleMiddleware runs on every request and determines which language to use based on:
- URL Parameter (
?lang=fr) - Explicit user choice (highest priority) - Session Value - Previously saved preference
- Default Locale - English fallback (lowest priority)
Why Session Persistence?
Without session persistence, users would need to add ?lang=fr to every URL. With session persistence:
- Language preference is remembered across page navigation
- Users only need to select their language once
- Better user experience
Prerequisites:
- SessionManager must exist from the Session Middleware lab
- SessionMiddleware must be registered (starts the session)
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;
/** * Locale Middleware * * Detects and sets the application locale based on: * 1. Query parameter (?lang=fr) - Highest priority * 2. Session value - Previously saved preference * 3. Default locale - Fallback */class LocaleMiddleware implements MiddlewareInterface{ private TranslationHelper $translator;
/** * Initialize the Locale Middleware * * @param TranslationHelper $translator Translation helper service */ public function __construct(TranslationHelper $translator) { // TODO: Store the translator parameter in the class property }
/** * Process an incoming server request. * * Detects the user's preferred locale from query parameters or session, * and sets it in the translation helper. */ public function process(Request $request, RequestHandler $handler): ResponseInterface { // TODO: Get query parameters from the request // Hint: Use $request->getQueryParams()
// TODO: Extract the 'lang' parameter from query params (if provided) // Hint: Use null coalescing operator (??) to default to null
// TODO: Determine which locale to use based on priority: // Priority 1: URL parameter (?lang=fr) // Priority 2: Session value (retrieve 'locale' key from session using SessionManager) // Priority 3: Default locale // Hint: Use the null coalescing operator (??) to chain these three sources
// TODO: If a NEW locale was explicitly requested via URL parameter AND it's valid: // 1. Set it in the translator // 2. Save it to the session for future requests // Hint: Check both that $locale is not null AND that it's available // Use SessionManager to store the key 'locale' with the locale value in the session
// TODO: If no URL parameter was provided, but session has a saved locale AND it's valid: // Set the session locale in the translator // Hint: Use elseif to handle this case // Use SessionManager to retrieve the 'locale' value from the session
// TODO: Store the current locale in the request as an attribute named 'locale' // Hint: Use $request->withAttribute() and reassign the result
// TODO: Pass the request to the next middleware/handler and return the response }
}Implementation Hints:
The method should follow this logic:
- Get the
langparameter from URL query string - If URL parameter provided and valid:
- Set it in translator
- Store it in session with key
'locale'
- Else if session has a saved locale (key
'locale') and it’s valid:- Set it in translator
- Store current locale in request attribute
- Continue to next middleware
Important Notes:
- SessionManager uses static methods (recall what you learned in the Session lab)
- You do NOT need to inject SessionManager into the constructor
- SessionMiddleware (from the Session lab) starts the session automatically
- Use the null coalescing operator (
??) to chain fallbacks for priority detection
Step 6: Update Configuration Files
Section titled “Step 6: Update Configuration Files”Objective: Register new components in the application’s dependency injection container and middleware stack.
6.1: Add Language Path Constant
Section titled “6.1: Add Language Path Constant”Objective: Define a constant for the language files directory path.
Open config/constants.php and add a new constant:
Your Task:
- Location: After the
APP_VIEWS_PATHconstant - Constant name:
APP_LANG_PATH - Value: Combine
APP_BASE_DIR_PATHwith'/lang' - Add a comment explaining the constant’s purpose
Why? Centralizes the path to translation files so you can change it in one place.
6.2: Register Services in DI Container
Section titled “6.2: Register Services in DI Container”Objective: Register TranslationHelper and LocaleMiddleware in the dependency injection container.
Open config/container.php.
Step 1: Add Use Statements
At the top of the file with other use statements, add:
use App\Helpers\TranslationHelper;use App\Middleware\LocaleMiddleware;
Step 2: Register TranslationHelper
In the $definitions array, add a definition for TranslationHelper::class:
TranslationHelper::class => function (ContainerInterface $container): TranslationHelper { return new TranslationHelper( APP_LANG_PATH, // Path to language files 'en', // Default locale (fallback language) ['en', 'fr'] // Available locales (languages your app supports) );},Step 3: Register LocaleMiddleware
In the $definitions array, add a definition for LocaleMiddleware::class:
LocaleMiddleware::class => function (ContainerInterface $container): LocaleMiddleware { return new LocaleMiddleware( $container->get(TranslationHelper::class) // Inject TranslationHelper dependency );},What This Does:
- Tells the container how to create TranslationHelper with the correct constructor parameters
- Tells the container how to create LocaleMiddleware and inject TranslationHelper as a dependency
- Enables automatic dependency injection throughout your application
6.3: Register Middleware
Section titled “6.3: Register Middleware”Objective: Add LocaleMiddleware to the middleware stack in the correct order.
Open config/middleware.php.
Your Task:
-
Add the use statement at the top with other middleware imports:
use App\Middleware\LocaleMiddleware;
-
Register the middleware by adding this line:
// Detect and set the application locale (with session persistence)$app->add(LocaleMiddleware::class);
Important: Middleware Order
Middleware must be registered in the correct order. SessionMiddleware MUST be added BEFORE LocaleMiddleware:
// Correct order:$app->add(SessionMiddleware::class); // This must come FIRST$app->add(LocaleMiddleware::class); // This comes AFTERWhy is order critical?
Middleware in Slim 4 works like an onion - the last one added runs first:
- SessionMiddleware must run first to start the session
- LocaleMiddleware runs second and can then use SessionManager to work with locale data
What happens if the order is wrong?
- If LocaleMiddleware runs before SessionMiddleware, the session won’t be started yet
- Attempting to retrieve or store locale in session would fail or return incorrect values
- Locale won’t persist across requests
6.4: Create Global Translation Function
Section titled “6.4: Create Global Translation Function”Objective: Create a global helper function trans() for easy translation in views.
Open config/functions.php and add the following function at the end of the file:
if (!function_exists('trans')) { /** * Translate a message using the application's translation helper. * * This function provides a convenient way to translate strings in views and controllers. * It uses the global TranslationHelper instance to perform translations with support * for nested keys (dot notation) and parameter substitution. * * @param string $key Translation key (supports dot notation: 'home.welcome') * @param array $parameters Parameters to replace in translation (e.g., ['name' => 'John']) * @param string|null $locale Override current locale for this translation * @return string Translated message or the key itself if translation not found * * @example * echo trans('home.welcome'); * // Outputs: "Welcome to our e-commerce store" (in current locale) * * @example * echo trans('common.hello', ['name' => 'Alice']); * // Outputs: "Hello, Alice" (with parameter substitution) * * @example * echo trans('nav.home', [], 'fr'); * // Outputs: "Accueil" (forced French translation) */ function trans(string $key, array $parameters = [], ?string $locale = null): string { global $translator;
if (!isset($translator)) { // Fallback: return the key if translator is not initialized return $key; }
return $translator->trans($key, $parameters, $locale); }}Why a global function? Makes views cleaner - you can write trans('home.welcome') instead of $translator->trans('home.welcome').
6.5: Initialize Global Translator
Section titled “6.5: Initialize Global Translator”Objective: Make the TranslationHelper instance available globally for the trans() helper function.
Open config/bootstrap.php.
Your Task:
Add the following code AFTER the container is built (after the line $container = $containerBuilder->build();):
// Set up global translator for use in trans() helper functionglobal $translator;$translator = $container->get(\App\Helpers\TranslationHelper::class);What This Does:
- Declares a global variable
$translator - Retrieves the TranslationHelper instance from the container
- Makes it available to the
trans()helper function throughout your application
Step 7: Update Views to Use Translations
Section titled “Step 7: Update Views to Use Translations”Objective: Replace hardcoded text in views with translation keys.
Example 1: Update Home Page
Section titled “Example 1: Update Home Page”Open app/Views/homeView.php (or equivalent).
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>IMPORTANT: Always use hs() to escape output and prevent XSS attacks!
Example 2: Update Navigation
Section titled “Example 2: Update 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>Example 3: Update 404 Error Page
Section titled “Example 3: Update 404 Error Page”Open app/Views/errors/404.php.
Before:
<?php$page_title = '404 - Page Not Found';?>
<h1>Page Not Found</h1><p>Sorry, the page you are looking for does not exist.</p><a href="/">Go Back Home</a>After:
<?php$page_title = trans('errors.404.title');?>
<h1><?= hs(trans('errors.404.title')) ?></h1><p><?= hs(trans('errors.404.message')) ?></p><a href="/"><?= hs(trans('errors.404.back_home')) ?></a>Example 4: Buttons and Form Labels
Section titled “Example 4: Buttons and Form Labels”Before:
<button type="submit">Save</button><button type="button">Cancel</button>After:
<button type="submit"><?= hs(trans('common.save')) ?></button><button type="button"><?= hs(trans('common.cancel')) ?></button>Optional Enhancement: Language Switcher
Section titled “Optional Enhancement: Language Switcher”Objective: Create a UI component that allows users to easily switch languages.
You can add this to your header (or admin_header.php) or navigation file:
<div class="language-switcher"> <?php // Get current locale from global translator global $translator; $currentLocale = $translator->getLocale(); $availableLocales = $translator->getAvailableLocales(); ?>
<?php foreach ($availableLocales as $locale): ?> <?php if ($locale !== $currentLocale): ?> <a href="?lang=<?= hs($locale) ?>" class="lang-link"> <?= $locale === 'en' ? 'English' : 'Français' ?> </a> <?php endif; ?> <?php endforeach; ?>
<span class="current-lang"> <?= $currentLocale === 'en' ? '🇬🇧 English' : '🇫🇷 Français' ?> </span></div>Styling Example:
.language-switcher { display: flex; gap: 10px; align-items: center;}
.lang-link { padding: 5px 10px; text-decoration: none; border: 1px solid #ccc; border-radius: 4px;}
.current-lang { font-weight: bold;}Testing Your Implementation
Section titled “Testing Your Implementation”Test Case 1: Default Language (English)
Section titled “Test Case 1: Default Language (English)”- Visit
http://localhost/[your-app]/ - Expected: Page displays in English
- Check home page title shows “Welcome to our e-commerce store”
Test Case 2: Switch to French via URL
Section titled “Test Case 2: Switch to French via URL”- Visit
http://localhost/[your-app]/?lang=fr - Expected: Page displays in French
- Check home page title shows “Bienvenue dans notre boutique en ligne”
Test Case 3: Session Persistence - Locale Persists Across Pages
Section titled “Test Case 3: Session Persistence - Locale Persists Across Pages”- Visit
http://localhost/[your-app]/?lang=fr - Expected: Page displays in French
- Click any navigation link (without
?lang=fr) - Expected: New page still displays in French ✓
- Why? Locale was saved to session and persists across navigation
Test Case 4: URL Parameter Overrides Session
Section titled “Test Case 4: URL Parameter Overrides Session”- Have French saved in session (from previous test)
- Visit
http://localhost/[your-app]/?lang=en - Expected: Page displays in English
- Navigate without query parameter
- Expected: Still shows English (session was updated)
Test Case 5: Session Survives Browser Refresh
Section titled “Test Case 5: Session Survives Browser Refresh”- Select French via
?lang=fr - Refresh the page (without
?lang=frin URL) - Expected: Page still displays in French
Test Case 6: Default for New Users
Section titled “Test Case 6: Default for New Users”- Clear browser cookies/session
- Visit
http://localhost/[your-app]/(no query parameter) - Expected: Page displays in English (default locale)
Test Case 7: Invalid Language Code
Section titled “Test Case 7: Invalid Language Code”- Visit
http://localhost/[your-app]/?lang=es(Spanish not supported) - Expected: Falls back to English (default)
Test Case 8: Translation Key Consistency
Section titled “Test Case 8: Translation Key Consistency”- Visit multiple pages in French mode
- Expected: All pages show French consistently
- Navigation, buttons, errors should all be translated
Test Case 9: Missing Translation Key
Section titled “Test Case 9: Missing Translation Key”- Temporarily use a non-existent key:
trans('fake.key') - Expected: Displays “fake.key” (fallback behavior)
Test Case 10: Special Characters
Section titled “Test Case 10: Special Characters”- Switch to French
- Check that special characters display correctly: é, à, è, ç
- Expected: No garbled characters or encoding issues
Test Case 11: XSS Protection
Section titled “Test Case 11: XSS Protection”- Test with translation values
- Expected: Content is HTML-escaped (no script execution)
Common Issues and Solutions
Section titled “Common Issues and Solutions”Issue 1: Translations Not Working
Section titled “Issue 1: Translations Not Working”Symptom: Translation keys are displayed instead of actual text (e.g., “home.welcome” instead of “Welcome…”)
Possible Causes:
- JSON file has invalid syntax
- Translation key doesn’t exist in JSON file
- File not loaded by TranslationHelper
Solution:
- Validate JSON using JSONLint ↗
- Check that key exists in both
lang/en/messages.jsonandlang/fr/messages.json - Verify
loadTranslations()method is called in constructor - Check file permissions (must be readable)
Issue 2: Always Shows Default Language
Section titled “Issue 2: Always Shows Default Language”Symptom: Application always displays English even with ?lang=fr
Possible Causes:
- LocaleMiddleware not registered
- Middleware registered in wrong order
- Query parameter not being checked correctly
Solution:
- Check
config/middleware.phphas$app->add(LocaleMiddleware::class); - Ensure LocaleMiddleware runs BEFORE route handlers
- Verify
process()method checks query parameters correctly
Issue 3: Special Characters Display Incorrectly
Section titled “Issue 3: Special Characters Display Incorrectly”Symptom: French characters show as � or weird symbols
Possible Causes:
- JSON files not saved with UTF-8 encoding
- Missing charset in HTML
Solution:
- Save all JSON files with UTF-8 encoding (no BOM)
- Ensure views have
<meta charset="UTF-8">in header - Check database charset is utf8mb4 (if storing translations in DB)
Issue 4: Translations Work in Some Views But Not Others
Section titled “Issue 4: Translations Work in Some Views But Not Others”Symptom: Some pages translated, others show hardcoded text
Possible Cause:
- Forgot to update those views to use
trans()
Solution:
- Search codebase for hardcoded text strings
- Replace with appropriate translation keys
- Add missing keys to JSON files if needed
Issue 5: Locale Not Persisting in Session
Section titled “Issue 5: Locale Not Persisting in Session”Symptom: Language changes with ?lang=fr but reverts to default when navigating to other pages
Possible Causes:
- Forgot to save locale to session in LocaleMiddleware
- SessionMiddleware not registered or running after LocaleMiddleware
- Session not started
- Incorrect middleware order
Solution:
- Check that
process()method in LocaleMiddleware saves locale to session when URL parameter is provided - Ensure SessionMiddleware is registered BEFORE LocaleMiddleware in
config/middleware.php:$app->add(SessionMiddleware::class); // First$app->add(LocaleMiddleware::class); // Second - Verify SessionMiddleware is calling the start method from SessionManager
- Check browser cookies are enabled (use browser dev tools → Application → Cookies)
- Test by checking session directly: Add a debug statement in your view to display the locale from session
Security Best Practices
Section titled “Security Best Practices”1. Always Escape Output
Section titled “1. Always Escape Output”Vulnerable Code:
<h1><?= trans('home.title') ?></h1> <!-- XSS RISK -->Secure Code:
<h1><?= hs(trans('home.title')) ?></h1> <!-- SAFE -->Why? Even though you control translation files, always escape output to prevent injection attacks if translations are ever stored in a database or come from user input.
2. Validate Locale Input
Section titled “2. Validate Locale Input”The setLocale() method already validates that requested locales are in the allowed list:
if (!in_array($locale, $this->availableLocales)) { throw new \InvalidArgumentException("Locale '{$locale}' is not available.");}Why? Prevents attackers from using locale codes to traverse directories or inject malicious values.
3. Use UTF-8 Encoding
Section titled “3. Use UTF-8 Encoding”Always save translation files and serve pages with UTF-8:
<meta charset="UTF-8">Why? Prevents encoding issues that could lead to XSS or display problems.
4. Don’t Trust Client-Provided Locales Blindly
Section titled “4. Don’t Trust Client-Provided Locales Blindly”LocaleMiddleware validates requested locales against the whitelist:
if ($locale && $this->translator->isLocaleAvailable($locale)) { $this->translator->setLocale($locale);}Why? Ensures users can’t request arbitrary locale codes.
Best Practices
Section titled “Best Practices”1. Keep Translation Files Synchronized
Section titled “1. Keep Translation Files Synchronized”All language files should have the same structure:
Good:
// en/messages.json{ "home": { "title": "Home", "welcome": "Welcome" } }
// fr/messages.json{ "home": { "title": "Accueil", "welcome": "Bienvenue" } }Bad:
// en/messages.json{ "home": { "title": "Home", "welcome": "Welcome" } }
// fr/messages.json{ "home": { "title": "Accueil" } } // Missing 'welcome' key!2. Use Consistent Key Naming
Section titled “2. Use Consistent Key Naming”Follow dot notation pattern:
section.subsection.key
Examples:- home.title- nav.products- errors.404.message- common.save3. Group Related Translations
Section titled “3. Group Related Translations”{ "products": { "title": "Products", "add_to_cart": "Add to Cart", "price": "Price" }, "cart": { "title": "Shopping Cart", "empty": "Your cart is empty" }}4. Don’t Hardcode Text Anywhere
Section titled “4. Don’t Hardcode Text Anywhere”Bad:
<button>Save</button>Good:
<button><?= hs(trans('common.save')) ?></button>5. Use Placeholders for Dynamic Content
Section titled “5. Use Placeholders for Dynamic Content”You can use placeholders in translations to insert dynamic content.
In the following example, the placeholders %name% and %count% in the translation will be replaced with the actual values passed in the parameters array.
If you need to include placeholders in translations:
Translation file:
{ "user": { "greeting": "Hello, %name%! You have %count% messages." }}Usage:
<?= hs(trans('user.greeting', [ '%name%' => $username, '%count%' => $messageCount])) ?>