Skip to content

[HttpKernel] Add basic support for language negotiation #43108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions UPGRADE-5.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Form
FrameworkBundle
---------------

* Deprecate the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead
* Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead
* Deprecate the public `profiler` service to private
* Deprecate `get()`, `has()`, `getDoctrine()`, and `dispatchMessage()` in `AbstractController`, use method/constructor injection instead
Expand Down
1 change: 1 addition & 0 deletions UPGRADE-6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Form
FrameworkBundle
---------------

* Remove the `framework.translator.enabled_locales` config option, use `framework.enabled_locales` instead
* Remove the `session.storage` alias and `session.storage.*` services, use the `session.storage.factory` alias and `session.storage.factory.*` services instead
* Remove `framework.session.storage_id` configuration option, use the `framework.session.storage_factory_id` configuration option instead
* 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
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ CHANGELOG
5.4
---

* Add `set_locale_from_accept_language` config option to automatically set the request locale based on the `Accept-Language`
HTTP request header and the `framework.enabled_locales` config option
* Add `set_content_language_from_locale` config option to automatically set the `Content-Language` HTTP response header based on the Request locale
* Deprecate the `framework.translator.enabled_locales`, use `framework.enabled_locales` instead
* Add autowiring alias for `HttpCache\StoreInterface`
* Deprecate the `AdapterInterface` autowiring alias, use `CacheItemPoolInterface` instead
* Deprecate the public `profiler` service to private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public function getConfigTreeBuilder()
return $v;
})
->end()
->fixXmlConfig('enabled_locale')
->children()
->scalarNode('secret')->end()
->scalarNode('http_method_override')
Expand All @@ -85,6 +86,18 @@ public function getConfigTreeBuilder()
->scalarNode('ide')->defaultNull()->end()
->booleanNode('test')->end()
->scalarNode('default_locale')->defaultValue('en')->end()
->booleanNode('set_locale_from_accept_language')
->info('Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed).')
->defaultFalse()
->end()
->booleanNode('set_content_language_from_locale')
->info('Whether to set the Content-Language HTTP header on the Response using the Request locale.')
->defaultFalse()
->end()
->arrayNode('enabled_locales')
->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").')
->prototype('scalar')->end()
->end()
->arrayNode('trusted_hosts')
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
->prototype('scalar')->end()
Expand Down Expand Up @@ -812,6 +825,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e
->prototype('scalar')->end()
->end()
->arrayNode('enabled_locales')
->setDeprecated('symfony/framework-bundle', '5.3', 'Option "%node%" at "%path%" is deprecated, set the "framework.enabled_locales" option instead.')
->prototype('scalar')->end()
->defaultValue([])
->end()
Expand Down Expand Up @@ -846,7 +860,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $e
->arrayNode('locales')
->prototype('scalar')->end()
->defaultValue([])
->info('If not set, all locales listed under framework.translator.enabled_locales are used.')
->info('If not set, all locales listed under framework.enabled_locales are used.')
->end()
->end()
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ public function load(array $configs, ContainerBuilder $container)
}
}

$container->getDefinition('locale_listener')->replaceArgument(3, $config['set_locale_from_accept_language']);
$container->getDefinition('response_listener')->replaceArgument(1, $config['set_content_language_from_locale']);

