alex-no / language-detector
Framework-agnostic language detector with Yii2 and Laravel adapters
Installs: 22
Dependents: 0
Suggesters: 1
Security: 0
Stars: 1
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/alex-no/language-detector
Requires
- php: >=8.0
- psr/simple-cache: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^9.5 || ^10.0
- squizlabs/php_codesniffer: ^3.0
This package is auto-updated.
Last update: 2025-11-28 23:01:38 UTC
README
A framework-agnostic language detection library for PHP 8.0+ with adapters for Yii 2, Yii 3, Laravel, and Symfony.
✨ Features
- Multi-source language detection with customizable priority (default order):
- POST parameter
- GET parameter
- URL Path segment
- Authenticated User Profile
- Session
- Cookie
- Accept-Language header
- Default language fallback
- Customizable source order — you can define which sources to use and in what order via
sourceKeysconfiguration - Database-backed language list — caches allowed languages from database with configurable TTL
- Language persistence — automatically saves detected language to session, cookie, and user profile (DB)
- Separate configuration — independent
paramName(for GET/POST/Cookie/Session) anduserAttribute(for DB field name) - API mode support — works in both web and API contexts (API mode skips session/cookie)
- Framework-agnostic — clean DDD architecture with adapters for Yii 2, Yii 3, Laravel, and Symfony
- Event system — dispatches
LanguageChangedEventwhen user's language changes - Type-safe — full PHP 8.0+ strict typing throughout
Starting from version 1.1.3, the package follows a clean DDD-inspired structure:
- Domain — interfaces (contracts), events, and pure business logic (Sources).
- Application — orchestrates domain services (e.g., LanguageDetector, SourceFactory).
- Infrastructure — framework adapters, repositories, cache, request/response bridges.
Each framework adapter implements FrameworkContextInterface which provides access to all framework-specific services (request, response, user, cache, repository, event dispatcher). This makes the library framework-agnostic and easy to extend.
⚙️ Installation
Install via Composer:
composer require alex-no/language-detector
🔔 Language change event
When the detector changes the stored language for a user (for example when a new lang parameter is provided or a higher-priority source selects another language), LanguageDetector will update the user's profile attribute and — if an event dispatcher is provided — dispatch a LanguageDetector\Domain\Events\LanguageChangedEvent.
The event object exposes three public properties:
oldLanguage(string) — previous language codenewLanguage(string) — new language codeuser(UserInterface|null) — the user instance (if available)
🚀 Usage in Yii 2
Register the bootstrap component in config/web.php:
'bootstrap' => [ 'languageBootstrap', ], 'components' => [ 'languageBootstrap' => [ 'class' => \LanguageDetector\Infrastructure\Adapters\Yii2\Bootstrap::class, 'paramName' => 'lang', // GET/POST/Cookie/Session parameter name 'userAttribute' => 'language_code', // User DB field name for storing language 'default' => 'en', // Default language code 'pathSegmentIndex' => 0, // URL path segment index (0 = first segment) ], ],
The bootstrap component will:
- Automatically detect language on each request
- Check sources in priority order: POST → GET → Path → User → Session → Cookie → Header → Default
- Update
Yii::$app->languageaccordingly - Persist language to session, cookie, and user profile
Manual usage:
// Access detector manually $detector = Yii::$app->languageDetector; $lang = $detector->detect();
Custom source order:
You can customize the detection order by passing sourceKeys in the configuration:
'languageBootstrap' => [ 'class' => \LanguageDetector\Infrastructure\Adapters\Yii2\Bootstrap::class, 'paramName' => 'lang', 'userAttribute' => 'language_code', 'default' => 'en', 'pathSegmentIndex' => 0, // Custom order: only check GET parameter and Accept-Language header 'sourceKeys' => ['get', 'header', 'default'], ],
Event handling:
Listen to language change events using Yii's event system:
Yii::$app->on('language.changed', function($event) { // $event is yii\base\Event // Access the LanguageChangedEvent object via $event->data $languageEvent = $event->data; echo "Language changed from {$languageEvent->oldLanguage} to {$languageEvent->newLanguage}"; // Access user if available if ($languageEvent->user) { echo "User ID: " . $languageEvent->user->getId(); } });
Note: The language change event is currently dispatched only for authenticated users.
🚀 Usage in Yii 3
Yii3 adapter supports two usage approaches:
Approach 1: Middleware (Recommended)
1. Register the Middleware in DI
Add to your DI configuration (typically in config/web/di.php):
use LanguageDetector\Infrastructure\Adapters\Yii3\LanguageMiddleware; use Yiisoft\Cache\CacheInterface; return [ LanguageMiddleware::class => static function (\PDO $pdo, CacheInterface $cache) { return new LanguageMiddleware( $pdo, $cache, [ 'paramName' => 'lang', // GET/POST/Cookie/Session parameter name 'userAttribute' => 'language_code', // User DB field name for storing language 'default' => 'en', // Default language code 'pathSegmentIndex' => 0, // URL path segment index (0 = first segment) 'table' => 'language', // Database table name 'codeField' => 'code', // Language code field name 'enabledField' => 'is_enabled', // Enabled status field name 'orderField' => 'order', // Sort order field name ] ); }, ];
2. Register Middleware in application stack
Add to config/web/application.php (IMPORTANT: place AFTER authentication middleware):
return [ 'middlewares' => [ // ... other middlewares \Yiisoft\Auth\Middleware\Authentication::class, // Authentication MUST run first \LanguageDetector\Infrastructure\Adapters\Yii3\LanguageMiddleware::class, // ... other middlewares ], ];
How it works:
- Automatically detects language on each request
- Checks sources in priority order: POST → GET → Path → User → Session → Cookie → Header → Default
- Stores detected language as request attribute
language - Persists language to session, cookie, and authenticated user profile
- Requires authentication middleware to run BEFORE to enable user language persistence
- Identity must be stored in request attributes as
'identity'or'user'
Usage in controllers:
use Psr\Http\Message\ServerRequestInterface; class HomeController { public function index(ServerRequestInterface $request): ResponseInterface { // Get detected language from request attribute $lang = $request->getAttribute('language', 'en'); // Use the detected language // ... return $this->render('home/index', ['lang' => $lang]); } }
Approach 2: Manual Usage with Full Context
For advanced scenarios where you need full control over all components:
1. Register services in DI
use LanguageDetector\Infrastructure\Adapters\Yii3\Yii3Context; use LanguageDetector\Application\LanguageDetector; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Yiisoft\Auth\IdentityInterface; use Yiisoft\Cache\CacheInterface; use Yiisoft\Db\Connection\ConnectionInterface; return [ Yii3Context::class => static function ( ServerRequestInterface $request, ResponseInterface $response, ?IdentityInterface $identity, CacheInterface $cache, EventDispatcherInterface $eventDispatcher, ConnectionInterface $db ) { return new Yii3Context( [ 'paramName' => 'lang', 'userAttribute' => 'language_code', 'default' => 'en', 'pathSegmentIndex' => 0, ], $request, $response, $identity, $cache, $eventDispatcher, $db ); }, LanguageDetector::class => static function (Yii3Context $context) { return new LanguageDetector($context, null, [ 'paramName' => 'lang', 'userAttribute' => 'language_code', 'default' => 'en', 'pathSegmentIndex' => 0, ]); }, ];
2. Use in controllers:
use LanguageDetector\Application\LanguageDetector; class HomeController { public function index(LanguageDetector $detector): ResponseInterface { $lang = $detector->detect(); return $this->render('home/index', ['lang' => $lang]); } }
Configuration Parameters
All configuration parameters for Yii3 adapter:
| Parameter | Type | Default | Description |
|---|---|---|---|
paramName |
string | 'lang' |
Parameter name for GET/POST/Cookie/Session |
userAttribute |
string | 'language_code' |
User database field name for storing language |
default |
string | 'en' |
Default language code |
pathSegmentIndex |
int | 0 |
URL path segment index for language detection (0 = first segment) |
table |
string | 'language' |
Database table name for enabled languages |
codeField |
string | 'code' |
Language code field name in database |
enabledField |
string | 'is_enabled' |
Enabled status field name (should contain 1/0) |
orderField |
string | 'order' |
Sort order field name |
cacheKey |
string | 'allowed_languages' |
Cache key for storing enabled languages |
cacheTtl |
int | 3600 |
Cache TTL in seconds |
Event Handling
Listen to LanguageChangedEvent using PSR-14 event listeners:
use LanguageDetector\Domain\Events\LanguageChangedEvent; use Psr\EventDispatcher\ListenerProviderInterface; return [ ListenerProviderInterface::class => static function () { $provider = new SimpleEventDispatcher(); $provider->listen(LanguageChangedEvent::class, function (LanguageChangedEvent $event) { // Log or handle language change // Available properties: $event->oldLanguage, $event->newLanguage, $event->user }); return $provider; }, ];
Note: The language change event is dispatched only for authenticated users.
🚀 Usage in Laravel
1. Register the Service Provider
Add to the providers array in config/app.php (if not auto-discovered):
'providers' => [ LanguageDetector\Infrastructure\Adapters\Laravel\LanguageDetectorServiceProvider::class, ],
2. Configure the Service Provider
You can customize parameters directly in the service provider or extend it:
// In config/app.php or create a custom service provider 'providers' => [ \App\Providers\CustomLanguageServiceProvider::class, ], // app/Providers/CustomLanguageServiceProvider.php namespace App\Providers; use LanguageDetector\Infrastructure\Adapters\Laravel\LanguageDetectorServiceProvider; class CustomLanguageServiceProvider extends LanguageDetectorServiceProvider { public string $paramName = 'lang'; public string $userAttribute = 'language_code'; // User DB field name public string $default = 'en'; public int $pathSegmentIndex = 0; // URL path segment index }
3. Register the Middleware
Add to app/Http/Kernel.php:
protected $middlewareGroups = [ 'web' => [ // ... \LanguageDetector\Infrastructure\Adapters\Laravel\LaravelMiddleware::class, ], ];
The middleware will:
- Automatically detect language on each request
- Apply it globally using
App::setLocale() - Check sources in priority order: POST → GET → Path → User → Session → Cookie → Header → Default
Manual usage in controller:
use LanguageDetector\Application\LanguageDetector; use Illuminate\Support\Facades\App; public function index(LanguageDetector $detector) { $lang = $detector->detect(); App::setLocale($lang); return view('welcome', ['lang' => $lang]); }
Event handling:
Listen to LanguageChangedEvent using Laravel event listeners:
// In EventServiceProvider use LanguageDetector\Domain\Events\LanguageChangedEvent; protected $listen = [ LanguageChangedEvent::class => [ \App\Listeners\LogLanguageChange::class, ], ];
Note: The language change event is currently dispatched only for authenticated users.
🚀 Usage in Symfony
1. Register the services
Create or update config/services.yaml:
services: # Register SymfonyContext LanguageDetector\Infrastructure\Adapters\Symfony\SymfonyContext: arguments: $requestStack: '@request_stack' $cache: '@cache.app' $dispatcher: '@event_dispatcher' $connection: '@doctrine.dbal.default_connection' $config: paramName: 'lang' userAttribute: 'language_code' default: 'en' pathSegmentIndex: 0 # Register LanguageDetector LanguageDetector\Application\LanguageDetector: arguments: $context: '@LanguageDetector\Infrastructure\Adapters\Symfony\SymfonyContext' $sourceKeys: null # Use default order, or customize: ['get', 'header', 'default'] $config: paramName: 'lang' userAttribute: 'language_code' default: 'en' pathSegmentIndex: 0 # Register RequestListener LanguageDetector\Infrastructure\Adapters\Symfony\RequestListener: arguments: $detector: '@LanguageDetector\Application\LanguageDetector' tags: - { name: kernel.event_listener, event: kernel.request, priority: 10 }
2. How it works
The RequestListener will:
- Listen to
kernel.requestevent - Automatically detect language on each request
- Set the locale on the request and session
- Update
$request->setLocale($lang)
Manual usage in controller:
use LanguageDetector\Application\LanguageDetector; class HomeController extends AbstractController { public function index(LanguageDetector $detector): Response { $lang = $detector->detect(); $this->get('request_stack')->getCurrentRequest()->setLocale($lang); return $this->render('home/index.html.twig', [ 'language' => $lang, ]); } }
Event handling:
Listen to LanguageChangedEvent:
use LanguageDetector\Domain\Events\LanguageChangedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class LanguageChangeSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ LanguageChangedEvent::class => 'onLanguageChanged', ]; } public function onLanguageChanged(LanguageChangedEvent $event): void { // Log or handle language change // $event->oldLanguage, $event->newLanguage, $event->user } }
Note: The language change event is currently dispatched only for authenticated users.
⚙️ Configuration Options
| Option | Description | Default | Used in |
|---|---|---|---|
paramName |
Request parameter name for language (GET/POST/etc) | lang |
All adapters |
default |
Fallback language code | en |
All adapters |
pathSegmentIndex |
URL path segment index for language detection | 0 |
All adapters |
sourceKeys |
Array defining custom source detection order | null |
All adapters |
cacheKey |
Cache key for storing allowed languages | allowed_languages |
LanguageDetector |
cacheTtl |
Cache TTL in seconds | 3600 |
LanguageDetector |
Note: Repository-related options (tableName, codeField, enabledField, orderField) are configured within each framework's repository implementation, not in the main configuration.
🔍 Available Language Sources
You can customize which sources to use and their priority order via the sourceKeys configuration parameter. Available sources:
| Source Key | Description | Class |
|---|---|---|
post |
Reads language from POST parameter (e.g., $_POST['lang']) |
PostSource |
get |
Reads language from GET parameter (e.g., $_GET['lang']) |
GetSource |
path |
Extracts language from URL path segment (e.g., /en/home) |
PathSource |
user |
Reads from authenticated user's profile attribute | UserProfileSource |
session |
Reads from session storage | SessionSource |
cookie |
Reads from cookie | CookieSource |
header |
Parses Accept-Language HTTP header | HeaderSource |
default |
Returns the configured default language | DefaultSource |
Default order: ['post', 'get', 'path', 'user', 'session', 'cookie', 'header', 'default']
Example custom order:
// Only use GET parameter and Accept-Language header $sourceKeys = ['get', 'header', 'default']; // Yii 2 $context = new Yii2Context($config); $detector = new LanguageDetector($context, $sourceKeys, $config); // Laravel - extend ServiceProvider and pass to constructor // Symfony - configure in services.yaml
🗃️ Example Language Table
CREATE TABLE `language` ( `code` VARCHAR(5) NOT NULL, `short_name` VARCHAR(3) NOT NULL, `full_name` VARCHAR(32) NOT NULL, `is_enabled` TINYINT(1) NOT NULL DEFAULT '1', `order` TINYINT NOT NULL, PRIMARY KEY (`code`)) ENGINE = InnoDB
Sample data:
INSERT INTO language (code, is_enabled, `order`) VALUES ('en', 1, 1), ('uk', 1, 2), ('ru', 0, 3);
🧪 Running Tests
Install PHPUnit as a dev dependency:
composer require --dev phpunit/phpunit
Run the test suite:
./vendor/bin/phpunit -c phpunit.xml.dist
Or define a shortcut in composer.json:
"scripts": { "test": "phpunit -c phpunit.xml.dist" }
Then simply run:
composer test
📁 Directory Structure
language-detector/ │ src/ │ ├── Application/ │ │ ├── LanguageDetector.php │ │ └── SourceFactory.php │ ├── Domain/ │ │ ├── Contracts/ │ │ │ ├── FrameworkContextInterface.php // namespace LanguageDetector\Domain\Contracts │ │ │ ├── RequestInterface.php │ │ │ ├── ResponseInterface.php │ │ │ ├── UserInterface.php │ │ │ ├── SourceInterface.php │ │ │ ├── LanguageRepositoryInterface.php │ │ │ └── EventDispatcherInterface.php │ │ ├── Events/ │ │ │ └── LanguageChangedEvent.php // namespace LanguageDetector\Domain\Events │ │ └── Sources/ │ │ ├── PathSource.php // namespace LanguageDetector\Domain\Sources │ │ ├── PostSource.php │ │ ├── GetSource.php │ │ ├── UserProfileSource.php │ │ ├── SessionSource.php │ │ ├── CookieSource.php │ │ ├── HeaderSource.php │ │ └── DefaultSource.php │ └── Infrastructure/ │ └── Adapters/ │ ├── Yii2/ │ │ ├── Bootstrap.php │ │ ├── Yii2Context.php │ │ ├── YiiRequestAdapter.php // implements RequestInterface │ │ ├── YiiResponseAdapter.php // implements ResponseInterface │ │ ├── YiiUserAdapter.php // implements UserInterface │ │ ├── YiiCacheAdapter.php // implements CacheInterface │ │ ├── YiiLanguageRepository.php // implements LanguageRepositoryInterface │ │ └── YiiEventDispatcher.php // implements EventDispatcherInterface │ ├── Yii3/ │ │ ├── LanguageMiddleware.php │ │ ├── Yii3Context.php │ │ ├── Yii3RequestAdapter.php // implements RequestInterface │ │ ├── Yii3ResponseAdapter.php // implements ResponseInterface │ │ ├── Yii3UserAdapter.php // implements UserInterface │ │ ├── Yii3CacheAdapter.php // implements CacheInterface │ │ ├── Yii3LanguageRepository.php // implements LanguageRepositoryInterface │ │ └── Yii3EventDispatcher.php // implements EventDispatcherInterface │ ├── Symfony/ │ │ ├── RequestListener.php │ │ ├── SymfonyContext.php │ │ ├── SymfonyRequestAdapter.php // implements RequestInterface │ │ ├── SymfonyResponseAdapter.php // implements ResponseInterface │ │ ├── SymfonyUserAdapter.php // implements UserInterface │ │ ├── SymfonyCacheAdapter.php // implements CacheInterface │ │ ├── SymfonyLanguageRepository.php // implements LanguageRepositoryInterface │ │ └── SymfonyEventDispatcher.php // implements EventDispatcherInterface │ └── Laravel/ │ ├── LanguageDetectorServiceProvider.php │ ├── LaravelMiddleware.php │ ├── LaravelContext.php │ ├── LaravelRequestAdapter.php // implements RequestInterface │ ├── LaravelResponseAdapter.php // implements ResponseInterface │ ├── LaravelUserAdapter.php // implements UserInterface │ ├── LaravelCacheAdapter.php // implements CacheInterface │ ├── LaravelLanguageRepository.php // implements LanguageRepositoryInterface │ └── LaravelEventDispatcher.php // implements EventDispatcherInterface ├── tests │ └── TestLanguageDetector.php composer.json phpunit.xml.dist LICENSE
🧩 DDD Architecture Layers
Domain Layer (src/Domain/):
- Contracts — interfaces defining core abstractions (RequestInterface, UserInterface, FrameworkContextInterface, etc.)
- Events — domain events (LanguageChangedEvent)
- Sources — language detection sources (PostSource, GetSource, PathSource, UserProfileSource, etc.)
Application Layer (src/Application/):
- LanguageDetector — main service orchestrating language detection
- SourceFactory — factory for creating source instances
Infrastructure Layer (src/Infrastructure/Adapters/):
- Framework-specific implementations (Yii2, Laravel, Symfony)
- Each adapter implements
FrameworkContextInterfaceproviding access to framework services - Adapters are isolated from business logic and can be easily swapped
🧰 Example Test
Running the included test file:
php tests/TestLanguageDetector.php
Sample output:
=== Language Detector Tests ===
Test 1 - Path (/en/test): ✓ PASS
Test 2 - GET parameter (lang=uk): ✓ PASS
Test 3 - POST parameter (lang=fr): ✓ PASS
Test 4 - Cookie (lang=de): ✓ PASS
Test 5 - Session (lang=uk): ✓ PASS
Test 6 - User profile (language_code=fr): ✓ PASS
Test 7 - Accept-Language header (de-DE,de;q=0.9,en;q=0.8): ✓ PASS
Test 8 - Default fallback: ✓ PASS
Test 9 - Invalid language (lang=invalid): ✓ PASS
Test 10 - Cache stores enabled languages: ✓ PASS
=== Tests Complete ===
The test file demonstrates how to create mock implementations of all required interfaces and test the detector in isolation.
📄 License
Released under the MIT License © 2025 Oleksandr Nosov