Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43f0161

Browse files
committed
feature #43108 [HttpKernel] Add basic support for language negotiation (GregoireHebert)
This PR was merged into the 5.4 branch. Discussion ---------- [HttpKernel] Add basic support for language negotiation | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Continuation of #36507. Thanks `@GregoireHebert`! This PR adds two options to the framework configuration: - `set_locale_from_accept_language`: Makes the `Request`' locale automatically set based on the `Accept-Language` header (restricted by a new `framework.enabled_locales` config option which replaces `framework.translator.enabled_locales`). The explicit `_locale` request attribute always wins over the `Accept-Language` header when it's set. - `set_content_language_from_locale`: Sets the `Content-Language` Response header based on the `Request`' locale. This is going to be useful for API Platform and related (e.g. Sylius/Sylius#11412). Commits ------- 904b54f [HttpKernel] Add basic support for language negotiation
2 parents 2100d7b + 904b54f commit 43f0161

23 files changed

+259
-12
lines changed

UPGRADE-5.4.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Form
2525
FrameworkBundle
2626
---------------
2727

28+
* Deprecate the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead
2829
* Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead
2930
* Deprecate the public `profiler` service to private
3031
* Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead

UPGRADE-6.0.md

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Form
8989
FrameworkBundle
9090
---------------
9191

