alex-no/language-detector

Framework-agnostic language detector with Yii2 and Laravel adapters

Maintainers

๐Ÿ‘ alex-no

Package info

github.com/alex-no/language-detector

pkg:composer/alex-no/language-detector

Statistics

Installs: 31

Dependents: 0

Suggesters: 1

Stars: 1

Open Issues: 0

v1.2.1 2025-11-26 14:30 UTC

Requires

Requires (Dev)

Suggests

None

Provides

None

Conflicts

None

Replaces

None

MIT 809d310db5d109ef59fdd61fe746b70353e6923b

  • Oleksandr Nosov <alex.woop@4n.com.ua>

README

๐Ÿ‘ Packagist Version
๐Ÿ‘ License: MIT
๐Ÿ‘ PHP Version
๐Ÿ‘ Downloads

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):
    1. POST parameter
    2. GET parameter
    3. URL Path segment
    4. Authenticated User Profile
    5. Session
    6. Cookie
    7. Accept-Language header
    8. Default language fallback
  • Customizable source order โ€” you can define which sources to use and in what order via sourceKeys configuration
  • 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) and userAttribute (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 LanguageChangedEvent when 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 code
  • newLanguage (string) โ€” new language code
  • user (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->language accordingly
  • 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.request event
  • 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 FrameworkContextInterface providing 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