2
0
forked from Wavyzz/dolibarr

SwissQR: add Sprain\SwissQrBill and dependencies

This commit is contained in:
Didier 'OdyX' Raboud
2023-03-16 06:47:59 +01:00
parent 55ab3d2c56
commit f53120c915
2192 changed files with 971358 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReader;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\IntlBundleReader;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
/**
* The rule for compiling the currency bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal
*/
abstract class AbstractDataGenerator
{
private BundleCompilerInterface $compiler;
private string $dirName;
public function __construct(BundleCompilerInterface $compiler, string $dirName)
{
$this->compiler = $compiler;
$this->dirName = $dirName;
}
public function generateData(GeneratorConfig $config)
{
$filesystem = new Filesystem();
$localeScanner = new LocaleScanner();
$reader = new BundleEntryReader(new IntlBundleReader());
$writers = $config->getBundleWriters();
$tempDir = sys_get_temp_dir().'/icu-data-'.$this->dirName;
// Prepare filesystem directories
foreach ($writers as $targetDir => $writer) {
$filesystem->remove($targetDir.'/'.$this->dirName);
$filesystem->mkdir($targetDir.'/'.$this->dirName);
}
$filesystem->remove($tempDir);
$filesystem->mkdir($tempDir);
$locales = $this->scanLocales($localeScanner, $config->getSourceDir());
$this->compileTemporaryBundles($this->compiler, $config->getSourceDir(), $tempDir);
$this->preGenerate();
foreach ($locales as $locale) {
$localeData = $this->generateDataForLocale($reader, $tempDir, $locale);
if (null !== $localeData) {
foreach ($writers as $targetDir => $writer) {
$writer->write($targetDir.'/'.$this->dirName, $locale, $localeData);
}
}
}
$rootData = $this->generateDataForRoot($reader, $tempDir);
if (null !== $rootData) {
foreach ($writers as $targetDir => $writer) {
$writer->write($targetDir.'/'.$this->dirName, 'root', $rootData);
}
}
$metaData = $this->generateDataForMeta($reader, $tempDir);
if (null !== $metaData) {
foreach ($writers as $targetDir => $writer) {
$writer->write($targetDir.'/'.$this->dirName, 'meta', $metaData);
}
}
// Clean up
$filesystem->remove($tempDir);
}
/**
* @return string[]
*/
abstract protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array;
abstract protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir);
abstract protected function preGenerate();
abstract protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array;
abstract protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array;
abstract protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array;
}

View File