// If the slugger is used but the String component is not available, we should throw an error
if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) {
$container->register('slugger', 'stdClass')
Expand All @@ -297,6 +300,7 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter('kernel.http_method_override', $config['http_method_override']);
$container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']);
$container->setParameter('kernel.default_locale', $config['default_locale']);
$container->setParameter('kernel.enabled_locales', $config['enabled_locales']);
$container->setParameter('kernel.error_controller', $config['error_controller']);

if (($config['trusted_proxies'] ?? false) && ($config['trusted_headers'] ?? false)) {
Expand Down Expand Up @@ -418,11 +422,13 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
$this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']);
$this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale'], $config['enabled_locales']);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
$this->registerDebugConfiguration($config['php_errors'], $container, $loader);
$this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?? []);
// @deprecated since Symfony 5.4, in 6.0 change to:
// $this->registerRouterConfiguration($config['router'], $container, $loader, $config['enabled_locales']);
$this->registerRouterConfiguration($config['router'], $container, $loader, $config['translator']['enabled_locales'] ?: $config['enabled_locales']);
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
Expand Down Expand Up @@ -1221,7 +1227,7 @@ private function createVersion(ContainerBuilder $container, ?string $version, ?s
return new Reference('assets.empty_version_strategy');
}

private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale)
private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale, array $enabledLocales)
{
if (!$this->isConfigEnabled($container, $config)) {
$container->removeDefinition('console.command.translation_debug');
Expand All @@ -1245,7 +1251,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
$defaultOptions['cache_dir'] = $config['cache_dir'];
$translator->setArgument(4, $defaultOptions);

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

$container->setParameter('translator.logging', $config['logging']);
$container->setParameter('translator.default_path', $config['default_path']);
Expand Down Expand Up @@ -1378,7 +1386,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
return;
}

$locales = $config['enabled_locales'] ?? [];
// @deprecated since Symfony 5.4, in 6.0 change to:
// $locales = $enabledLocales;
$locales = $config['enabled_locales'] ?: $enabledLocales;

foreach ($config['providers'] as $provider) {
if ($provider['locales']) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
<xsd:element name="uid" type="uid" minOccurs="0" maxOccurs="1" />
<xsd:element name="notifier" type="notifier" minOccurs="0" maxOccurs="1" />
<xsd:element name="enabled-locale" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>

<xsd:attribute name="http-method-override" type="xsd:boolean" />
<xsd:attribute name="ide" type="xsd:string" />
<xsd:attribute name="secret" type="xsd:string" />
<xsd:attribute name="default-locale" type="xsd:string" />
<xsd:attribute name="set_locale_from_accept_language" type="xsd:boolean" />
<xsd:attribute name="set_content_language_from_locale" type="xsd:boolean" />
<xsd:attribute name="test" type="xsd:boolean" />
<xsd:attribute name="error-controller" type="xsd:string" />
<xsd:attribute name="trusted-hosts" type="xsd:string" />
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
->set('response_listener', ResponseListener::class)
->args([
param('kernel.charset'),
abstract_arg('The "set_content_language_from_locale" config value'),
param('kernel.enabled_locales'),
])
->tag('kernel.event_subscriber')

Expand All @@ -80,6 +82,8 @@
service('request_stack'),
param('kernel.default_locale'),
service('router')->ignoreOnInvalid(),
abstract_arg('The "set_locale_from_accept_language" config value'),
param('kernel.enabled_locales'),
])
->tag('kernel.event_subscriber')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ protected static function getBundleDefaultConfig()
'http_method_override' => true,
'ide' => null,
'default_locale' => 'en',
'enabled_locales' => [],
'set_locale_from_accept_language' => false,
'set_content_language_from_locale' => false,
'secret' => 's3cr3t',
'trusted_hosts' => [],
'trusted_headers' => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
$container->loadFromExtension('framework', [
'secret' => 's3cr3t',
'default_locale' => 'fr',
'enabled_locales' => ['fr', 'en'],
'csrf_protection' => true,
'form' => [
'csrf_protection' => [
Expand Down Expand Up @@ -51,7 +52,6 @@
'fallback' => 'fr',
'paths' => ['%kernel.project_dir%/Fixtures/translations'],
'cache_dir' => '%kernel.cache_dir%/translations',
'enabled_locales' => ['fr', 'en'],
],
'validation' => [
'enabled' => true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

$container->loadFromExtension('framework', [
'secret' => 's3cr3t',
'default_locale' => 'fr',
'router' => [
'resource' => '%kernel.project_dir%/config/routing.xml',
'type' => 'xml',
'utf8' => true,
],
'translator' => [
'enabled' => true,
'fallback' => 'fr',
'paths' => ['%kernel.project_dir%/Fixtures/translations'],
'cache_dir' => '%kernel.cache_dir%/translations',
'enabled_locales' => ['fr', 'en'],
],
]);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

<framework:config secret="s3cr3t" ide="file%%link%%format" default-locale="fr" http-method-override="false">
<framework:enabled-locale>fr</framework:enabled-locale>
<framework:enabled-locale>en</framework:enabled-locale>
<framework:csrf-protection />
<framework:form legacy-error-messages="false">
<framework:csrf-protection field-name="_csrf"/>
Expand All @@ -28,8 +30,6 @@
<framework:assets version="v1" />
<framework:translator enabled="true" fallback="fr" logging="true" cache-dir="%kernel.cache_dir%/translations">
<framework:path>%kernel.project_dir%/Fixtures/translations</framework:path>
<framework:enabled-locale>fr</framework:enabled-locale>
<framework:enabled-locale>en</framework:enabled-locale>
</framework:translator>
<framework:validation enabled="true" />
<framework:annotations cache="file" debug="true" file-cache-dir="%kernel.cache_dir%/annotations" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

<framework:config secret="s3cr3t" ide="file%%link%%format" default-locale="fr" http-method-override="false">
<framework:router resource="%kernel.project_dir%/config/routing.xml" type="xml" utf8="true" />
<framework:translator enabled="true" fallback="fr" logging="true" cache-dir="%kernel.cache_dir%/translations">
<framework:path>%kernel.project_dir%/Fixtures/translations</framework:path>
<framework:enabled-locale>fr</framework:enabled-locale>
<framework:enabled-locale>en</framework:enabled-locale>
</framework:translator>
</framework:config>
</container>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
framework:
secret: s3cr3t
default_locale: fr
enabled_locales: ['fr', 'en']
csrf_protection: true
form:
csrf_protection:
Expand Down Expand Up @@ -42,7 +43,6 @@ framework:
default_path: '%kernel.project_dir%/translations'
cache_dir: '%kernel.cache_dir%/translations'
paths: ['%kernel.project_dir%/Fixtures/translations']
enabled_locales: [fr, en]
validation:
enabled: true
annotations:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
framework:
secret: s3cr3t
default_locale: fr
router:
resource: '%kernel.project_dir%/config/routing.xml'
type: xml
utf8: true
translator:
enabled: true
fallback: fr
default_path: '%kernel.project_dir%/translations'
cache_dir: '%kernel.cache_dir%/translations'
paths: ['%kernel.project_dir%/Fixtures/translations']
enabled_locales: [ 'fr', 'en' ]
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,22 @@ public function testRouter()
$this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2));
}

/**
* @group legacy
*/
public function testRouterWithLegacyTranslatorEnabledLocales()
{
$container = $this->createContainerFromFile('legacy_translator_enabled_locales');

$this->assertTrue($container->has('router'), '->registerRouterConfiguration() loads routing.xml');
$arguments = $container->findDefinition('router')->getArguments();
$this->assertEquals($container->getParameter('kernel.project_dir').'/config/routing.xml', $container->getParameter('router.resource'), '->registerRouterConfiguration() sets routing resource');
$this->assertEquals('%router.resource%', $arguments[1], '->registerRouterConfiguration() sets routing resource');
$this->assertEquals('xml', $arguments[2]['resource_type'], '->registerRouterConfiguration() sets routing resource type');

$this->assertSame(['_locale' => 'fr|en'], $container->getDefinition('routing.loader')->getArgument(2));
}

public function testRouterRequiresResourceOption()
{
$this->expectException(InvalidConfigurationException::class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ imports:
framework:
secret: '%secret%'
default_locale: '%env(LOCALE)%'
enabled_locales: ['%env(LOCALE)%']
session:
storage_factory_id: session.storage.factory.native
cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ imports:
framework:
secret: '%secret%'
default_locale: '%env(LOCALE)%'
enabled_locales: ['%env(LOCALE)%']
translator:
fallbacks:
- '%env(LOCALE)%'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ imports:
framework:
secret: '%secret%'
default_locale: '%env(LOCALE)%'
enabled_locales: ['%env(LOCALE)%']
translator:
fallbacks:
- '%env(LOCALE)%'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ framework:
legacy_error_messages: false
test: true
default_locale: en
enabled_locales: ['en', 'fr']
session:
storage_factory_id: session.storage.factory.mock_file

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ class LocaleListener implements EventSubscriberInterface
private $router;
private $defaultLocale;
private $requestStack;
private $useAcceptLanguageHeader;
private $enabledLocales;

public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', RequestContextAwareInterface $router = null)
public function __construct(RequestStack $requestStack, string $defaultLocale = 'en', RequestContextAwareInterface $router = null, bool $useAcceptLanguageHeader = false, array $enabledLocales = [])
{
$this->defaultLocale = $defaultLocale;
$this->requestStack = $requestStack;
$this->router = $router;
$this->useAcceptLanguageHeader = $useAcceptLanguageHeader;
$this->enabledLocales = $enabledLocales;
}

public function setDefaultLocale(KernelEvent $event)
Expand All @@ -64,6 +68,8 @@ private function setLocale(Request $request)
{
if ($locale = $request->attributes->get('_locale')) {
$request->setLocale($locale);
} elseif ($this->useAcceptLanguageHeader && $this->enabledLocales && ($preferredLanguage = $request->getPreferredLanguage($this->enabledLocales))) {
$request->setLocale($preferredLanguage);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
class ResponseListener implements EventSubscriberInterface
{
private $charset;
private $addContentLanguageHeader;

public function __construct(string $charset)
public function __construct(string $charset, bool $addContentLanguageHeader = false)
{
$this->charset = $charset;
$this->addContentLanguageHeader = $addContentLanguageHeader;
}

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

if ($this->addContentLanguageHeader && !$response->isInformational() && !$response->isEmpty() && !$response->headers->has('Content-Language')) {
$response->headers->set('Content-Language', $event->getRequest()->getLocale());
$response->setVary('Accept-Language', false);
}

$response->prepare($event->getRequest());
}

Expand Down
Loading