92+
* Remove the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead
9293
* Remove the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead
9394
* Remove `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead
9495
* Remove the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ CHANGELOG
44
5.4
55
---
66

7+
* Add `set_locale_from_accept_language` config option to automatically set the request locale based on the `Accept-Language`
8+
HTTP request header and the `framework.enabled_locales` config option
9+
* Add `set_content_language_from_locale` config option to automatically set the `Content-Language` HTTP response header based on the Request locale
10+
* Deprecate the `framework.translator.enabled_locales`, use `framework.enabled_locales` instead
711
* Add autowiring alias for `HttpCache\StoreInterface`
812
* Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead
913
* Deprecate the public `profiler` service to private

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public function getConfigTreeBuilder()
7777
return $v;
7878
})
7979
->end()
80+
->fixXmlConfig('enabled_locale')
8081
->children()
8182
->scalarNode('secret')->end()
8283
->scalarNode('http_method_override')
@@ -86,6 +87,18 @@ public function getConfigTreeBuilder()
8687
->scalarNode('ide')->defaultNull()->end()
8788
->booleanNode('test')->end()
8889
->scalarNode('default_locale')->defaultValue('en')->end()
90+
->booleanNode('set_locale_from_accept_language')
91+
->info('Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed).')
92+
->defaultFalse()
93+
->end()
94+
->booleanNode('set_content_language_from_locale')
95+
->info('Whether to set the Content-Language HTTP header on the Response using the Request locale.')
96+
->defaultFalse()
97+
->end()
98+
->arrayNode('enabled_locales')
99+
->info('Defines the possible locales for the application. This list is used for generating translations files, but also to restrict which locales are allowed when it is set from Accept-Language header (using "set_locale_from_accept_language").')
100+
->prototype('scalar')->end()
101+
->end()
89102
->arrayNode('trusted_hosts')
90103
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
91104
->prototype('scalar')->end()
@@ -814,6 +827,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e
814827
->prototype('scalar')->end()
815828
->end()
816829
->arrayNode('enabled_locales')
830+
->setDeprecated('symfony/framework-bundle', '5.3', 'Option "%node%" at "%path%" is deprecated, set the "framework.enabled_locales" option instead.')
817831
->prototype('scalar')->end()
818832
->defaultValue([])
819833
->end()
@@ -848,7 +862,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e
848862
->arrayNode('locales')
849863
->prototype('scalar')->end()
850864
->defaultValue([])
851-
->info('If not set, all locales listed under framework.translator.enabled_locales are used.')
865+
->info('If not set, all locales listed under framework.enabled_locales are used.')
852866
->end()
853867
->end()
854868
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ public function load(array $configs, ContainerBuilder $container)
275275
}
276276
}
277277

278+
$container->getDefinition('locale_listener')->replaceArgument(3, $config['set_locale_from_accept_language']);
279+
$container->getDefinition('response_listener')->replaceArgument(1, $config['set_content_language_from_locale']);
280+
278281
// If the slugger is used but the String component is not available, we should throw an error
279282
if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) {
280283
$container->register('slugger', 'stdClass')
@@ -297,6 +300,7 @@ public function load(array $configs, ContainerBuilder $container)
297300
$container->setParameter('kernel.http_method_override', $config['http_method_override']);
298301
$container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']);
299302
$container->setParameter('kernel.default_locale', $config['default_locale']);
303+
$container->setParameter('kernel.enabled_locales', $config['enabled_locales']);
300304
$container->setParameter('kernel.error_controller', $config['error_controller']);
301305

302306
if (($config['trusted_proxies'] ?? false) && ($config['trusted_headers'] ?? false)) {
@@ -418,11 +422,13 @@ public function load(array $configs, ContainerBuilder $container)
418422
$this->registerEsiConfiguration($config['esi'], $container, $loader);
419423
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
420424
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
421-
$this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']);
425+
$this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale'], $config['enabled_locales']);
422426
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
423427
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
424428
$this->registerDebugConfiguration($config['php_errors'], $container, $loader);
425-
$this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?? []);
429+
// @deprecated since Symfony 5.4, in 6.0 change to:
430+
// $this->registerRouterConfiguration($config['router'], $container, $loader, $config['enabled_locales']);
431+
$this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?: $config['enabled_locales']);
426432
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
427433
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
428434
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
@@ -1228,7 +1234,7 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s
12281234
return new Reference('assets.empty_version_strategy');
12291235
}
12301236

1231-
private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale)
1237+
private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale, array $enabledLocales)
12321238
{
12331239
if (!$this->isConfigEnabled($container, $config)) {
12341240
$container->removeDefinition('console.command.translation_debug');
@@ -1252,7 +1258,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
12521258
$defaultOptions['cache_dir'] = $config['cache_dir'];
12531259
$translator->setArgument(4, $defaultOptions);
12541260

1255-
$translator->setArgument(5, $config['enabled_locales']);
1261+
// @deprecated since Symfony 5.4, in 6.0 change to:
1262+
// $translator->setArgument(5, $enabledLocales);
1263+
$translator->setArgument(5, $config['enabled_locales'] ?: $enabledLocales);
12561264

12571265
$container->setParameter('translator.logging', $config['logging']);
12581266
$container->setParameter('translator.default_path', $config['default_path']);
@@ -1385,7 +1393,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
13851393
return;
13861394
}
13871395

1388-
$locales = $config['enabled_locales'] ?? [];
1396+
// @deprecated since Symfony 5.4, in 6.0 change to:
1397+
// $locales = $enabledLocales;
1398+
$locales = $config['enabled_locales'] ?: $enabledLocales;
13891399

13901400
foreach ($config['providers'] as $provider) {
13911401
if ($provider['locales']) {

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

+3
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@
3838
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
3939
<xsd:element name="uid" type="uid" minOccurs="0" maxOccurs="1" />
4040
<xsd:element name="notifier" type="notifier" minOccurs="0" maxOccurs="1" />
41+
<xsd:element name="enabled-locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
4142
</xsd:choice>
4243

4344
<xsd:attribute name="http-method-override" type="xsd:boolean" />
4445
<xsd:attribute name="ide" type="xsd:string" />
4546
<xsd:attribute name="secret" type="xsd:string" />
4647
<xsd:attribute name="default-locale" type="xsd:string" />
48+
<xsd:attribute name="set_locale_from_accept_language" type="xsd:boolean" />
49+
<xsd:attribute name="set_content_language_from_locale" type="xsd:boolean" />
4750
<xsd:attribute name="test" type="xsd:boolean" />
4851
<xsd:attribute name="error-controller" type="xsd:string" />
4952
<xsd:attribute name="trusted-hosts" type="xsd:string" />

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
->set('response_listener', ResponseListener::class)
7070
->args([
7171
param('kernel.charset'),
72+
abstract_arg('The "set_content_language_from_locale" config value'),
73+
param('kernel.enabled_locales'),
7274
])
7375
->tag('kernel.event_subscriber')
7476

@@ -80,6 +82,8 @@
8082
service('request_stack'),
8183
param('kernel.default_locale'),
8284
service('router')->ignoreOnInvalid(),
85+
abstract_arg('The "set_locale_from_accept_language" config value'),
86+
param('kernel.enabled_locales'),
8387
])
8488
->tag('kernel.event_subscriber')
8589

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

+3
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ protected static function getBundleDefaultConfig()
370370
'http_method_override' => true,
371371
'ide' => null,
372372
'default_locale' => 'en',
373+
'enabled_locales' => [],
374+
'set_locale_from_accept_language' => false,
375+
'set_content_language_from_locale' => false,
373376
'secret' => 's3cr3t',
374377
'trusted_hosts' => [],
375378
'trusted_headers' => [

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
$container->loadFromExtension('framework', [
44
'secret' => 's3cr3t',
55
'default_locale' => 'fr',
6+
'enabled_locales' => ['fr', 'en'],
67
'csrf_protection' => true,
78
'form' => [
89
'csrf_protection' => [
@@ -51,7 +52,6 @@
5152
'fallback' => 'fr',
5253
'paths' => ['%kernel.project_dir%/Fixtures/translations'],
5354
'cache_dir' => '%kernel.cache_dir%/translations',
54-
'enabled_locales' => ['fr', 'en'],
5555
],
5656
'validation' => [
5757
'enabled' => true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
$container->loadFromExtension('framework', [
4+
'secret' => 's3cr3t',
5+
'default_locale' => 'fr',
6+
'router' => [
7+
'resource' => '%kernel.project_dir%/config/routing.xml',
8+
'type' => 'xml',
9+
'utf8' => true,
10+
],
11+
'translator' => [
12+
'enabled' => true,
13+
'fallback' => 'fr',
14+
'paths' => ['%kernel.project_dir%/Fixtures/translations'],
15+
'cache_dir' => '%kernel.cache_dir%/translations',
16+
'enabled_locales' => ['fr', 'en'],
17+
],
18+
]);

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
88

99
<framework:config secret="s3cr3t" ide="file%%link%%format" default-locale="fr" http-method-override="false">
10+
<framework:enabled-locale>fr</framework:enabled-locale>
11+
<framework:enabled-locale>en</framework:enabled-locale>
1012
<framework:csrf-protection />
1113
<framework:form legacy-error-messages="false">
1214
<framework:csrf-protection field-name="_csrf"/>
@@ -28,8 +30,6 @@
2830
<framework:assets version="v1" />
2931
<framework:translator enabled="true" fallback="fr" logging="true" cache-dir="%kernel.cache_dir%/translations">
3032
<framework:path>%kernel.project_dir%/Fixtures/translations</framework:path>
31-
<framework:enabled-locale>fr</framework:enabled-locale>
32-
<framework:enabled-locale>en</framework:enabled-locale>
3333
</framework:translator>
3434
<framework:validation enabled="true" />
3535
<framework:annotations cache="file" debug="true" file-cache-dir="%kernel.cache_dir%/annotations" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xmlns:framework="http://symfony.com/schema/dic/symfony"
6+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
7+
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
8+
9+
<framework:config secret="s3cr3t" ide="file%%link%%format" default-locale="fr" http-method-override="false">
10+
<framework:router resource="%kernel.project_dir%/config/routing.xml" type="xml" utf8="true" />
11+
<framework:translator enabled="true" fallback="fr" logging="true" cache-dir="%kernel.cache_dir%/translations">
12+
<framework:path>%kernel.project_dir%/Fixtures/translations</framework:path>
13+
<framework:enabled-locale>fr</framework:enabled-locale>
14+
<framework:enabled-locale>en</framework:enabled-locale>
15+
</framework:translator>
16+
</framework:config>
17+
</container>

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
framework:
22
secret: s3cr3t
33
default_locale: fr
4+
enabled_locales: ['fr', 'en']
45
csrf_protection: true
56
form:
67
csrf_protection:
@@ -42,7 +43,6 @@ framework:
4243
default_path: '%kernel.project_dir%/translations'
4344
cache_dir: '%kernel.cache_dir%/translations'
4445
paths: ['%kernel.project_dir%/Fixtures/translations']
45-
enabled_locales: [fr, en]
4646
validation:
4747
enabled: true
4848
annotations:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
framework:
2+
secret: s3cr3t
3+
default_locale: fr
4+
router:
5+
resource: '%kernel.project_dir%/config/routing.xml'
6+
type: xml
7+
utf8: true
8+
translator:
9+
enabled: true
10+
fallback: fr
11+
default_path: '%kernel.project_dir%/translations'
12+
cache_dir: '%kernel.cache_dir%/translations'
13+
paths: ['%kernel.project_dir%/Fixtures/translations']
14+
enabled_locales: [ 'fr', 'en' ]

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

+16
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,22 @@ public function testRouter()
546546
$this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2));
547547
}
548548

549+
/**
550+
* @group legacy
551+
*/
552+
public function testRouterWithLegacyTranslatorEnabledLocales()
553+
{
554+
$container = $this->createContainerFromFile('legacy_translator_enabled_locales');
555+
556+
$this->assertTrue($container->has('router'), '->registerRouterConfiguration() loads routing.xml');
557+
$arguments = $container->findDefinition('router')->getArguments();
558+
$this->assertEquals($container->getParameter('kernel.project_dir').'/config/routing.xml', $container->getParameter('router.resource'), '->registerRouterConfiguration() sets routing resource');
559+
$this->assertEquals('%router.resource%', $arguments[1], '->registerRouterConfiguration() sets routing resource');
560+
$this->assertEquals('xml', $arguments[2]['resource_type'], '->registerRouterConfiguration() sets routing resource type');
561+
562+
$this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2));
563+
}
564+
549565
public function testRouterRequiresResourceOption()
550566
{
551567
$this->expectException(InvalidConfigurationException::class);

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ imports:
44
framework:
55
secret: '%secret%'
66
default_locale: '%env(LOCALE)%'
7+
enabled_locales: ['%env(LOCALE)%']
78
session:
89
storage_factory_id: session.storage.factory.native
910
cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Slugger/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ imports:
55
framework:
66
secret: '%secret%'
77
default_locale: '%env(LOCALE)%'
8+
enabled_locales: ['%env(LOCALE)%']
89
translator:
910
fallbacks:
1011
- '%env(LOCALE)%'

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/TransDebug/config.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ imports:
55
framework:
66
secret: '%secret%'
77
default_locale: '%env(LOCALE)%'
8+
enabled_locales: ['%env(LOCALE)%']
89
translator:
910
fallbacks:
1011
- '%env(LOCALE)%'

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/config/framework.yml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ framework:
88
legacy_error_messages: false
99
test: true
1010
default_locale: en
11+
enabled_locales: ['en', 'fr']
1112
session:
1213
storage_factory_id: session.storage.factory.mock_file
1314

src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ class LocaleListener implements EventSubscriberInterface
3232
private $router;
3333
private $defaultLocale;
3434
private $requestStack;
35+
private $useAcceptLanguageHeader;
36+
private $enabledLocales;
3537

36-
public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', RequestContextAwareInterface $router = null)
38+
public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', RequestContextAwareInterface $router = null, bool $useAcceptLanguageHeader = false, array $enabledLocales = [])
3739
{
3840
$this->defaultLocale = $defaultLocale;
3941
$this->requestStack = $requestStack;
4042
$this->router = $router;
43+
$this->useAcceptLanguageHeader = $useAcceptLanguageHeader;
44+
$this->enabledLocales = $enabledLocales;
4145
}
4246

4347
public function setDefaultLocale(KernelEvent $event)
@@ -64,6 +68,8 @@ private function setLocale(Request $request)
6468
{
6569
if ($locale = $request->attributes->get('_locale')) {
6670
$request->setLocale($locale);
71+
} elseif ($this->useAcceptLanguageHeader && $this->enabledLocales && ($preferredLanguage = $request->getPreferredLanguage($this->enabledLocales))) {
72+
$request->setLocale($preferredLanguage);
6773
}
6874
}
6975

src/Symfony/Component/HttpKernel/EventListener/ResponseListener.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
class ResponseListener implements EventSubscriberInterface
2626
{
2727
private $charset;
28+
private $addContentLanguageHeader;
2829

29-
public function __construct(string $charset)
30+
public function __construct(string $charset, bool $addContentLanguageHeader = false)
3031
{
3132
$this->charset = $charset;
33+
$this->addContentLanguageHeader = $addContentLanguageHeader;
3234
}
3335

3436
/**
@@ -46,6 +48,11 @@ public function onKernelResponse(ResponseEvent $event)
4648
$response->setCharset($this->charset);
4749
}
4850

51+
if ($this->addContentLanguageHeader && !$response->isInformational() && !$response->isEmpty() && !$response->headers->has('Content-Language')) {
52+
$response->headers->set('Content-Language', $event->getRequest()->getLocale());
53+
$response->setVary('Accept-Language', false);
54+
}
55+
4956
$response->prepare($event->getRequest());
5057
}
5158

0 commit comments

Comments
 (0)