@@ -0,0 +1,161 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
/**
* The rule for compiling the currency bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal
*/
class CurrencyDataGenerator extends AbstractDataGenerator
{
private const DENYLIST = [
'XBA' => true, // European Composite Unit
'XBB' => true, // European Monetary Unit
'XBC' => true, // European Unit of Account (XBC)
'XBD' => true, // European Unit of Account (XBD)
'XUA' => true, // ADB Unit of Account
'XAU' => true, // Gold
'XAG' => true, // Silver
'XPT' => true, // Platinum
'XPD' => true, // Palladium
'XSU' => true, // Sucre
'XDR' => true, // Special Drawing Rights
'XTS' => true, // Testing Currency Code
'XXX' => true, // Unknown Currency
];
/**
* Collects all available currency codes.
*
* @var string[]
*/
private array $currencyCodes = [];
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
return $scanner->scanLocales($sourceDir.'/curr');
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$compiler->compile($sourceDir.'/curr', $tempDir);
$compiler->compile($sourceDir.'/misc/currencyNumericCodes.txt', $tempDir);
}
protected function preGenerate()
{
$this->currencyCodes = [];
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
$localeBundle = $reader->read($tempDir, $displayLocale);
if (isset($localeBundle['Currencies']) && null !== $localeBundle['Currencies']) {
$data = [
'Names' => $this->generateSymbolNamePairs($localeBundle),
];
$this->currencyCodes = array_merge($this->currencyCodes, array_keys($data['Names']));
return $data;
}
return null;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$rootBundle = $reader->read($tempDir, 'root');
return [
'Names' => $this->generateSymbolNamePairs($rootBundle),
];
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$supplementalDataBundle = $reader->read($tempDir, 'supplementalData');
$numericCodesBundle = $reader->read($tempDir, 'currencyNumericCodes');
$this->currencyCodes = array_unique($this->currencyCodes);
sort($this->currencyCodes);
$data = [
'Currencies' => $this->currencyCodes,
'Meta' => $this->generateCurrencyMeta($supplementalDataBundle),
'Alpha3ToNumeric' => $this->generateAlpha3ToNumericMapping($numericCodesBundle, $this->currencyCodes),
];
$data['NumericToAlpha3'] = $this->generateNumericToAlpha3Mapping($data['Alpha3ToNumeric']);
return $data;
}
private function generateSymbolNamePairs(ArrayAccessibleResourceBundle $rootBundle): array
{
$symbolNamePairs = array_map(function ($pair) {
return \array_slice(iterator_to_array($pair), 0, 2);
}, iterator_to_array($rootBundle['Currencies']));
// Remove unwanted currencies
$symbolNamePairs = array_diff_key($symbolNamePairs, self::DENYLIST);
return $symbolNamePairs;
}
private function generateCurrencyMeta(ArrayAccessibleResourceBundle $supplementalDataBundle): array
{
// The metadata is already de-duplicated. It contains one key "DEFAULT"
// which is used for currencies that don't have dedicated entries.
return iterator_to_array($supplementalDataBundle['CurrencyMeta']);
}
private function generateAlpha3ToNumericMapping(ArrayAccessibleResourceBundle $numericCodesBundle, array $currencyCodes): array
{
$alpha3ToNumericMapping = iterator_to_array($numericCodesBundle['codeMap']);
asort($alpha3ToNumericMapping);
// Filter unknown currencies (e.g. "AYM")
$alpha3ToNumericMapping = array_intersect_key($alpha3ToNumericMapping, array_flip($currencyCodes));
return $alpha3ToNumericMapping;
}
private function generateNumericToAlpha3Mapping(array $alpha3ToNumericMapping): array
{
$numericToAlpha3Mapping = [];
foreach ($alpha3ToNumericMapping as $alpha3 => $numeric) {
// Make sure that the mapping is stored as table and not as array
$numeric = (string) $numeric;
if (!isset($numericToAlpha3Mapping[$numeric])) {
$numericToAlpha3Mapping[$numeric] = [];
}
$numericToAlpha3Mapping[$numeric][] = $alpha3;
}
return $numericToAlpha3Mapping;
}
}

View File

@@ -0,0 +1,58 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Locale;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*
* @internal
*/
trait FallbackTrait
{
private array $fallbackCache = [];
private bool $generatingFallback = false;
/**
* @see AbstractDataGenerator::generateDataForLocale()
*/
abstract protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array;
/**
* @see AbstractDataGenerator::generateDataForRoot()
*/
abstract protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array;
private function generateFallbackData(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): array
{
if (null === $fallback = Locale::getFallback($displayLocale)) {
return [];
}
if (isset($this->fallbackCache[$fallback])) {
return $this->fallbackCache[$fallback];
}
$prevGeneratingFallback = $this->generatingFallback;
$this->generatingFallback = true;
try {
$data = 'root' === $fallback ? $this->generateDataForRoot($reader, $tempDir) : $this->generateDataForLocale($reader, $tempDir, $fallback);
} finally {
$this->generatingFallback = $prevGeneratingFallback;
}
return $this->fallbackCache[$fallback] = $data ?: [];
}
}

View File

@@ -0,0 +1,73 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Writer\BundleWriterInterface;
/**
* Stores contextual information for resource bundle generation.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal
*/
class GeneratorConfig
{
private string $sourceDir;
private string $icuVersion;
/**
* @var BundleWriterInterface[]
*/
private array $bundleWriters = [];
public function __construct(string $sourceDir, string $icuVersion)
{
$this->sourceDir = $sourceDir;
$this->icuVersion = $icuVersion;
}
/**
* Adds a writer to be used during the data conversion.
*/
public function addBundleWriter(string $targetDir, BundleWriterInterface $writer)
{
$this->bundleWriters[$targetDir] = $writer;
}
/**
* Returns the writers indexed by their output directories.
*
* @return BundleWriterInterface[]
*/
public function getBundleWriters(): array
{
return $this->bundleWriters;
}
/**
* Returns the directory where the source versions of the resource bundles
* are stored.
*/
public function getSourceDir(): string
{
return $this->sourceDir;
}
/**
* Returns the ICU version of the bundles being converted.
*/
public function getIcuVersion(): string
{
return $this->icuVersion;
}
}

