Skip to content

[Translation] Extract locale fallback computation into a dedicated class #45553

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

Open
wants to merge 10 commits into
base: 7.4
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ private function loadFallbackCatalogues(string $locale, array $transPaths): arra
{
$fallbackCatalogues = [];
if ($this->translator instanceof Translator || $this->translator instanceof DataCollectorTranslator || $this->translator instanceof LoggingTranslator) {
foreach ($this->translator->getFallbackLocales() as $fallbackLocale) {
foreach ($this->translator->getFallbackLocaleProvider()->getUltimateFallbackLocales() as $fallbackLocale) {
if ($fallbackLocale === $locale) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,14 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder
$container->setAlias('translator', 'translator.default')->setPublic(true);
$container->setAlias('translator.formatter', new Alias($config['formatter'], false));
$translator = $container->findDefinition('translator.default');
$translator->addMethodCall('setFallbackLocales', [$config['fallbacks'] ?: [$defaultLocale]]);

if ($container->has('translation.fallback_locale_provider')) {
$container
->findDefinition('translation.fallback_locale_provider')
->replaceArgument(0, [$config['fallbacks'] ?: [$defaultLocale]]);
} else {
$translator->addMethodCall('setFallbackLocales', [$config['fallbacks'] ?: [$defaultLocale]]);
}

$defaultOptions = $translator->getArgument(4);
$defaultOptions['cache_dir'] = $config['cache_dir'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use Symfony\Component\Translation\Extractor\ChainExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\Extractor\PhpExtractor;
use Symfony\Component\Translation\FallbackLocaleProvider;
use Symfony\Component\Translation\FallbackLocaleProviderInterface;
use Symfony\Component\Translation\Formatter\MessageFormatter;
use Symfony\Component\Translation\Loader\CsvFileLoader;
use Symfony\Component\Translation\Loader\IcuDatFileLoader;
Expand Down Expand Up @@ -59,6 +61,7 @@
'debug' => param('kernel.debug'),
],
abstract_arg('enabled locales'),
service('translation.fallback_locale_provider')->ignoreOnInvalid(),
])
->call('setConfigCacheFactory', [service('config_cache_factory')])
->tag('kernel.locale_aware')
Expand Down Expand Up @@ -163,4 +166,10 @@
->tag('container.service_subscriber', ['id' => 'translator'])
->tag('kernel.cache_warmer')
;

if (class_exists(FallbackLocaleProvider::class)) {
$container->services()
->set('translation.fallback_locale_provider', FallbackLocaleProvider::class)
->alias(FallbackLocaleProviderInterface::class, 'translation.fallback_locale_provider');
}
};
1 change: 1 addition & 0 deletions src/Symfony/Component/Translation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Parameters implementing `TranslatableInterface` are processed
* Add the file extension to the `XliffFileDumper` constructor
* Add `FallbackLocaleProvider` to configure the fallback locale chain in `Translator`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch must be rebased on 6.4 and this line moves to 6.4 section


5.4
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function lateCollect()
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
$this->data['fallback_locales'] = $this->translator->getFallbackLocaleProvider()->getUltimateFallbackLocales();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public function warmUp(string $cacheDir): array
*/
public function getFallbackLocales(): array
{
trigger_deprecation('symfony/translation', '6.2', 'DataCollectorTranslator::getFallbackLocales() is deprecated. Get the FallbackLocaleProvider instead.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
trigger_deprecation('symfony/translation', '6.2', 'DataCollectorTranslator::getFallbackLocales() is deprecated. Get the FallbackLocaleProvider instead.');
trigger_deprecation('symfony/translation', '6.2', '"%s::getFallbackLocales()" is deprecated, use "%s::getUltimateFallbackLocales()" instead.', DataCollectorTranslator::class, FallbackLocaleProvider::class);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6.2 must be replaced with 6.4 here and in all other trigger_deprecation occurrences.


if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) {
return $this->translator->getFallbackLocales();
}
Expand Down
100 changes: 100 additions & 0 deletions src/Symfony/Component/Translation/FallbackLocaleProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Translation;

use Symfony\Component\Translation\Exception\InvalidArgumentException;

/**
* Derives fallback locales based on ICU parent locale information, by shortening locale
* sub tags and ultimately by going through a list of configured fallback locales.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class FallbackLocaleProvider implements FallbackLocaleProviderInterface
{
/**
* @var string[]
*
* List of fallback locales to add _after_ the ones derived from ICU information.
*/
private array $ultimateFallbackLocales;

private ?array $parentLocales = null;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be static? It is used to cache parsed JSON data, so we might want some extra efficiency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be, I've no strong opinion on it. @nicolas-grekas WDYT?


/**
* @param string[] $ultimateFallbackLocales
*
* @throws InvalidArgumentException If a locale contains invalid characters
*/
public function __construct(array $ultimateFallbackLocales = [])
{
foreach ($ultimateFallbackLocales as $locale) {
LocaleValidator::validate($locale);
}

$this->ultimateFallbackLocales = $ultimateFallbackLocales;
}

/**
* @return string[]
*/
public function getUltimateFallbackLocales(): array
{
return $this->ultimateFallbackLocales;
}

/**
* @return string[]
*/
public function computeFallbackLocales(string $locale): array
{
LocaleValidator::validate($locale);

$this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true);

$originLocale = $locale;
$locales = [];

while ($locale) {
$parent = $this->parentLocales[$locale] ?? null;

if ($parent) {
$locale = 'root' !== $parent ? $parent : null;
} elseif (\function_exists('locale_parse')) {
$localeSubTags = locale_parse($locale);
$locale = null;
if (1 < \count($localeSubTags)) {
array_pop($localeSubTags);
$locale = locale_compose($localeSubTags) ?: null;
}
} elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) {
$locale = substr($locale, 0, $i);
} else {
$locale = null;
}

if (null !== $locale) {
$locales[] = $locale;
}
}

foreach ($this->ultimateFallbackLocales as $fallback) {
if ($fallback === $originLocale) {
continue;
}

$locales[] = $fallback;
}

return array_unique($locales);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Translation;

use Symfony\Component\Translation\Exception\InvalidArgumentException;

/**
* For a given locale, implementations provide the list of alternative locales to
* try when a translation cannot be found.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface FallbackLocaleProviderInterface
{
/**
* @internal
*
* @return string[]
*/
public function getUltimateFallbackLocales(): array;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that it is @internal, should it even be part of the interface?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have callees from external? If not, feel free to indeed remove from the interface. If we have calls from outside the class, do we really need those calls because calling internal classes might be considered a violation.


/**
* For a given locale, this method provides the ordered list of alternative (fallback) locales
* to try.
*
* @return string[]
*/
public function computeFallbackLocales(string $locale): array;
}
34 changes: 34 additions & 0 deletions src/Symfony/Component/Translation/LocaleValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Translation;

use Symfony\Component\Translation\Exception\InvalidArgumentException;

/**
* Asserts (syntactical) validity for given locale identifiers.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class LocaleValidator
{
/**
* Asserts that the locale is valid, throws an Exception if not.
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public static function validate(string $locale): void
{
if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) {
throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
}
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Component/Translation/LoggingTranslator.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ public function getCatalogues(): array
*/
public function getFallbackLocales(): array
{
trigger_deprecation('symfony/translation', '6.2', 'LoggingTranslator::getFallbackLocales() is deprecated. Get the FallbackLocaleProvider instead.');

if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) {
return $this->translator->getFallbackLocales();
}
Expand Down
Loading