diff --git a/src/Symfony/Component/Intl/Data/Generator/LocaleDataGenerator.php b/src/Symfony/Component/Intl/Data/Generator/LocaleDataGenerator.php index 01f0f742ea0ed..19a8b5b7377fe 100644 --- a/src/Symfony/Component/Intl/Data/Generator/LocaleDataGenerator.php +++ b/src/Symfony/Component/Intl/Data/Generator/LocaleDataGenerator.php @@ -57,6 +57,7 @@ public function generateData(GeneratorConfig $config) $locales = $localeScanner->scanLocales($config->getSourceDir().'/locales'); $aliases = $localeScanner->scanAliases($config->getSourceDir().'/locales'); + $parents = $localeScanner->scanParents($config->getSourceDir().'/locales'); // Flip to facilitate lookup $flippedLocales = array_flip($locales); @@ -134,6 +135,12 @@ public function generateData(GeneratorConfig $config) 'Aliases' => $aliases, )); } + + // Write parents locale file for the Translation component + \file_put_contents( + __DIR__.'/../../../Translation/Resources/data/parents.json', + \json_encode($parents, \JSON_PRETTY_PRINT).\PHP_EOL + ); } private function generateLocaleName($locale, $displayLocale) diff --git a/src/Symfony/Component/Intl/Data/Util/LocaleScanner.php b/src/Symfony/Component/Intl/Data/Util/LocaleScanner.php index 0604593f6e1d2..884a0d9e06268 100644 --- a/src/Symfony/Component/Intl/Data/Util/LocaleScanner.php +++ b/src/Symfony/Component/Intl/Data/Util/LocaleScanner.php @@ -82,4 +82,24 @@ public function scanAliases($sourceDir) return $aliases; } + + /** + * Returns all locale parents found in the given directory. + */ + public function scanParents(string $sourceDir): array + { + $locales = $this->scanLocales($sourceDir); + $fallbacks = array(); + + foreach ($locales as $locale) { + $content = \file_get_contents($sourceDir.'/'.$locale.'.txt'); + + // Aliases contain the text "%%PARENT" followed by the aliased locale + if (\preg_match('/%%Parent{"([^"]+)"}/', $content, $matches)) { + $fallbacks[$locale] = $matches[1]; + } + } + + return $fallbacks; + } } diff --git a/src/Symfony/Component/Intl/Tests/Data/Util/LocaleScannerTest.php b/src/Symfony/Component/Intl/Tests/Data/Util/LocaleScannerTest.php index f76c71d8f7a4d..abbe7f83f83e9 100644 --- a/src/Symfony/Component/Intl/Tests/Data/Util/LocaleScannerTest.php +++ b/src/Symfony/Component/Intl/Tests/Data/Util/LocaleScannerTest.php @@ -42,10 +42,13 @@ protected function setUp() $this->filesystem->touch($this->directory.'/en.txt'); $this->filesystem->touch($this->directory.'/en_alias.txt'); + $this->filesystem->touch($this->directory.'/en_child.txt'); $this->filesystem->touch($this->directory.'/de.txt'); $this->filesystem->touch($this->directory.'/de_alias.txt'); + $this->filesystem->touch($this->directory.'/de_child.txt'); $this->filesystem->touch($this->directory.'/fr.txt'); $this->filesystem->touch($this->directory.'/fr_alias.txt'); + $this->filesystem->touch($this->directory.'/fr_child.txt'); $this->filesystem->touch($this->directory.'/root.txt'); $this->filesystem->touch($this->directory.'/supplementalData.txt'); $this->filesystem->touch($this->directory.'/supplementaldata.txt'); @@ -54,6 +57,9 @@ protected function setUp() file_put_contents($this->directory.'/en_alias.txt', 'en_alias{"%%ALIAS"{"en"}}'); file_put_contents($this->directory.'/de_alias.txt', 'de_alias{"%%ALIAS"{"de"}}'); file_put_contents($this->directory.'/fr_alias.txt', 'fr_alias{"%%ALIAS"{"fr"}}'); + file_put_contents($this->directory.'/en_child.txt', 'en_GB{%%Parent{"en"}}'); + file_put_contents($this->directory.'/de_child.txt', 'en_GB{%%Parent{"de"}}'); + file_put_contents($this->directory.'/fr_child.txt', 'en_GB{%%Parent{"fr"}}'); } protected function tearDown() @@ -63,7 +69,7 @@ protected function tearDown() public function testScanLocales() { - $sortedLocales = array('de', 'de_alias', 'en', 'en_alias', 'fr', 'fr_alias'); + $sortedLocales = array('de', 'de_alias', 'de_child', 'en', 'en_alias', 'en_child', 'fr', 'fr_alias', 'fr_child'); $this->assertSame($sortedLocales, $this->scanner->scanLocales($this->directory)); } @@ -74,4 +80,11 @@ public function testScanAliases() $this->assertSame($sortedAliases, $this->scanner->scanAliases($this->directory)); } + + public function testScanParents() + { + $sortedParents = array('de_child' => 'de', 'en_child' => 'en', 'fr_child' => 'fr'); + + $this->assertSame($sortedParents, $this->scanner->scanParents($this->directory)); + } } diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 527b80495d572..ac316b8bface1 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * Started using ICU parent locales as fallback locales. + 4.1.0 ----- diff --git a/src/Symfony/Component/Translation/Resources/data/parents.json b/src/Symfony/Component/Translation/Resources/data/parents.json new file mode 100644 index 0000000000000..09709d230292c --- /dev/null +++ b/src/Symfony/Component/Translation/Resources/data/parents.json @@ -0,0 +1,136 @@ +{ + "az_Cyrl": "root", + "bs_Cyrl": "root", + "en_150": "en_001", + "en_AG": "en_001", + "en_AI": "en_001", + "en_AT": "en_150", + "en_AU": "en_001", + "en_BB": "en_001", + "en_BE": "en_001", + "en_BM": "en_001", + "en_BS": "en_001", + "en_BW": "en_001", + "en_BZ": "en_001", + "en_CA": "en_001", + "en_CC": "en_001", + "en_CH": "en_150", + "en_CK": "en_001", + "en_CM": "en_001", + "en_CX": "en_001", + "en_CY": "en_001", + "en_DE": "en_150", + "en_DG": "en_001", + "en_DK": "en_150", + "en_DM": "en_001", + "en_ER": "en_001", + "en_FI": "en_150", + "en_FJ": "en_001", + "en_FK": "en_001", + "en_FM": "en_001", + "en_GB": "en_001", + "en_GD": "en_001", + "en_GG": "en_001", + "en_GH": "en_001", + "en_GI": "en_001", + "en_GM": "en_001", + "en_GY": "en_001", + "en_HK": "en_001", + "en_IE": "en_001", + "en_IL": "en_001", + "en_IM": "en_001", + "en_IN": "en_001", + "en_IO": "en_001", + "en_JE": "en_001", + "en_JM": "en_001", + "en_KE": "en_001", + "en_KI": "en_001", + "en_KN": "en_001", + "en_KY": "en_001", + "en_LC": "en_001", + "en_LR": "en_001", + "en_LS": "en_001", + "en_MG": "en_001", + "en_MO": "en_001", + "en_MS": "en_001", + "en_MT": "en_001", + "en_MU": "en_001", + "en_MW": "en_001", + "en_MY": "en_001", + "en_NA": "en_001", + "en_NF": "en_001", + "en_NG": "en_001", + "en_NL": "en_150", + "en_NR": "en_001", + "en_NU": "en_001", + "en_NZ": "en_001", + "en_PG": "en_001", + "en_PH": "en_001", + "en_PK": "en_001", + "en_PN": "en_001", + "en_PW": "en_001", + "en_RW": "en_001", + "en_SB": "en_001", + "en_SC": "en_001", + "en_SD": "en_001", + "en_SE": "en_150", + "en_SG": "en_001", + "en_SH": "en_001", + "en_SI": "en_150", + "en_SL": "en_001", + "en_SS": "en_001", + "en_SX": "en_001", + "en_SZ": "en_001", + "en_TC": "en_001", + "en_TK": "en_001", + "en_TO": "en_001", + "en_TT": "en_001", + "en_TV": "en_001", + "en_TZ": "en_001", + "en_UG": "en_001", + "en_VC": "en_001", + "en_VG": "en_001", + "en_VU": "en_001", + "en_WS": "en_001", + "en_ZA": "en_001", + "en_ZM": "en_001", + "en_ZW": "en_001", + "es_AR": "es_419", + "es_BO": "es_419", + "es_BR": "es_419", + "es_BZ": "es_419", + "es_CL": "es_419", + "es_CO": "es_419", + "es_CR": "es_419", + "es_CU": "es_419", + "es_DO": "es_419", + "es_EC": "es_419", + "es_GT": "es_419", + "es_HN": "es_419", + "es_MX": "es_419", + "es_NI": "es_419", + "es_PA": "es_419", + "es_PE": "es_419", + "es_PR": "es_419", + "es_PY": "es_419", + "es_SV": "es_419", + "es_US": "es_419", + "es_UY": "es_419", + "es_VE": "es_419", + "pa_Arab": "root", + "pt_AO": "pt_PT", + "pt_CH": "pt_PT", + "pt_CV": "pt_PT", + "pt_GQ": "pt_PT", + "pt_GW": "pt_PT", + "pt_LU": "pt_PT", + "pt_MO": "pt_PT", + "pt_MZ": "pt_PT", + "pt_ST": "pt_PT", + "pt_TL": "pt_PT", + "sr_Latn": "root", + "uz_Arab": "root", + "uz_Cyrl": "root", + "zh_Hant": "root", + "zh_Hant_MO": "zh_Hant_HK" +} diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 3e54839f77ee8..b0efefb7d089e 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -234,6 +234,42 @@ public function testTransWithFallbackLocaleFile($format, $loader) $this->assertEquals('bar', $translator->trans('foo', array(), 'resources')); } + public function testTransWithIcuFallbackLocale() + { + $translator = new Translator('en_GB'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', array('foo' => 'foofoo'), 'en_GB'); + $translator->addResource('array', array('bar' => 'foobar'), 'en_001'); + $translator->addResource('array', array('baz' => 'foobaz'), 'en'); + $this->assertSame('foofoo', $translator->trans('foo')); + $this->assertSame('foobar', $translator->trans('bar')); + $this->assertSame('foobaz', $translator->trans('baz')); + } + + public function testTransWithIcuVariantFallbackLocale() + { + $translator = new Translator('en_GB_scouse'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', array('foo' => 'foofoo'), 'en_GB_scouse'); + $translator->addResource('array', array('bar' => 'foobar'), 'en_GB'); + $translator->addResource('array', array('baz' => 'foobaz'), 'en_001'); + $translator->addResource('array', array('qux' => 'fooqux'), 'en'); + $this->assertSame('foofoo', $translator->trans('foo')); + $this->assertSame('foobar', $translator->trans('bar')); + $this->assertSame('foobaz', $translator->trans('baz')); + $this->assertSame('fooqux', $translator->trans('qux')); + } + + public function testTransWithIcuRootFallbackLocale() + { + $translator = new Translator('az_Cyrl'); + $translator->addLoader('array', new ArrayLoader()); + $translator->addResource('array', array('foo' => 'foofoo'), 'az_Cyrl'); + $translator->addResource('array', array('bar' => 'foobar'), 'az'); + $this->assertSame('foofoo', $translator->trans('foo')); + $this->assertSame('bar', $translator->trans('bar')); + } + public function testTransWithFallbackLocaleBis() { $translator = new Translator('en_US'); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 97690410e2410..3c7b9b3ad71b9 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -73,6 +73,11 @@ class Translator implements TranslatorInterface, TranslatorBagInterface */ private $configCacheFactory; + /** + * @var array|null + */ + private $parentLocales; + /** * @throws InvalidArgumentException If a locale contains invalid characters */ @@ -392,6 +397,10 @@ private function loadFallbackCatalogues($locale): void protected function computeFallbackLocales($locale) { + if (null === $this->parentLocales) { + $parentLocales = \json_decode(\file_get_contents(__DIR__.'/Resources/data/parents.json'), true); + } + $locales = array(); foreach ($this->fallbackLocales as $fallback) { if ($fallback === $locale) { @@ -401,8 +410,20 @@ protected function computeFallbackLocales($locale) $locales[] = $fallback; } - if (false !== strrchr($locale, '_')) { - array_unshift($locales, substr($locale, 0, -\strlen(strrchr($locale, '_')))); + while ($locale) { + $parent = $parentLocales[$locale] ?? null; + + if (!$parent && false !== strrchr($locale, '_')) { + $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); + } elseif ('root' !== $parent) { + $locale = $parent; + } else { + $locale = null; + } + + if (null !== $locale) { + array_unshift($locales, $locale); + } } return array_unique($locales);