View File

@@ -0,0 +1,237 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
use Symfony\Component\Intl\Exception\RuntimeException;
/**
* The rule for compiling the language bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal
*/
class LanguageDataGenerator extends AbstractDataGenerator
{
/**
* Source: https://iso639-3.sil.org/code_tables/639/data.
*/
private const PREFERRED_ALPHA2_TO_ALPHA3_MAPPING = [
'ak' => 'aka',
'ar' => 'ara',
'ay' => 'aym',
'az' => 'aze',
'bo' => 'bod',
'cr' => 'cre',
'cs' => 'ces',
'cy' => 'cym',
'de' => 'deu',
'dz' => 'dzo',
'el' => 'ell',
'et' => 'est',
'eu' => 'eus',
'fa' => 'fas',
'ff' => 'ful',
'fr' => 'fra',
'gn' => 'grn',
'hy' => 'hye',
'hr' => 'hrv',
'ik' => 'ipk',
'is' => 'isl',
'iu' => 'iku',
'ka' => 'kat',
'kr' => 'kau',
'kg' => 'kon',
'kv' => 'kom',
'ku' => 'kur',
'lv' => 'lav',
'mg' => 'mlg',
'mi' => 'mri',
'mk' => 'mkd',
'mn' => 'mon',
'ms' => 'msa',
'my' => 'mya',
'nb' => 'nob',
'ne' => 'nep',
'nl' => 'nld',
'oj' => 'oji',
'om' => 'orm',
'or' => 'ori',
'ps' => 'pus',
'qu' => 'que',
'ro' => 'ron',
'sc' => 'srd',
'sk' => 'slk',
'sq' => 'sqi',
'sr' => 'srp',
'sw' => 'swa',
'uz' => 'uzb',
'yi' => 'yid',
'za' => 'zha',
'zh' => 'zho',
];
private const DENYLIST = [
'root' => true, // Absolute root language
'mul' => true, // Multiple languages
'mis' => true, // Uncoded language
'und' => true, // Unknown language
'zxx' => true, // No linguistic content
];
/**
* Collects all available language codes.
*
* @var string[]
*/
private array $languageCodes = [];
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
return $scanner->scanLocales($sourceDir.'/lang');
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$compiler->compile($sourceDir.'/lang', $tempDir);
$compiler->compile($sourceDir.'/misc/metadata.txt', $tempDir);
}
protected function preGenerate()
{
$this->languageCodes = [];
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
$localeBundle = $reader->read($tempDir, $displayLocale);
// isset() on \ResourceBundle returns true even if the value is null
if (isset($localeBundle['Languages']) && null !== $localeBundle['Languages']) {
$names = [];
$localizedNames = [];
foreach (self::generateLanguageNames($localeBundle) as $language => $name) {
if (!str_contains($language, '_')) {
$this->languageCodes[] = $language;
$names[$language] = $name;
} else {
$localizedNames[$language] = $name;
}
}
$data = [
'Names' => $names,
'LocalizedNames' => $localizedNames,
];
return $data;
}
return null;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
return null;
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$metadataBundle = $reader->read($tempDir, 'metadata');
$this->languageCodes = array_unique($this->languageCodes);
sort($this->languageCodes);
return [
'Languages' => $this->languageCodes,
'Alpha3Languages' => $this->generateAlpha3Codes($this->languageCodes, $metadataBundle),
'Alpha2ToAlpha3' => $this->generateAlpha2ToAlpha3Mapping($metadataBundle),
'Alpha3ToAlpha2' => $this->generateAlpha3ToAlpha2Mapping($metadataBundle),
];
}
private static function generateLanguageNames(ArrayAccessibleResourceBundle $localeBundle): array
{
return array_diff_key(iterator_to_array($localeBundle['Languages']), self::DENYLIST);
}
private function generateAlpha3Codes(array $languageCodes, ArrayAccessibleResourceBundle $metadataBundle): array
{
$alpha3Codes = array_flip(array_filter($languageCodes, static function (string $language): bool {
return 3 === \strlen($language);
}));
foreach ($metadataBundle['alias']['language'] as $alias => $data) {
if (3 === \strlen($alias) && 'overlong' === $data['reason']) {
$alpha3Codes[$alias] = true;
}
}
ksort($alpha3Codes);
return array_keys($alpha3Codes);
}
private function generateAlpha2ToAlpha3Mapping(ArrayAccessibleResourceBundle $metadataBundle): array
{
$aliases = iterator_to_array($metadataBundle['alias']['language']);
$alpha2ToAlpha3 = [];
foreach ($aliases as $alias => $data) {
$language = $data['replacement'];
if (2 === \strlen($language) && 3 === \strlen($alias) && 'overlong' === $data['reason']) {
if (isset(self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$language])) {
// Validate to prevent typos
if (!isset($aliases[self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$language]])) {
throw new RuntimeException('The statically set three-letter mapping '.self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$language].' for the language code '.$language.' seems to be invalid. Typo?');
}
$alpha3 = self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$language];
$alpha2 = $aliases[$alpha3]['replacement'];
if ($language !== $alpha2) {
throw new RuntimeException('The statically set three-letter mapping '.$alpha3.' for the language code '.$language.' seems to be an alias for '.$alpha2.'. Wrong mapping?');
}
$alpha2ToAlpha3[$language] = $alpha3;
} elseif (isset($alpha2ToAlpha3[$language])) {
throw new RuntimeException('Multiple three-letter mappings exist for the language code '.$language.'. Please add one of them to the const PREFERRED_ALPHA2_TO_ALPHA3_MAPPING.');
} else {
$alpha2ToAlpha3[$language] = $alias;
}
}
}
asort($alpha2ToAlpha3);
return $alpha2ToAlpha3;
}
private function generateAlpha3ToAlpha2Mapping(ArrayAccessibleResourceBundle $metadataBundle): array
{
$alpha3ToAlpha2 = [];
foreach ($metadataBundle['alias']['language'] as $alias => $data) {
$language = $data['replacement'];
if (2 === \strlen($language) && 3 === \strlen($alias) && 'overlong' === $data['reason']) {
$alpha3ToAlpha2[$alias] = $language;
}
}
asort($alpha3ToAlpha2);
return $alpha3ToAlpha2;
}
}

View File

@@ -0,0 +1,167 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
use Symfony\Component\Intl\Exception\MissingResourceException;
/**
* The rule for compiling the locale bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Roland Franssen <franssen.roland@gmail.com>
*
* @internal
*/
class LocaleDataGenerator extends AbstractDataGenerator
{
use FallbackTrait;
private array $locales = [];
private array $localeAliases = [];
private array $localeParents = [];
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
$this->locales = $scanner->scanLocales($sourceDir.'/locales');
$this->localeAliases = $scanner->scanAliases($sourceDir.'/locales');
$this->localeParents = $scanner->scanParents($sourceDir.'/locales');
return $this->locales;
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$filesystem = new Filesystem();
$filesystem->mkdir([
$tempDir.'/lang',
$tempDir.'/region',
]);
$compiler->compile($sourceDir.'/lang', $tempDir.'/lang');
$compiler->compile($sourceDir.'/region', $tempDir.'/region');
}
protected function preGenerate()
{
// Write parents locale file for the Translation component
file_put_contents(
__DIR__.'/../../../Translation/Resources/data/parents.json',
json_encode($this->localeParents, \JSON_PRETTY_PRINT).\PHP_EOL
);
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
// Don't generate aliases, as they are resolved during runtime
// Unless an alias is needed as fallback for de-duplication purposes
if (isset($this->localeAliases[$displayLocale]) && !$this->generatingFallback) {
return null;
}
// Generate locale names for all locales that have translations in
// at least the language or the region bundle
$displayFormat = $reader->readEntry($tempDir.'/lang', $displayLocale, ['localeDisplayPattern']);
$pattern = $displayFormat['pattern'] ?? '{0} ({1})';
$separator = $displayFormat['separator'] ?? '{0}, {1}';
$localeNames = [];
foreach ($this->locales as $locale) {
// Ensure a normalized list of pure locales
if (\Locale::getAllVariants($locale)) {
continue;
}
try {
// Generate a locale name in the language of each display locale
// Each locale name has the form: "Language (Script, Region, Variant1, ...)
// Script, Region and Variants are optional. If none of them is
// available, the braces are not printed.
$localeNames[$locale] = $this->generateLocaleName($reader, $tempDir, $locale, $displayLocale, $pattern, $separator);
} catch (MissingResourceException) {
// Silently ignore incomplete locale names
// In this case one should configure at least one fallback locale that is complete (e.g. English) during
// runtime. Alternatively a translation for the missing resource can be proposed upstream.
}
}
$data = [
'Names' => $localeNames,
];
// Don't de-duplicate a fallback locale
// Ensures the display locale can be de-duplicated on itself
if ($this->generatingFallback) {
return $data;
}
// Process again to de-duplicate locale and its fallback locales
// Only keep the differences
$fallbackData = $this->generateFallbackData($reader, $tempDir, $displayLocale);
if (isset($fallbackData['Names'])) {
$data['Names'] = array_diff($data['Names'], $fallbackData['Names']);
}
if (!$data['Names']) {
return null;
}
return $data;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
return null;
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
return [
'Locales' => $this->locales,
'Aliases' => $this->localeAliases,
];
}
private function generateLocaleName(BundleEntryReaderInterface $reader, string $tempDir, string $locale, string $displayLocale, string $pattern, string $separator): string
{
// Apply generic notation using square brackets as described per http://cldr.unicode.org/translation/language-names
$name = str_replace(['(', ')'], ['[', ']'], $reader->readEntry($tempDir.'/lang', $displayLocale, ['Languages', \Locale::getPrimaryLanguage($locale)]));
$extras = [];
// Discover the name of the script part of the locale
// i.e. in zh_Hans_MO, "Hans" is the script
if ($script = \Locale::getScript($locale)) {
$extras[] = str_replace(['(', ')'], ['[', ']'], $reader->readEntry($tempDir.'/lang', $displayLocale, ['Scripts', $script]));
}
// Discover the name of the region part of the locale
// i.e. in de_AT, "AT" is the region
if ($region = \Locale::getRegion($locale)) {
if (ctype_alpha($region) && !RegionDataGenerator::isValidCountryCode($region)) {
throw new MissingResourceException(sprintf('Skipping "%s" due an invalid country.', $locale));
}
$extras[] = str_replace(['(', ')'], ['[', ']'], $reader->readEntry($tempDir.'/region', $displayLocale, ['Countries', $region]));
}
if ($extras) {
$extra = array_shift($extras);
foreach ($extras as $part) {
$extra = str_replace(['{0}', '{1}'], [$extra, $part], $separator);
}
$name = str_replace(['{0}', '{1}'], [$name, $extra], $pattern);
}
return $name;
}
}

View File

@@ -0,0 +1,192 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
use Symfony\Component\Intl\Exception\RuntimeException;
/**
* The rule for compiling the region bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @see http://source.icu-project.org/repos/icu/icu4j/trunk/main/classes/core/src/com/ibm/icu/util/Region.java
*
* @internal
*/
class RegionDataGenerator extends AbstractDataGenerator
{
/**
* Source: https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes.
*/
private const PREFERRED_ALPHA2_TO_ALPHA3_MAPPING = [
'CD' => 'COD',
'DE' => 'DEU',
'FR' => 'FRA',
'MM' => 'MMR',
'TL' => 'TLS',
'YE' => 'YEM',
];
private const DENYLIST = [
// Exceptional reservations
'AC' => true, // Ascension Island
'CP' => true, // Clipperton Island
'DG' => true, // Diego Garcia
'EA' => true, // Ceuta & Melilla
'EU' => true, // European Union
'EZ' => true, // Eurozone
'IC' => true, // Canary Islands
'TA' => true, // Tristan da Cunha
'UN' => true, // United Nations
// User-assigned
'QO' => true, // Outlying Oceania
'XA' => true, // Pseudo-Accents
'XB' => true, // Pseudo-Bidi
'XK' => true, // Kosovo
// Misc
'ZZ' => true, // Unknown Region
];
/**
* Collects all available language codes.
*
* @var string[]
*/
private array $regionCodes = [];
public static function isValidCountryCode(int|string|null $region)
{
if (isset(self::DENYLIST[$region])) {
return false;
}
// WORLD/CONTINENT/SUBCONTINENT/GROUPING
if (\is_int($region) || ctype_digit($region)) {
return false;
}
return true;
}
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
return $scanner->scanLocales($sourceDir.'/region');
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$compiler->compile($sourceDir.'/region', $tempDir);
$compiler->compile($sourceDir.'/misc/metadata.txt', $tempDir);
}
protected function preGenerate()
{
$this->regionCodes = [];
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
$localeBundle = $reader->read($tempDir, $displayLocale);
// isset() on \ResourceBundle returns true even if the value is null
if (isset($localeBundle['Countries']) && null !== $localeBundle['Countries']) {
$data = [
'Names' => $this->generateRegionNames($localeBundle),
];
$this->regionCodes = array_merge($this->regionCodes, array_keys($data['Names']));
return $data;
}
return null;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
return null;
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$metadataBundle = $reader->read($tempDir, 'metadata');
$this->regionCodes = array_unique($this->regionCodes);
sort($this->regionCodes);
$alpha2ToAlpha3 = $this->generateAlpha2ToAlpha3Mapping(array_flip($this->regionCodes), $metadataBundle);
$alpha3ToAlpha2 = array_flip($alpha2ToAlpha3);
asort($alpha3ToAlpha2);
return [
'Regions' => $this->regionCodes,
'Alpha2ToAlpha3' => $alpha2ToAlpha3,
'Alpha3ToAlpha2' => $alpha3ToAlpha2,
];
}
protected function generateRegionNames(ArrayAccessibleResourceBundle $localeBundle): array
{
$unfilteredRegionNames = iterator_to_array($localeBundle['Countries']);
$regionNames = [];
foreach ($unfilteredRegionNames as $region => $regionName) {
if (!self::isValidCountryCode($region)) {
continue;
}
$regionNames[$region] = $regionName;
}
return $regionNames;
}
private function generateAlpha2ToAlpha3Mapping(array $countries, ArrayAccessibleResourceBundle $metadataBundle): array
{
$aliases = iterator_to_array($metadataBundle['alias']['territory']);
$alpha2ToAlpha3 = [];
foreach ($aliases as $alias => $data) {
$country = $data['replacement'];
if (2 === \strlen($country) && 3 === \strlen($alias) && 'overlong' === $data['reason']) {
if (isset(self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country])) {
// Validate to prevent typos
if (!isset($aliases[self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country]])) {
throw new RuntimeException('The statically set three-letter mapping '.self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country].' for the country code '.$country.' seems to be invalid. Typo?');
}
$alpha3 = self::PREFERRED_ALPHA2_TO_ALPHA3_MAPPING[$country];
$alpha2 = $aliases[$alpha3]['replacement'];
if ($country !== $alpha2) {
throw new RuntimeException('The statically set three-letter mapping '.$alpha3.' for the country code '.$country.' seems to be an alias for '.$alpha2.'. Wrong mapping?');
}
$alpha2ToAlpha3[$country] = $alpha3;
} elseif (isset($alpha2ToAlpha3[$country])) {
throw new RuntimeException('Multiple three-letter mappings exist for the country code '.$country.'. Please add one of them to the const PREFERRED_ALPHA2_TO_ALPHA3_MAPPING.');
} elseif (isset($countries[$country]) && self::isValidCountryCode($alias)) {
$alpha2ToAlpha3[$country] = $alias;
}
}
}
asort($alpha2ToAlpha3);
return $alpha2ToAlpha3;
}
}

View File

@@ -0,0 +1,86 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
/**
* The rule for compiling the script bundle.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal
*/
class ScriptDataGenerator extends AbstractDataGenerator
{
private const DENYLIST = [
'Zzzz' => true, // Unknown Script
];
/**
* Collects all available language codes.
*
* @var string[]
*/
private array $scriptCodes = [];
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
return $scanner->scanLocales($sourceDir.'/lang');
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$compiler->compile($sourceDir.'/lang', $tempDir);
}
protected function preGenerate()
{
$this->scriptCodes = [];
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
$localeBundle = $reader->read($tempDir, $displayLocale);
// isset() on \ResourceBundle returns true even if the value is null
if (isset($localeBundle['Scripts']) && null !== $localeBundle['Scripts']) {
$data = [
'Names' => array_diff_key(iterator_to_array($localeBundle['Scripts']), self::DENYLIST),
];
$this->scriptCodes = array_merge($this->scriptCodes, array_keys($data['Names']));
return $data;
}
return null;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
return null;
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$this->scriptCodes = array_unique($this->scriptCodes);
sort($this->scriptCodes);
return [
'Scripts' => $this->scriptCodes,
];
}
}

View File

@@ -0,0 +1,274 @@
<?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\Intl\Data\Generator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Intl\Data\Bundle\Compiler\BundleCompilerInterface;
use Symfony\Component\Intl\Data\Bundle\Reader\BundleEntryReaderInterface;
use Symfony\Component\Intl\Data\Util\ArrayAccessibleResourceBundle;
use Symfony\Component\Intl\Data\Util\LocaleScanner;
use Symfony\Component\Intl\Exception\MissingResourceException;
use Symfony\Component\Intl\Locale;
/**
* The rule for compiling the zone bundle.
*
* @author Roland Franssen <franssen.roland@gmail.com>
*
* @internal
*/
class TimezoneDataGenerator extends AbstractDataGenerator
{
use FallbackTrait;
/**
* Collects all available zone IDs.
*
* @var string[]
*/
private array $zoneIds = [];
private array $zoneToCountryMapping = [];
private array $localeAliases = [];
protected function scanLocales(LocaleScanner $scanner, string $sourceDir): array
{
$this->localeAliases = $scanner->scanAliases($sourceDir.'/locales');
return $scanner->scanLocales($sourceDir.'/zone');
}
protected function compileTemporaryBundles(BundleCompilerInterface $compiler, string $sourceDir, string $tempDir)
{
$filesystem = new Filesystem();
$filesystem->mkdir($tempDir.'/region');
$compiler->compile($sourceDir.'/region', $tempDir.'/region');
$compiler->compile($sourceDir.'/zone', $tempDir);
$compiler->compile($sourceDir.'/misc/timezoneTypes.txt', $tempDir);
$compiler->compile($sourceDir.'/misc/metaZones.txt', $tempDir);
$compiler->compile($sourceDir.'/misc/windowsZones.txt', $tempDir);
}
protected function preGenerate()
{
$this->zoneIds = [];
$this->zoneToCountryMapping = [];
}
protected function generateDataForLocale(BundleEntryReaderInterface $reader, string $tempDir, string $displayLocale): ?array
{
if (!$this->zoneToCountryMapping) {
$this->zoneToCountryMapping = self::generateZoneToCountryMapping($reader->read($tempDir, 'windowsZones'));
}
// Don't generate aliases, as they are resolved during runtime
// Unless an alias is needed as fallback for de-duplication purposes
if (isset($this->localeAliases[$displayLocale]) && !$this->generatingFallback) {
return null;
}
$localeBundle = $reader->read($tempDir, $displayLocale);
if (!isset($localeBundle['zoneStrings']) || null === $localeBundle['zoneStrings']) {
return null;
}
$data = [
'Names' => $this->generateZones($reader, $tempDir, $displayLocale),
'Meta' => self::generateZoneMetadata($localeBundle),
];
// Don't de-duplicate a fallback locale
// Ensures the display locale can be de-duplicated on itself
if ($this->generatingFallback) {
return $data;
}
// Process again to de-duplicate locales and their fallback locales
// Only keep the differences
$fallback = $this->generateFallbackData($reader, $tempDir, $displayLocale);
if (isset($fallback['Names'])) {
$data['Names'] = array_diff($data['Names'], $fallback['Names']);
}
if (isset($fallback['Meta'])) {
$data['Meta'] = array_diff($data['Meta'], $fallback['Meta']);
}
if (!$data['Names'] && !$data['Meta']) {
return null;
}
$this->zoneIds = array_merge($this->zoneIds, array_keys($data['Names']));
return $data;
}
protected function generateDataForRoot(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$rootBundle = $reader->read($tempDir, 'root');
return [
'Meta' => self::generateZoneMetadata($rootBundle),
];
}
protected function generateDataForMeta(BundleEntryReaderInterface $reader, string $tempDir): ?array
{
$rootBundle = $reader->read($tempDir, 'root');
$this->zoneIds = array_unique($this->zoneIds);
sort($this->zoneIds);
ksort($this->zoneToCountryMapping);
$data = [
'Zones' => $this->zoneIds,
'ZoneToCountry' => $this->zoneToCountryMapping,
'CountryToZone' => self::generateCountryToZoneMapping($this->zoneToCountryMapping),
];
return $data;
}
private function generateZones(BundleEntryReaderInterface $reader, string $tempDir, string $locale): array
{
$typeBundle = $reader->read($tempDir, 'timezoneTypes');
$available = [];
foreach ($typeBundle['typeMap']['timezone'] as $zone => $_) {
if ('Etc:Unknown' === $zone || preg_match('~^Etc:GMT[-+]\d+$~', $zone)) {
continue;
}
$available[$zone] = true;
}
$metaBundle = $reader->read($tempDir, 'metaZones');
$metazones = [];
foreach ($metaBundle['metazoneInfo'] as $zone => $info) {
foreach ($info as $metazone) {
$metazones[$zone] = $metazone->get(0);
}
}
$regionFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'regionFormat']);
$fallbackFormat = $reader->readEntry($tempDir, $locale, ['zoneStrings', 'fallbackFormat']);
$resolveName = function (string $id, string $city = null) use ($reader, $tempDir, $locale, $regionFormat, $fallbackFormat): ?string {
// Resolve default name as described per http://cldr.unicode.org/translation/timezones
if (isset($this->zoneToCountryMapping[$id])) {
try {
$country = $reader->readEntry($tempDir.'/region', $locale, ['Countries', $this->zoneToCountryMapping[$id]]);
} catch (MissingResourceException) {
return null;
}
$name = str_replace('{0}', $country, $regionFormat);
return null === $city ? $name : str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat);
}
if (null !== $city) {
return str_replace('{0}', $city, $regionFormat);
}
return null;
};
$accessor = static function (array $indices, array ...$fallbackIndices) use ($locale, $reader, $tempDir) {
foreach (\func_get_args() as $indices) {
try {
return $reader->readEntry($tempDir, $locale, $indices);
} catch (MissingResourceException) {
}
}
return null;
};
$zones = [];
foreach (array_keys($available) as $zone) {
// lg: long generic, e.g. "Central European Time"
// ls: long specific (not DST), e.g. "Central European Standard Time"
// ld: long DST, e.g. "Central European Summer Time"
// ec: example city, e.g. "Amsterdam"
$name = $accessor(['zoneStrings', $zone, 'lg'], ['zoneStrings', $zone, 'ls']);
$city = $accessor(['zoneStrings', $zone, 'ec']);
$id = str_replace(':', '/', $zone);
if (null === $name && isset($metazones[$zone])) {
$meta = 'meta:'.$metazones[$zone];
$name = $accessor(['zoneStrings', $meta, 'lg'], ['zoneStrings', $meta, 'ls']);
}
// Infer a default English named city for all locales
// Ensures each timezone ID has a distinctive name
if (null === $city && 0 !== strrpos($zone, 'Etc:') && false !== $i = strrpos($zone, ':')) {
$city = str_replace('_', ' ', substr($zone, $i + 1));
}
if (null === $name) {
$name = $resolveName($id, $city);
$city = null;
}
if (null === $name) {
continue;
}
// Ensure no duplicated content is generated
if (null !== $city && false === mb_stripos(str_replace('-', ' ', $name), str_replace('-', ' ', $city))) {
$name = str_replace(['{0}', '{1}'], [$city, $name], $fallbackFormat);
}
$zones[$id] = $name;
}
return $zones;
}
private static function generateZoneMetadata(ArrayAccessibleResourceBundle $localeBundle): array
{
$metadata = [];
if (isset($localeBundle['zoneStrings']['gmtFormat'])) {
$metadata['GmtFormat'] = str_replace('{0}', '%s', $localeBundle['zoneStrings']['gmtFormat']);
}
if (isset($localeBundle['zoneStrings']['hourFormat'])) {
$hourFormat = explode(';', str_replace(['HH', 'mm', 'H', 'm'], ['%02d', '%02d', '%d', '%d'], $localeBundle['zoneStrings']['hourFormat']), 2);
$metadata['HourFormatPos'] = $hourFormat[0];
$metadata['HourFormatNeg'] = $hourFormat[1];
}
return $metadata;
}
private static function generateZoneToCountryMapping(ArrayAccessibleResourceBundle $windowsZoneBundle): array
{
$mapping = [];
foreach ($windowsZoneBundle['mapTimezones'] as $zoneInfo) {
foreach ($zoneInfo as $region => $zones) {
if (RegionDataGenerator::isValidCountryCode($region)) {
$mapping += array_fill_keys(explode(' ', $zones), $region);
}
}
}
ksort($mapping);
return $mapping;
}
private static function generateCountryToZoneMapping(array $zoneToCountryMapping): array
{
$mapping = [];
foreach ($zoneToCountryMapping as $zone => $country) {
$mapping[$country][] = $zone;
}
ksort($mapping);
return $mapping;
}
}