diff --git a/UPGRADE-2.4.md b/UPGRADE-2.4.md index bd9ecc3c4dae4..18395c3fd56a7 100644 --- a/UPGRADE-2.4.md +++ b/UPGRADE-2.4.md @@ -7,3 +7,128 @@ Form * The constructor parameter `$precision` in `IntegerToLocalizedStringTransformer` is now ignored completely, because a precision does not make sense for integers. + +Intl +---- + + * A new method `getLocaleAliases()` was added to `LocaleBundleInterface`. If + any of your classes implements this interface, you should add an implementation + of this method. + + * The methods in the various resource bundles of the `Intl` class used to + return `null` when invalid arguments were given. These methods throw a + `MissingResourceException` now. + + Before: + + ``` + use Symfony\Component\Intl\Intl; + + // invalid language code + $language = Intl::getLanguageBundle()->getLanguageName('foo', null, 'en'); + + // invalid locale + $language = Intl::getLanguageBundle()->getLanguageName('de', null, 'foo'); + + if (null === $language) { + // error handling... + } + ``` + + After: + + ``` + use Symfony\Component\Intl\Intl; + use Symfony\Component\Intl\Exception\MissingResourceException; + use Symfony\Component\Intl\Exception\NoSuchLocaleException; + + try { + // invalid language code + $language = Intl::getLanguageBundle()->getLanguageName('foo', null, 'en'); + + // invalid locale + $language = Intl::getLanguageBundle()->getLanguageName('de', null, 'foo'); + } catch (MissingResourceException $e) { + if ($e->getPrevious() instanceof NoSuchLocaleException) { + // locale was invalid... + } else { + // locale was valid, but entry not found... + } + } + ``` + + * The `$fallback` argument of the protected method `AbstractBundle::readEntry()` + was changed to be `true` by default. This way the signature is consistent + with the proxied `BundleEntryReaderInterface::readEntry()` method. + Consequently, if an entry cannot be found for the accessed locale (e.g. "en_GB"), + it is looked for in the fallback locale (if any, e.g. "en"). + + If you extend this class and explicitly want to disable locale fallback, you + should pass `false` as last argument. + + Before: + + ``` + use Symfony\Component\Intl\ResourceBundle\AbstractBundle; + + class MyBundle extends AbstractBundle + { + public function getEntry($key, $locale) + { + return $this->readEntry($locale, array('Entries', $key)); + } + } + ``` + + After: + + ``` + use Symfony\Component\Intl\ResourceBundle\AbstractBundle; + + class MyBundle extends AbstractBundle + { + public function getEntry($key, $locale) + { + // disable locale fallback! + return $this->readEntry($locale, array('Entries', $key), false); + } + } + ``` + + * The interfaces `CompilationContextInterface` and `StubbingContextInterface` + were removed. Code against their implementations `CompilationContext` and + `StubbingContext` in the same namespace instead. + + Before: + + ``` + use Symfony\Component\Intl\ResourceBundle\Transformation\CompilationContextInterface; + use Symfony\Component\Intl\ResourceBundle\Transformation\StubbingContextInterface; + + public function beforeCompile(CompilationContextInterface $context) + { + // ... + } + + public function beforeCreateStub(StubbingContextInterface $context) + { + // ... + } + ``` + + After: + + ``` + use Symfony\Component\Intl\ResourceBundle\Transformation\CompilationContext; + use Symfony\Component\Intl\ResourceBundle\Transformation\StubbingContext; + + public function beforeCompile(CompilationContext $context) + { + // ... + } + + public function beforeCreateStub(StubbingContext $context) + { + // ... + } + ``` diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index be37fabc2c7c3..774b6fda8a1d8 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -257,6 +257,70 @@ UPGRADE FROM 2.x to 3.0 * The `Symfony\Component\HttpKernel\EventListener\ExceptionListener` now passes the Request format as the `_format` argument instead of `format`. +### Intl + + * The class `BundleCompiler` was renamed to `GenrbBundleCompiler`. + + Before: + + ``` + use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler; + + $genrb = '/path/to/icu/build/bin/genrb'; + $genrbEnv = 'LD_LIBRARY_PATH=/path/to/icu/build/lib' + $compiler = new BundleCompiler($genrb, $genrbEnv); + ``` + + After: + + ``` + use Symfony\Component\Intl\ResourceBundle\Compiler\GenrbBundleCompiler; + + $genrb = '/path/to/icu/build/bin/genrb'; + $genrbEnv = 'LD_LIBRARY_PATH=/path/to/icu/build/lib' + $compiler = new GenrbBundleCompiler($genrb, $genrbEnv); + ``` + + * The class `StructuredBundleReader` was renamed to `BundleEntryReader`. The + corresponding interface `StructuredBundleReaderInterface` was renamed to + `BundleEntryReaderInterface`. + + Before: + + ``` + class MyEntryReader extends StructuredBundleReader + { + // ... + } + ``` + + After: + + ``` + class MyEntryReader extends BundleEntryReader + { + // ... + } + ``` + + Before: + + ``` + public function __construct(StructuredBundleReaderInterface $entryReader) + { + // ... + } + ``` + + After: + + ``` + public function __construct(BundleEntryReaderInterface $entryReader) + { + // ... + } + ``` + ### Locale * The Locale component was removed and replaced by the Intl component. diff --git a/src/Symfony/Component/Intl/CHANGELOG.md b/src/Symfony/Component/Intl/CHANGELOG.md new file mode 100644 index 0000000000000..d91887ebf2365 --- /dev/null +++ b/src/Symfony/Component/Intl/CHANGELOG.md @@ -0,0 +1,26 @@ +CHANGELOG +========= + +2.4.0 +----- + + * [BC BREAK] the various Intl methods now throw a `MissingResourceException` + whenever a non-existing locale, language, currency, etc. is accessed + * the available locales of each resource bundle are now stored in a generic + "misc.res" file in order to improve reading performance + * improved `LocaleBundleTransformationRule` to not generate duplicate locale + names when fallback (e.g. "en_GB"->"en") is possible anyway. This reduced + the Resources/ directory file size of the Icu 1.2.x branch from 14M to 12M at + the time of this writing + * [BC BREAK] a new method `getLocaleAliases()` was added to `LocaleBundleInterface` + * deprecated `StructuredBundleReader` and `StructuredBundleReaderInterface` in + favor of `BundleEntryReader` and `BundleEntryReaderInterface` + * `BundleEntryReader` now follows aliases when looking for fallback locales + * [BC BREAK] changed default value of the argument `$fallback` in the protected + method `AbstractBundle::readEntry()` to `true` in order to be consistent with + the proxied `BundleEntryReaderInterface::readEntry()` method + * deprecated `BundleCompiler` in favor of `GenrbBundleCompiler` + * fixed `TextBundleWriter` to correctly escape keys that contain colons (":") + * fixed `TextBundleWriter` to correctly escape keys that contain spaces (" ") + * [BC BREAK] removed the pointless interfaces `CompilationContextInterface` + and `StubbingContextInterface`. You can depend on their implementation instead diff --git a/src/Symfony/Component/Intl/Currency.php b/src/Symfony/Component/Intl/Currency.php new file mode 100644 index 0000000000000..9230389f147e1 --- /dev/null +++ b/src/Symfony/Component/Intl/Currency.php @@ -0,0 +1,334 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +use Symfony\Component\Icu\CurrencyDataProvider; +use Symfony\Component\Intl\Exception\InvalidArgumentException; +use Symfony\Component\Intl\Exception\MissingResourceException; + +/** + * Provides access to currency-related data. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @api + */ +class Currency +{ + /** + * @var CurrencyDataProvider + */ + private static $dataProvider; + + /** + * @var string[]|null + */ + private static $currencies; + + /** + * Returns all available currencies. + * + * @return string[] An array of ISO 4217 three-letter currency codes + * + * @api + */ + public static function getCurrencies() + { + if (null === self::$currencies) { + self::$currencies = self::getDataProvider()->getCurrencies(); + } + + return self::$currencies; + } + + /** + * Returns whether the given ISO 4217 currency code exists. + * + * This method does not canonicalize the given currency code. Specifically, + * it will return false if the currency is not correctly cased or is + * provided as numeric code instead of as three-letter code. For + * example, this method returns false for "cad" and 124 (the numeric ISO + * 4217 code of the Canadian Dollar), but true for "CAD". + * + * If you want to support the lowercase currencies, you should manually + * canonicalize the currency code prior to calling this method. If you + * want to support numeric codes, you should convert them into three-letter + * codes by calling {@link forNumericCode()}. + * + * @param string $currency A canonical ISO 4217 three-letter currency code + * (e.g. "EUR") + * + * @return Boolean Whether the currency code exists + * + * @see canonicalize + * @see forNumericCode + * + * @api + */ + public static function exists($currency) + { + if (null === self::$lookupTable) { + self::$lookupTable = array_flip(static::getCurrencies()); + } + + return isset(self::$lookupTable[$currency]); + } + + /** + * Canonicalizes the given ISO 4217 currency code. + * + * The currency code is converted to uppercase during canonicalization. This + * method does not check whether the given currency actually exists. In case + * of doubt, you should pass the canonicalized currency to {@link exists()}. + * + * @param string $currency An ISO 4217 three-letter currency code (e.g. "EUR") + * + * @return string The canonicalized currency code + * + * @see exists + * + * @api + */ + public static function canonicalize($currency) + { + return strtoupper($currency); + } + + /** + * Returns the symbol of a currency in the given locale. + * + * For example, the symbol of the US Dollar ("USD") in the locale "en_US" is + * "$". If the resource data for the given locale contains no entry for the + * given currency, then the ISO 4217 three-letter currency code is returned. + * + * If null is passed as locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $currency A canonical ISO 4217 three-letter currency + * code (e.g. "EUR") + * @param string $displayLocale The ICU locale code to return the symbol in + * + * @return string The currency symbol for the specified locale + * + * @throws InvalidArgumentException If the currency or the locale is invalid + * + * @api + */ + public static function getSymbol($currency, $displayLocale = null) + { + if (!static::exists($currency)) { + throw new InvalidArgumentException('The currency "' . $currency . '" does not exist.'); + } + + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getSymbol($currency, $displayLocale); + } + + /** + * Returns the name of a currency in the given locale. + * + * For example, the name of the Euro ("EUR") in the locale "ru_RU" is + * "Евро". If the resource data for the given locale contains no entry for + * the given currency, then the ISO 4217 three-letter currency code is + * returned. + * + * If null is passed as locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $currency A canonical ISO 4217 three-letter currency + * code (e.g. "EUR") + * @param string $displayLocale The ICU locale code to return the name in + * + * @return string The name of the currency + * + * @throws InvalidArgumentException If the currency or the locale is invalid + * + * @api + */ + public static function getName($currency, $displayLocale = null) + { + if (!static::exists($currency)) { + throw new InvalidArgumentException('The currency "' . $currency . '" does not exist.'); + } + + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getName($currency, $displayLocale); + } + + /** + * Returns the names of all known currencies in the specified locale. + * + * If the resource data for the given locale contains no entry for a + * currency, then the ISO 4217 three-letter currency code is used instead. + * + * If null is passed as locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $displayLocale The ICU locale code to return the names in + * + * @return string[] An array of currency names indexed by ISO 4217 + * three-letter currency codes + * + * @throws InvalidArgumentException If the locale is invalid + * + * @api + */ + public static function getNames($displayLocale = null) + { + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getNames($displayLocale); + } + + /** + * Returns the default number of fraction digits used with a currency. + * + * For example, the default number of fraction digits for the Euro is 2, + * while for the Japanese Yen it's 0. + * + * @param string $currency A canonical ISO 4217 three-letter currency code + * (e.g. "EUR") + * + * @return integer The number of digits after the comma + * + * @throws InvalidArgumentException If the currency is invalid + * + * @api + */ + public static function getFractionDigits($currency) + { + if (!static::exists($currency)) { + throw new InvalidArgumentException('The currency "' . $currency . '" does not exist.'); + } + + return self::getDataProvider()->getFractionDigits($currency); + } + + /** + * Returns the rounding increment of a currency. + * + * The rounding increment indicates to which number a currency is rounded. + * For example, 1230 rounded to the nearest 50 is 1250. 1.234 rounded to the + * nearest 0.65 is 1.3. + * + * @param string $currency A canonical ISO 4217 three-letter currency code + * (e.g. "EUR") + * + * @return integer The rounding increment + * + * @throws InvalidArgumentException If the currency is invalid + * + * @api + */ + public static function getRoundingIncrement($currency) + { + if (!static::exists($currency)) { + throw new InvalidArgumentException('The currency "' . $currency . '" does not exist.'); + } + + return self::getDataProvider()->getRoundingIncrement($currency); + } + + /** + * Returns the ISO 4217 numeric code of a currency. + * + * For example, the numeric code of the Canadian Dollar ("CAD") is 124. If + * no numeric code is available for a currency, 0 is returned. + * + * @param string $currency A canonical ISO 4217 three-letter currency code + * (e.g. "EUR") + * + * @return integer The numeric code + * + * @throws InvalidArgumentException If the currency is invalid + * + * @api + */ + public static function getNumericCode($currency) + { + if (!static::exists($currency)) { + throw new InvalidArgumentException('The currency "'.$currency.'" does not exist.'); + } + + return self::getDataProvider()->getNumericCode($currency); + } + + /** + * Returns the matching ISO 4217 three-letter codes for a numeric code. + * + * For example, the numeric code 124 belongs to the Canadian Dollar ("CAD"). + * Some numeric codes belong to multiple currencies. For example, the + * number 428 is assigned to both the Latvian Ruble ("LVR") and the Latvian + * Lats ("LVL"). For this reason, this method always returns an array. + * + * @param integer $numericCode An ISO 4217 numeric currency code (e.g. 124) + * + * @return string[] The matching ISO 4217 three-letter currency codes + * + * @throws InvalidArgumentException If the numeric code does not exist + * + * @api + */ + public static function forNumericCode($numericCode) + { + try { + return self::getDataProvider()->forNumericCode($numericCode); + } catch (MissingResourceException $e) { + throw new InvalidArgumentException( + 'The numeric currency code "'.$numericCode.'" does not exist.', + 0, + $e + ); + } + } + + /** + * @return CurrencyDataProvider + */ + private static function getDataProvider() + { + if (null === self::$dataProvider) { + self::$dataProvider = new CurrencyDataProvider( + CurrencyDataProvider::getResourceDirectory(), + Intl::getEntryReader() + ); + } + + return self::$dataProvider; + } + + /** + * This class must not be instantiated. + */ + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/Exception/MissingResourceException.php b/src/Symfony/Component/Intl/Exception/MissingResourceException.php new file mode 100644 index 0000000000000..e2eb3f210e751 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/MissingResourceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Thrown when an invalid entry of a resource bundle was requested. + * + * @author Bernhard Schussek + */ +class MissingResourceException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php b/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php new file mode 100644 index 0000000000000..59da5ec0d53d5 --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/ResourceBundleNotFoundException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * @author Bernhard Schussek + */ +class ResourceBundleNotFoundException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php b/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php new file mode 100644 index 0000000000000..1076ca904893b --- /dev/null +++ b/src/Symfony/Component/Intl/Exception/UnexpectedTypeException.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Exception; + +/** + * Thrown when a method argument had an unexpected type. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class UnexpectedTypeException extends InvalidArgumentException +{ + public function __construct($value, $expectedType) + { + parent::__construct(sprintf('Expected argument of type "%s", "%s" given', $expectedType, is_object($value) ? get_class($value) : gettype($value))); + } +} diff --git a/src/Symfony/Component/Intl/Intl.php b/src/Symfony/Component/Intl/Intl.php index d0373988d2fd4..259836f8f69db 100644 --- a/src/Symfony/Component/Intl/Intl.php +++ b/src/Symfony/Component/Intl/Intl.php @@ -17,7 +17,7 @@ use Symfony\Component\Icu\IcuLocaleBundle; use Symfony\Component\Icu\IcuRegionBundle; use Symfony\Component\Intl\ResourceBundle\Reader\BufferedBundleReader; -use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; +use Symfony\Component\Intl\ResourceBundle\Reader\BundleEntryReader; /** * Gives access to internationalization data. @@ -63,9 +63,9 @@ class Intl private static $icuDataVersion = false; /** - * @var ResourceBundle\Reader\StructuredBundleReaderInterface + * @var BundleEntryReader */ - private static $bundleReader; + private static $entryReader; /** * Returns whether the intl extension is installed. @@ -81,11 +81,14 @@ public static function isExtensionLoaded() * Returns the bundle containing currency information. * * @return ResourceBundle\CurrencyBundleInterface The currency resource bundle. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Currency} class instead. */ public static function getCurrencyBundle() { if (null === self::$currencyBundle) { - self::$currencyBundle = new IcuCurrencyBundle(self::getBundleReader()); + self::$currencyBundle = new IcuCurrencyBundle(self::getEntryReader()); } return self::$currencyBundle; @@ -99,7 +102,7 @@ public static function getCurrencyBundle() public static function getLanguageBundle() { if (null === self::$languageBundle) { - self::$languageBundle = new IcuLanguageBundle(self::getBundleReader()); + self::$languageBundle = new IcuLanguageBundle(self::getEntryReader()); } return self::$languageBundle; @@ -109,11 +112,14 @@ public static function getLanguageBundle() * Returns the bundle containing locale information. * * @return ResourceBundle\LocaleBundleInterface The locale resource bundle. + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use the {@link Locale} class instead. */ public static function getLocaleBundle() { if (null === self::$localeBundle) { - self::$localeBundle = new IcuLocaleBundle(self::getBundleReader()); + self::$localeBundle = new IcuLocaleBundle(self::getEntryReader()); } return self::$localeBundle; @@ -127,7 +133,7 @@ public static function getLocaleBundle() public static function getRegionBundle() { if (null === self::$regionBundle) { - self::$regionBundle = new IcuRegionBundle(self::getBundleReader()); + self::$regionBundle = new IcuRegionBundle(self::getEntryReader()); } return self::$regionBundle; @@ -190,18 +196,22 @@ public static function getIcuStubVersion() /** * Returns a resource bundle reader for .php resource bundle files. * - * @return ResourceBundle\Reader\StructuredBundleReaderInterface The resource reader. + * @return ResourceBundle\Reader\BundleEntryReaderInterface The resource reader. */ - private static function getBundleReader() + public static function getEntryReader() { - if (null === self::$bundleReader) { - self::$bundleReader = new StructuredBundleReader(new BufferedBundleReader( + if (null === self::$entryReader) { + self::$entryReader = new BundleEntryReader(new BufferedBundleReader( IcuData::getBundleReader(), self::BUFFER_SIZE )); + + // Make sure that self::$bundleReader is already set to prevent + // a cycle + self::$entryReader->setLocaleAliases(Locale::getAliases()); } - return self::$bundleReader; + return self::$entryReader; } /** diff --git a/src/Symfony/Component/Intl/Language.php b/src/Symfony/Component/Intl/Language.php new file mode 100644 index 0000000000000..f0caba4354d7c --- /dev/null +++ b/src/Symfony/Component/Intl/Language.php @@ -0,0 +1,270 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +use Symfony\Component\Icu\LanguageDataProvider; +use Symfony\Component\Intl\Exception\InvalidArgumentException; + +/** + * Provides access to language-related data. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @api + */ +class Language +{ + /** + * @var LanguageDataProvider + */ + private static $dataProvider; + + /** + * @var string[]|null + */ + private static $languages; + + /** + * @var integer[]|null + */ + private static $lookupTable; + + /** + * @var string[]|null + */ + private static $aliases; + + /** + * Returns all available languages. + * + * Languages are returned as lowercase ISO 639-1 two-letter language codes. + * For languages that don't have a two-letter code, the ISO 639-2 + * three-letter code is used instead. + * + * A full table of ISO 639 language codes can be found here: + * http://www-01.sil.org/iso639-3/codes.asp + * + * @return string[] An array of canonical ISO 639 language codes + * + * @api + */ + public static function getLanguages() + { + if (null === self::$languages) { + self::$languages = self::getDataProvider()->getLanguages(); + } + + return self::$languages; + } + + /** + * Returns whether the given ISO 639 language code exists. + * + * This method does not canonicalize the given language code. Specifically, + * it will return false if the language code is not correctly cased or uses + * hyphens ("-") as separators between the subtags instead of underscores + * ("_"). For example, this method returns false for "en-GB", but true + * for "en_GB". + * + * This method also returns false if an ISO 639-2 three-letter code is + * provided where an equivalent ISO 639-1 two-letter code exists. + * + * If you want to support the above cases, you should manually canonicalize + * the language code prior to calling this method. + * + * @param string $language A canonical ISO 639 language code (e.g. "en") + * + * @return Boolean Whether the language code exists + * + * @see canonicalize + * + * @api + */ + public static function exists($language) + { + if (null === self::$lookupTable) { + self::$lookupTable = array_flip(static::getLanguages()); + } + + return isset(self::$lookupTable[$language]); + } + + /** + * Canonicalizes the given ISO 639 language code. + * + * Canonicalization performs the following steps: + * + * 1. Hyphens ("-") are replaced by underscores ("_") + * 2. The first subtag is interpreted as language code. The language + * code is lowercased. If a corresponding ISO 639-1 two-letter code + * exists for a given ISO 639-2 three-letter code, the two-letter code + * is used instead. For example, "DEU" is converted to "de". + * 3. The second subtag is interpreted as region code. The region code + * is uppercased. If the region code is an alias, it is replaced by + * the aliased region code. For example "aut" is converted to "AT". + * + * Canonicalization does not check whether a given language or region + * actually exists. In case of doubt, you should pass the canonicalized + * language to {@link exists()}. + * + * @param string $language A language code (e.g. "en") + * + * @return string The canonicalized ISO 639 language codde + * + * @see exists + * + * @api + */ + public static function canonicalize($language) + { + if (static::exists($language)) { + return $language; + } + + if (null === self::$aliases) { + self::$aliases = self::getDataProvider()->getAliases(); + } + + $parts = preg_split('/[-_]/', $language); + + // The language code is always in lower case + $parts[0] = strtolower($parts[0]); + + if (isset(self::$aliases[$parts[0]])) { + $parts[0] = self::$aliases[$parts[0]]; + } + + if (isset($parts[1])) { + // TODO: Uncomment once Region::canonicalize() is implemented + //$parts[1] = Region::canonicalize($parts[1]); + } + + // TODO: change index to 2 once Region::canonicalize() is implemented + for ($i = 1; $i < count($parts); ++$i) { + $parts[$i] = strtoupper($parts[$i]); + } + + return implode('_', $parts); + } + + /** + * Returns the name for a language in the given locale. + * + * For example, the name of British English ("en_GB") in the locale "ru_RU" + * is "британский английский". If the resource data for the given locale + * contains no entry for the given language, then the ISO 639 language code + * is returned. + * + * If null is passed as locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $language A canonical ISO 639 language code + * (e.g. "en") + * @param string $displayLocale The ICU locale code to return the name in + * + * @return string The name of the language + * + * @throws InvalidArgumentException If the language or the locale is invalid + * + * @api + */ + public static function getName($language, $displayLocale = null) + { + if (!static::exists($language)) { + throw new InvalidArgumentException('The language "' . $language . '" does not exist.'); + } + + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getName($language, $displayLocale); + } + + /** + * Returns the names of all known languages in the specified locale. + * + * If the resource data for the given locale contains no entry for a + * language, then the canonical ISO 639 language code is used instead. + * + * If null is passed as locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $displayLocale The ICU locale code to return the names in + * + * @return string[] An array of language names indexed by their + * canonical ISO 639 language codes + * + * @throws InvalidArgumentException If the locale is invalid + * + * @api + */ + public static function getNames($displayLocale = null) + { + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getNames($displayLocale); + } + + /** + * Returns the ISO 639-2 three-letter code of a language. + * + * @param string $language A canonical ISO 639 language code (e.g. "en") + * + * @return string The ISO 639-2 three-letter code of the language + * + * @throws Exception\InvalidArgumentException If the language is invalid + * @throws Exception\MissingResourceException If the language has no + * corresponding three-letter code + * + * @api + */ + public static function getAlpha3Code($language) + { + if (!static::exists($language)) { + throw new InvalidArgumentException('The language "' . $language . '" does not exist.'); + } + + return self::getDataProvider()->getAlpha3Code($language); + } + + /** + * @return LanguageDataProvider + */ + private static function getDataProvider() + { + if (null === self::$dataProvider) { + self::$dataProvider = new LanguageDataProvider( + LanguageDataProvider::getResourceDirectory(), + Intl::getEntryReader() + ); + } + + return self::$dataProvider; + } + + /** + * This class must not be instantiated. + */ + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/Locale.php b/src/Symfony/Component/Intl/Locale.php new file mode 100644 index 0000000000000..19301740966bb --- /dev/null +++ b/src/Symfony/Component/Intl/Locale.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl; + +use Symfony\Component\Icu\LocaleDataProvider; +use Symfony\Component\Intl\Exception\InvalidArgumentException; + +/** + * Provides access to locale-related data. + * + * @since 2.5 + * @author Bernhard Schussek + * + * @api + */ +class Locale extends \Locale +{ + /** + * @var LocaleDataProvider + */ + private static $dataProvider; + + /** + * @var string[]|null + */ + private static $locales; + + /** + * @var integer[]|null + */ + private static $lookupTable; + + /** + * Returns all available locales. + * + * @return string[] A list of ICU locale codes + * + * @api + */ + public static function getLocales() + { + if (null === self::$locales) { + self::$locales = self::getDataProvider()->getLocales(); + } + + return self::$locales; + } + + /** + * Returns whether the given ICU locale exists. + * + * This method does not canonicalize the given locale. Specifically, it will + * return false if the locale is not correctly cased or uses hyphens ("-") + * as separators between the subtags instead of underscores ("_"). For + * example, this method returns false for "en-Latn-GB", but true for + * "en_Latn_GB". + * + * If you want to support the above cases, you should manually canonicalize + * the locale prior to calling this method. + * + * @param string $locale A canonicalized ICU locale (e.g. "en_Latn_GB") + * + * @return Boolean Whether the locale exists + * + * @see canonicalize + * + * @api + */ + public static function exists($locale) + { + if (null === self::$lookupTable) { + self::$lookupTable = array_flip(static::getLocales()); + } + + return isset(self::$lookupTable[$locale]); + } + + /** + * Returns the name of a locale in the given display locale. + * + * If there is no suitable name found for a display locale, the ICU locale + * code is used instead. + * + * If null is passed as display locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $locale The ICU locale code to return the name of + * (e.g. "de_AT") + * @param string $displayLocale The ICU locale code to return the name in + * + * @return string The name of the locale + * + * @throws InvalidArgumentException If the locale or the display locale is + * invalid + * + * @see getNames + * + * @api + */ + public static function getName($locale, $displayLocale = null) + { + if (!static::exists($locale)) { + throw new InvalidArgumentException('The locale "' . $locale . '" does not exist.'); + } + + if (null !== $displayLocale && !in_array($displayLocale, self::getLocales(), true)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getName($locale, $displayLocale); + } + + /** + * Returns the names of all known locales in the given display locale. + * + * If null is passed as display locale, the result of + * {@link \Locale::getDefault()} is used instead. + * + * @param string $displayLocale The ICU locale code to return the names in + * + * @return string[] A list of locale names indexed by the corresponding ICU + * locale codes + * + * @throws InvalidArgumentException If the display locale is invalid + * + * @see getName + * + * @api + */ + public static function getNames($displayLocale = null) + { + if (null !== $displayLocale && !Locale::exists($displayLocale)) { + throw new InvalidArgumentException('The locale "' . $displayLocale . '" does not exist.'); + } + + if (null === $displayLocale) { + $displayLocale = \Locale::getDefault(); + } + + return self::getDataProvider()->getNames($displayLocale); + } + + /** + * Alias of {@link getName()}. + * + * This method exists for compatibility with the {@link \Locale} class. + * + * @param string $locale The ICU locale code to return the name of + * (e.g. "de_AT") + * @param string $displayLocale The ICU locale code to return the name in + * + * @return string The name of the locale + * + * @throws InvalidArgumentException If the locale or the display locale is + * invalid + * + * @see getName + */ + public static function getDisplayName($locale, $displayLocale = null) + { + return static::getName($locale, $displayLocale); + } + + /** + * Alias of {@link getNames()}. + * + * This method exists for compatibility with the {@link \Locale} class. + * + * @param string $displayLocale The ICU locale code to return the names in + * + * @return string[] A list of locale names indexed by the corresponding ICU + * locale codes + * + * @throws InvalidArgumentException If the display locale is invalid + * + * @see getNames + */ + public static function getDisplayNames($locale, $displayLocale = null) + { + return static::getNames($locale, $displayLocale); + } + + /** + * Returns a list of locale aliases. + * + * @return string[] An array with locale aliases as keys and ICU locale + * codes as values + * + * @api + */ + public static function getAliases() + { + return self::getDataProvider()->getAliases(); + } + + /** + * Returns the fallback locale for a given locale, if any + * + * @param string $locale The ICU locale code to find the fallback for. + * + * @return string|null The ICU locale code of the fallback locale, or null + * if no fallback exists + * + * @api + */ + public static function getFallback($locale) + { + if (false === $pos = strrpos($locale, '_')) { + if ('root' === $locale) { + return null; + } + + return 'root'; + } + + return substr($locale, 0, $pos); + } + + /** + * @return LocaleDataProvider + */ + private static function getDataProvider() + { + if (null === self::$dataProvider) { + self::$dataProvider = new LocaleDataProvider( + LocaleDataProvider::getResourceDirectory(), + Intl::getEntryReader() + ); + } + + return self::$dataProvider; + } + + /** + * This class must not be instantiated. + */ + private function __construct() {} +} diff --git a/src/Symfony/Component/Intl/Locale/Locale.php b/src/Symfony/Component/Intl/Locale/Locale.php index f16d937b02729..ce7ba96c9d36f 100644 --- a/src/Symfony/Component/Intl/Locale/Locale.php +++ b/src/Symfony/Component/Intl/Locale/Locale.php @@ -146,7 +146,7 @@ public static function getDisplayLanguage($locale, $inLocale = null) * * @throws MethodNotImplementedException */ - public static function getDisplayName($locale, $inLocale = null) + public static function getName($locale, $inLocale = null) { throw new MethodNotImplementedException(__METHOD__); } diff --git a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php index 8b3388e7ae373..a1929ae06f3b4 100644 --- a/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php +++ b/src/Symfony/Component/Intl/NumberFormatter/NumberFormatter.php @@ -16,8 +16,8 @@ use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException; use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; use Symfony\Component\Intl\Globals\IntlGlobals; -use Symfony\Component\Intl\Intl; -use Symfony\Component\Intl\Locale\Locale; +use Symfony\Component\Intl\Currency; +use Symfony\Component\Intl\Locale; /** * Replacement for PHP's native {@link \NumberFormatter} class. @@ -322,8 +322,8 @@ public function formatCurrency($value, $currency) return $this->format($value); } - $symbol = Intl::getCurrencyBundle()->getCurrencySymbol($currency, 'en'); - $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); + $symbol = Currency::getSymbol($currency, 'en'); + $fractionDigits = Currency::getFractionDigits($currency); $value = $this->roundCurrency($value, $currency); @@ -675,8 +675,8 @@ protected function resetError() */ private function roundCurrency($value, $currency) { - $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); - $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement($currency); + $fractionDigits = Currency::getFractionDigits($currency); + $roundingIncrement = Currency::getRoundingIncrement($currency); // Round with the formatter rounding mode $value = $this->round($value, $fractionDigits); diff --git a/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php b/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php index d1d523c40acd1..ac775ee0b22ef 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/AbstractBundle.php @@ -46,15 +46,7 @@ public function __construct($path, StructuredBundleReaderInterface $reader) } /** - * {@inheritdoc} - */ - public function getLocales() - { - return $this->reader->getLocales($this->path); - } - - /** - * Proxy method for {@link StructuredBundleReaderInterface#read}. + * Proxy method for {@link BundleEntryReaderInterface#read}. */ protected function read($locale) { @@ -62,10 +54,10 @@ protected function read($locale) } /** - * Proxy method for {@link StructuredBundleReaderInterface#readEntry}. + * Proxy method for {@link BundleEntryReaderInterface#readEntry}. */ - protected function readEntry($locale, array $indices, $mergeFallback = false) + protected function readEntry($locale, array $indices, $fallback = true) { - return $this->reader->readEntry($this->path, $locale, $indices, $mergeFallback); + return $this->reader->readEntry($this->path, $locale, $indices, $fallback); } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php index 174aa179f4067..311ca10bf3516 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Compiler/BundleCompiler.php @@ -11,61 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Compiler; -use Symfony\Component\Intl\Exception\RuntimeException; - /** - * Compiles .txt resource bundles to binary .res files. + * Alias of {@link GenrbBundleCompiler}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * the equivalent {@link GenrbBundleCompiler} instead. */ -class BundleCompiler implements BundleCompilerInterface +class BundleCompiler extends GenrbBundleCompiler { - /** - * @var string The path to the "genrb" executable. - */ - private $genrb; - - /** - * Creates a new compiler based on the "genrb" executable. - * - * @param string $genrb Optional. The path to the "genrb" executable. - * @param string $envVars Optional. Environment variables to be loaded when - * running "genrb". - * - * @throws RuntimeException If the "genrb" cannot be found. - */ - public function __construct($genrb = 'genrb', $envVars = '') - { - exec('which ' . $genrb, $output, $status); - - if (0 !== $status) { - throw new RuntimeException(sprintf( - 'The command "%s" is not installed', - $genrb - )); - } - - $this->genrb = ($envVars ? $envVars . ' ' : '') . $genrb; - } - - /** - * {@inheritdoc} - */ - public function compile($sourcePath, $targetDir) - { - if (is_dir($sourcePath)) { - $sourcePath .= '/*.txt'; - } - - exec($this->genrb.' --quiet -e UTF-8 -d '.$targetDir.' '.$sourcePath, $output, $status); - - if ($status !== 0) { - throw new RuntimeException(sprintf( - 'genrb failed with status %d while compiling %s to %s.', - $status, - $sourcePath, - $targetDir - )); - } - } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Compiler/GenrbBundleCompiler.php b/src/Symfony/Component/Intl/ResourceBundle/Compiler/GenrbBundleCompiler.php new file mode 100644 index 0000000000000..c8845cb0f7e3c --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Compiler/GenrbBundleCompiler.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Compiler; + +use Symfony\Component\Intl\Exception\RuntimeException; + +/** + * Compiles .txt resource bundles to binary .res files. + * + * @since 2.5 + * @author Bernhard Schussek + */ +class GenrbBundleCompiler implements BundleCompilerInterface +{ + /** + * @var string The path to the "genrb" executable. + */ + private $genrb; + + /** + * Creates a new compiler based on the "genrb" executable. + * + * @param string $genrb Optional. The path to the "genrb" executable. + * @param string $envVars Optional. Environment variables to be loaded when + * running "genrb". + * + * @throws RuntimeException If the "genrb" cannot be found. + */ + public function __construct($genrb = 'genrb', $envVars = '') + { + exec('which ' . $genrb, $output, $status); + + if (0 !== $status) { + throw new RuntimeException(sprintf( + 'The command "%s" is not installed', + $genrb + )); + } + + $this->genrb = ($envVars ? $envVars . ' ' : '') . $genrb; + } + + /** + * {@inheritdoc} + */ + public function compile($sourcePath, $targetDir) + { + if (is_dir($sourcePath)) { + $sourcePath .= '/*.txt'; + } + + exec($this->genrb.' --quiet -e UTF-8 -d '.$targetDir.' '.$sourcePath, $output, $status); + + if ($status !== 0) { + throw new RuntimeException(sprintf( + 'genrb failed with status %d while compiling %s to %s.', + $status, + $sourcePath, + $targetDir + )); + } + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php index 6f2a0ed39507b..429c24195fd85 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundle.php @@ -11,20 +11,26 @@ namespace Symfony\Component\Intl\ResourceBundle; +use Symfony\Component\Icu\CurrencyDataProvider; +use Symfony\Component\Intl\Locale; + /** * Default implementation of {@link CurrencyBundleInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link CurrencyDataProvider} instead. */ -class CurrencyBundle extends AbstractBundle implements CurrencyBundleInterface +class CurrencyBundle extends CurrencyDataProvider implements CurrencyBundleInterface { - const INDEX_NAME = 0; - - const INDEX_SYMBOL = 1; - - const INDEX_FRACTION_DIGITS = 2; - - const INDEX_ROUNDING_INCREMENT = 3; + /** + * {@inheritdoc} + */ + public function getLocales() + { + return Locale::getLocales(); + } /** * {@inheritdoc} @@ -35,7 +41,7 @@ public function getCurrencySymbol($currency, $locale = null) $locale = \Locale::getDefault(); } - return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_SYMBOL)); + return $this->getSymbol($currency, $locale); } /** @@ -47,7 +53,7 @@ public function getCurrencyName($currency, $locale = null) $locale = \Locale::getDefault(); } - return $this->readEntry($locale, array('Currencies', $currency, static::INDEX_NAME)); + return $this->getName($currency, $locale); } /** @@ -59,36 +65,6 @@ public function getCurrencyNames($locale = null) $locale = \Locale::getDefault(); } - if (null === ($currencies = $this->readEntry($locale, array('Currencies')))) { - return array(); - } - - if ($currencies instanceof \Traversable) { - $currencies = iterator_to_array($currencies); - } - - $index = static::INDEX_NAME; - - array_walk($currencies, function (&$value) use ($index) { - $value = $value[$index]; - }); - - return $currencies; - } - - /** - * {@inheritdoc} - */ - public function getFractionDigits($currency) - { - return $this->readEntry('en', array('Currencies', $currency, static::INDEX_FRACTION_DIGITS)); - } - - /** - * {@inheritdoc} - */ - public function getRoundingIncrement($currency) - { - return $this->readEntry('en', array('Currencies', $currency, static::INDEX_ROUNDING_INCREMENT)); + return $this->getNames($locale); } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php index 1a88e93722055..3bd32b3959a9a 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/CurrencyBundleInterface.php @@ -15,6 +15,9 @@ * Gives access to currency-related ICU data. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Icu\CurrencyDataProvider} instead. */ interface CurrencyBundleInterface extends ResourceBundleInterface { diff --git a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php index 6b98a29e39741..e47b6d90d25c8 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundle.php @@ -11,13 +11,48 @@ namespace Symfony\Component\Intl\ResourceBundle; +use Symfony\Component\Icu\LanguageDataProvider; +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Locale; +use Symfony\Component\Intl\ResourceBundle\Reader\BundleEntryReaderInterface; + /** * Default implementation of {@link LanguageBundleInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link LanguageDataProvider} instead. */ class LanguageBundle extends AbstractBundle implements LanguageBundleInterface { + /** + * @var LanguageDataProvider + */ + private $languageDataProvider; + + /** + * Creates a bundle at the given path using the given reader for reading + * bundle entries. + * + * @param string $path The path to the resource bundle. + * @param BundleEntryReaderInterface $reader The reader for reading the resource bundle. + */ + public function __construct($path, BundleEntryReaderInterface $reader) + { + $this->languageDataProvider = new LanguageDataProvider($path, $reader); + + parent::__construct($path, $reader); + } + + /** + * {@inheritdoc} + */ + public function getLocales() + { + return Locale::getLocales(); + } + /** * {@inheritdoc} */ @@ -27,17 +62,16 @@ public function getLanguageName($lang, $region = null, $locale = null) $locale = \Locale::getDefault(); } - if (null === ($languages = $this->readEntry($locale, array('Languages'), true))) { - return null; - } - // Some languages are translated together with their region, // i.e. "en_GB" is translated as "British English" - if (null !== $region && isset($languages[$lang.'_'.$region])) { - return $languages[$lang.'_'.$region]; + if (null !== $region) { + try { + return $this->languageDataProvider->getName($lang.'_'.$region, $locale); + } catch (MissingResourceException $e) { + } } - return $languages[$lang]; + return $this->languageDataProvider->getName($lang, $locale); } /** @@ -49,15 +83,7 @@ public function getLanguageNames($locale = null) $locale = \Locale::getDefault(); } - if (null === ($languages = $this->readEntry($locale, array('Languages'), true))) { - return array(); - } - - if ($languages instanceof \Traversable) { - $languages = iterator_to_array($languages); - } - - return $languages; + return $this->languageDataProvider->getNames($locale); } /** @@ -69,28 +95,7 @@ public function getScriptName($script, $lang = null, $locale = null) $locale = \Locale::getDefault(); } - $data = $this->read($locale); - - // Some languages are translated together with their script, - // e.g. "zh_Hans" is translated as "Simplified Chinese" - if (null !== $lang && isset($data['Languages'][$lang.'_'.$script])) { - $langName = $data['Languages'][$lang.'_'.$script]; - - // If the script is appended in braces, extract it, e.g. "zh_Hans" - // is translated as "Chinesisch (vereinfacht)" in locale "de" - if (strpos($langName, '(') !== false) { - list($langName, $scriptName) = preg_split('/[\s()]/', $langName, null, PREG_SPLIT_NO_EMPTY); - - return $scriptName; - } - } - - // "af" (Afrikaans) has no "Scripts" block - if (!isset($data['Scripts'][$script])) { - return null; - } - - return $data['Scripts'][$script]; + return $this->readEntry($locale, array('Scripts', $script)); } /** @@ -102,7 +107,7 @@ public function getScriptNames($locale = null) $locale = \Locale::getDefault(); } - if (null === ($scripts = $this->readEntry($locale, array('Scripts'), true))) { + if (null === ($scripts = $this->readEntry($locale, array('Scripts')))) { return array(); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php index de50bda05737b..063d1b3fff7f6 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/LanguageBundleInterface.php @@ -15,6 +15,9 @@ * Gives access to language-related ICU data. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link LanguageDataProvider} instead. */ interface LanguageBundleInterface extends ResourceBundleInterface { diff --git a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php index 6f6cdfcb189c2..3fccd8976213d 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundle.php @@ -11,42 +11,39 @@ namespace Symfony\Component\Intl\ResourceBundle; +use Symfony\Component\Icu\LocaleDataProvider; + /** * Default implementation of {@link LocaleBundleInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link LocaleDataProvider} instead. */ -class LocaleBundle extends AbstractBundle implements LocaleBundleInterface +class LocaleBundle extends LocaleDataProvider implements LocaleBundleInterface { /** - * {@inheritdoc} + * Alias of {@link getNames()}. */ - public function getLocaleName($ofLocale, $locale = null) + public function getLocaleNames($locale = null) { - if (null === $locale) { - $locale = \Locale::getDefault(); - } - - return $this->readEntry($locale, array('Locales', $ofLocale)); + return $this->getNames($locale); } /** - * {@inheritdoc} + * Alias of {@link getName()}. */ - public function getLocaleNames($locale = null) + public function getLocaleName($ofLocale, $locale = null) { - if (null === $locale) { - $locale = \Locale::getDefault(); - } - - if (null === ($locales = $this->readEntry($locale, array('Locales')))) { - return array(); - } - - if ($locales instanceof \Traversable) { - $locales = iterator_to_array($locales); - } + return $this->getName($ofLocale, $locale); + } - return $locales; + /** + * Alias of {@link getAliases()}. + */ + public function getLocaleAliases() + { + return $this->getAliases(); } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php index daf5be68a5125..2475389927090 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/LocaleBundleInterface.php @@ -15,6 +15,9 @@ * Gives access to locale-related ICU data. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.5, to be removed in Symfony 3.0. + * Use {@link \Symfony\Component\Icu\LocaleDataProvider} instead. */ interface LocaleBundleInterface extends ResourceBundleInterface { @@ -38,4 +41,12 @@ public function getLocaleName($ofLocale, $locale = null); * @return string[] A list of locale names indexed by locale codes. */ public function getLocaleNames($locale = null); + + /** + * Returns a list of locale aliases. + * + * @return array An array with aliases as keys and aliased locales as + * values. + */ + public function getLocaleAliases(); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php deleted file mode 100644 index c30693ac57a20..0000000000000 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/AbstractBundleReader.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\ResourceBundle\Reader; - -/** - * Base class for {@link BundleReaderInterface} implementations. - * - * @author Bernhard Schussek - */ -abstract class AbstractBundleReader implements BundleReaderInterface -{ - /** - * {@inheritdoc} - */ - public function getLocales($path) - { - $extension = '.' . $this->getFileExtension(); - $locales = glob($path . '/*' . $extension); - - // Remove file extension and sort - array_walk($locales, function (&$locale) use ($extension) { $locale = basename($locale, $extension); }); - sort($locales); - - return $locales; - } - - /** - * Returns the extension of locale files in this bundle. - * - * @return string The file extension (without leading dot). - */ - abstract protected function getFileExtension(); -} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php index 56cef806da5b7..fca9214b78488 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BinaryBundleReader.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; -use Symfony\Component\Intl\Exception\RuntimeException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; use Symfony\Component\Intl\ResourceBundle\Util\ArrayAccessibleResourceBundle; /** @@ -19,7 +19,7 @@ * * @author Bernhard Schussek */ -class BinaryBundleReader extends AbstractBundleReader implements BundleReaderInterface +class BinaryBundleReader implements BundleReaderInterface { /** * {@inheritdoc} @@ -28,24 +28,23 @@ public function read($path, $locale) { // Point for future extension: Modify this class so that it works also // if the \ResourceBundle class is not available. - $bundle = new \ResourceBundle($locale, $path); + // Never enable fallback. We want to know if a bundle cannot be found + $bundle = new \ResourceBundle($locale, $path, false); + + // The bundle is NULL if the path does not look like a resource bundle + // (i.e. contain a bunch of *.res files) if (null === $bundle) { - throw new RuntimeException(sprintf( - 'Could not load the resource bundle "%s/%s.res".', + throw new ResourceBundleNotFoundException(sprintf( + 'The resource bundle "%s/%s.res" could not be found.', $path, $locale )); } - return new ArrayAccessibleResourceBundle($bundle); - } + // Other possible errors are U_USING_FALLBACK_WARNING and U_ZERO_ERROR, + // which are OK for us. - /** - * {@inheritdoc} - */ - protected function getFileExtension() - { - return 'res'; + return new ArrayAccessibleResourceBundle($bundle); } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedBundleReader.php index e44074b168b4d..8a3d12bda33a8 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BufferedBundleReader.php @@ -51,12 +51,4 @@ public function read($path, $locale) return $this->buffer[$hash]; } - - /** - * {@inheritdoc} - */ - public function getLocales($path) - { - return $this->reader->getLocales($path); - } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReader.php new file mode 100644 index 0000000000000..bf8965646259b --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReader.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; +use Symfony\Component\Intl\Exception\OutOfBoundsException; +use Symfony\Component\Intl\Locale; +use Symfony\Component\Intl\ResourceBundle\Util\RecursiveArrayAccess; + +/** + * A structured reader wrapping an existing resource bundle reader. + * + * @since 2.5 + * @author Bernhard Schussek + * @see BundleEntryReaderInterface + */ +class BundleEntryReader implements BundleEntryReaderInterface, StructuredBundleReaderInterface +{ + /** + * @var BundleReaderInterface + */ + private $reader; + + /** + * A mapping of locale aliases to locales + * + * @var array + */ + private $localeAliases = array(); + + /** + * Creates an entry reader based on the given resource bundle reader. + * + * @param BundleReaderInterface $reader A resource bundle reader to use. + */ + public function __construct(BundleReaderInterface $reader) + { + $this->reader = $reader; + } + + /** + * Stores a mapping of locale aliases to locales. + * + * This mapping is used when reading entries and merging them with their + * fallback locales. If an entry is read for a locale alias (e.g. "mo") + * that points to a locale with a fallback locale ("ro_MD"), the reader + * can continue at the correct fallback locale ("ro"). + * + * @param array $localeAliases A mapping of locale aliases to locales + */ + public function setLocaleAliases($localeAliases) + { + $this->localeAliases = $localeAliases; + } + + /** + * {@inheritdoc} + */ + public function read($path, $locale) + { + return $this->reader->read($path, $locale); + } + + /** + * {@inheritdoc} + */ + public function readEntry($path, $locale, array $indices, $fallback = true) + { + $entry = null; + $isMultiValued = false; + $readSucceeded = false; + $exception = null; + $currentLocale = $locale; + $testedLocales = array(); + + while (null !== $currentLocale) { + // Resolve any aliases to their target locales + if (isset($this->localeAliases[$currentLocale])) { + $currentLocale = $this->localeAliases[$currentLocale]; + } + + try { + $data = $this->reader->read($path, $currentLocale); + $currentEntry = RecursiveArrayAccess::get($data, $indices); + $readSucceeded = true; + + $isCurrentTraversable = $currentEntry instanceof \Traversable; + $isCurrentMultiValued = $isCurrentTraversable || is_array($currentEntry); + + // Return immediately if fallback is disabled or we are dealing + // with a scalar non-null entry + if (!$fallback || (!$isCurrentMultiValued && null !== $currentEntry)) { + return $currentEntry; + } + + // ========================================================= + // Fallback is enabled, entry is either multi-valued or NULL + // ========================================================= + + // If entry is multi-valued, convert to array + if ($isCurrentTraversable) { + $currentEntry = iterator_to_array($currentEntry); + } + + // If previously read entry was multi-valued too, merge them + if ($isCurrentMultiValued && $isMultiValued) { + $currentEntry = array_merge($currentEntry, $entry); + } + + // Keep the previous entry if the current entry is NULL + if (null !== $currentEntry) { + $entry = $currentEntry; + } + + // If this or the previous entry was multi-valued, we are dealing + // with a merged, multi-valued entry now + $isMultiValued = $isMultiValued || $isCurrentMultiValued; + } catch (ResourceBundleNotFoundException $e) { + // Continue if there is a fallback locale for the current + // locale + $exception = $e; + } catch (OutOfBoundsException $e) { + // Remember exception and rethrow if we cannot find anything in + // the fallback locales either + $exception = $e; + } + + // Remember which locales we tried + $testedLocales[] = $currentLocale.'.res'; + + // Check whether fallback is allowed + if (!$fallback) { + break; + } + + // Then determine fallback locale + $currentLocale = Locale::getFallback($currentLocale); + } + + // Multi-valued entry was merged + if ($isMultiValued) { + return $entry; + } + + // Entry is still NULL, but no read error occurred + if ($readSucceeded) { + return $entry; + } + + // Entry is still NULL, read error occurred. Throw an exception + // containing the detailed path and locale + $errorMessage = sprintf( + 'Couldn\'t read the indices [%s] from "%s/%s.res".', + implode('][', $indices), + $path, + $locale + ); + + // Append fallback locales, if any + if (count($testedLocales) > 1) { + // Remove original locale + array_shift($testedLocales); + + $errorMessage .= sprintf( + ' The indices also couldn\'t be found in the fallback locale(s) "%s".', + implode('", "', $testedLocales) + ); + } + + throw new MissingResourceException($errorMessage, 0, $exception); + } +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReaderInterface.php new file mode 100644 index 0000000000000..a7001de831735 --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleEntryReaderInterface.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Reader; + +/** + * Reads individual entries of a resource file. + * + * @since 2.5 + * @author Bernhard Schussek + */ +interface BundleEntryReaderInterface extends BundleReaderInterface +{ + /** + * Reads an entry from a resource bundle. + * + * An entry can be selected from the resource bundle by passing the path + * to that entry in the bundle. For example, if the bundle is structured + * like this: + * + * TopLevel + * NestedLevel + * Entry: Value + * + * Then the value can be read by calling: + * + * $reader->readEntry('...', 'en', array('TopLevel', 'NestedLevel', 'Entry')); + * + * @param string $path The path to the resource bundle. + * @param string $locale The locale to read. + * @param string[] $indices The indices to read from the bundle. + * @param Boolean $fallback Whether to merge the value with the value from + * the fallback locale (e.g. "en" for "en_GB"). + * Only applicable if the result is multivalued + * (i.e. array or \ArrayAccess) or cannot be found + * in the requested locale. + * + * @return mixed Returns an array or {@link \ArrayAccess} instance for + * complex data and a scalar value for simple data. + * + * @throws \Symfony\Component\Intl\Exception\MissingResourceException If the indices cannot be accessed + */ + public function readEntry($path, $locale, array $indices, $fallback = true); +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php index bc485cd5267b4..80f84c9189453 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/BundleReaderInterface.php @@ -28,13 +28,4 @@ interface BundleReaderInterface * complex data, a scalar value otherwise. */ public function read($path, $locale); - - /** - * Reads the available locales of a resource bundle. - * - * @param string $path The path to the resource bundle. - * - * @return string[] A list of supported locale codes. - */ - public function getLocales($path); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php index 663bcc9d789d8..739e6de39024a 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/PhpBundleReader.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; -use Symfony\Component\Intl\Exception\InvalidArgumentException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; use Symfony\Component\Intl\Exception\RuntimeException; /** @@ -19,21 +19,17 @@ * * @author Bernhard Schussek */ -class PhpBundleReader extends AbstractBundleReader implements BundleReaderInterface +class PhpBundleReader implements BundleReaderInterface { /** * {@inheritdoc} */ public function read($path, $locale) { - if ('en' !== $locale) { - throw new InvalidArgumentException('Only the locale "en" is supported.'); - } - $fileName = $path . '/' . $locale . '.php'; if (!file_exists($fileName)) { - throw new RuntimeException(sprintf( + throw new ResourceBundleNotFoundException(sprintf( 'The resource bundle "%s/%s.php" does not exist.', $path, $locale @@ -50,12 +46,4 @@ public function read($path, $locale) return include $fileName; } - - /** - * {@inheritdoc} - */ - protected function getFileExtension() - { - return 'php'; - } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php index e3656fe2ebdf6..fb5ee300b4c91 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReader.php @@ -11,103 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; -use Symfony\Component\Intl\ResourceBundle\Util\RecursiveArrayAccess; - /** - * A structured reader wrapping an existing resource bundle reader. + * Alias of {@link BundleEntryReaderInterface}. * * @author Bernhard Schussek * - * @see StructuredResourceBundleBundleReaderInterface + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * {@link BundleEntryReader} instead. */ -class StructuredBundleReader implements StructuredBundleReaderInterface +class StructuredBundleReader extends BundleEntryReader { - /** - * @var BundleReaderInterface - */ - private $reader; - - /** - * Creates an entry reader based on the given resource bundle reader. - * - * @param BundleReaderInterface $reader A resource bundle reader to use. - */ - public function __construct(BundleReaderInterface $reader) - { - $this->reader = $reader; - } - - /** - * {@inheritdoc} - */ - public function read($path, $locale) - { - return $this->reader->read($path, $locale); - } - - /** - * {@inheritdoc} - */ - public function getLocales($path) - { - return $this->reader->getLocales($path); - } - - /** - * {@inheritdoc} - */ - public function readEntry($path, $locale, array $indices, $fallback = true) - { - $data = $this->reader->read($path, $locale); - - $entry = RecursiveArrayAccess::get($data, $indices); - $multivalued = is_array($entry) || $entry instanceof \Traversable; - - if (!($fallback && (null === $entry || $multivalued))) { - return $entry; - } - - if (null !== ($fallbackLocale = $this->getFallbackLocale($locale))) { - $parentEntry = $this->readEntry($path, $fallbackLocale, $indices, true); - - if ($entry || $parentEntry) { - $multivalued = $multivalued || is_array($parentEntry) || $parentEntry instanceof \Traversable; - - if ($multivalued) { - if ($entry instanceof \Traversable) { - $entry = iterator_to_array($entry); - } - - if ($parentEntry instanceof \Traversable) { - $parentEntry = iterator_to_array($parentEntry); - } - - $entry = array_merge( - $parentEntry ?: array(), - $entry ?: array() - ); - } else { - $entry = null === $entry ? $parentEntry : $entry; - } - } - } - - return $entry; - } - - /** - * Returns the fallback locale for a given locale, if any - * - * @param string $locale The locale to find the fallback for. - * - * @return string|null The fallback locale, or null if no parent exists - */ - private function getFallbackLocale($locale) - { - if (false === $pos = strrpos($locale, '_')) { - return null; - } - - return substr($locale, 0, $pos); - } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php index c22ad93b97dad..8978ca3b6e74d 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Reader/StructuredBundleReaderInterface.php @@ -12,39 +12,13 @@ namespace Symfony\Component\Intl\ResourceBundle\Reader; /** - * Reads individual entries of a resource file. + * Alias of {@link BundleEntryReaderInterface}. * * @author Bernhard Schussek + * + * @deprecated Deprecated since version 2.4, to be removed in Symfony 3.0. Use + * {@link BundleEntryReaderInterface} instead. */ -interface StructuredBundleReaderInterface extends BundleReaderInterface +interface StructuredBundleReaderInterface extends BundleEntryReaderInterface { - /** - * Reads an entry from a resource bundle. - * - * An entry can be selected from the resource bundle by passing the path - * to that entry in the bundle. For example, if the bundle is structured - * like this: - * - * TopLevel - * NestedLevel - * Entry: Value - * - * Then the value can be read by calling: - * - * $reader->readEntry('...', 'en', array('TopLevel', 'NestedLevel', 'Entry')); - * - * @param string $path The path to the resource bundle. - * @param string $locale The locale to read. - * @param string[] $indices The indices to read from the bundle. - * @param Boolean $fallback Whether to merge the value with the value from - * the fallback locale (e.g. "en" for "en_GB"). - * Only applicable if the result is multivalued - * (i.e. array or \ArrayAccess) or cannot be found - * in the requested locale. - * - * @return mixed Returns an array or {@link \ArrayAccess} instance for - * complex data, a scalar value for simple data and NULL - * if the given path could not be accessed. - */ - public function readEntry($path, $locale, array $indices, $fallback = true); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php b/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php index bbfbedeed9ddf..85f8889b8f52b 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/RegionBundle.php @@ -18,6 +18,20 @@ */ class RegionBundle extends AbstractBundle implements RegionBundleInterface { + /** + * {@inheritdoc} + */ + public function getLocales() + { + $locales = $this->readEntry('meta', array('Locales')); + + if ($locales instanceof \Traversable) { + $locales = iterator_to_array($locales); + } + + return $locales; + } + /** * {@inheritdoc} */ @@ -27,7 +41,7 @@ public function getCountryName($country, $locale = null) $locale = \Locale::getDefault(); } - return $this->readEntry($locale, array('Countries', $country), true); + return $this->readEntry($locale, array('Countries', $country)); } /** @@ -39,7 +53,7 @@ public function getCountryNames($locale = null) $locale = \Locale::getDefault(); } - if (null === ($countries = $this->readEntry($locale, array('Countries'), true))) { + if (null === ($countries = $this->readEntry($locale, array('Countries')))) { return array(); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Scanner/LocaleScanner.php b/src/Symfony/Component/Intl/ResourceBundle/Scanner/LocaleScanner.php new file mode 100644 index 0000000000000..cd3c62e78753e --- /dev/null +++ b/src/Symfony/Component/Intl/ResourceBundle/Scanner/LocaleScanner.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\ResourceBundle\Scanner; + +/** + * Scans a directory with text data files for locales. + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * Do NOT use this class in your own code. Backwards compatibility can NOT be + * guaranteed and BC breaks will NOT be documented. + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * The name of each *.txt file (without suffix) in the given source directory + * is considered a locale. + * + * @author Bernhard Schussek + */ +class LocaleScanner +{ + /** + * A list of known non-locales. + * + * @var array + */ + private static $blackList = array( + 'root', + 'meta', + 'supplementalData', + 'supplementaldata', + ); + + /** + * Returns all locales found in the given directory. + * + * @param string $sourceDir The directory with ICU *.txt files. + * + * @return array An array of locales. The result also contains locales that + * are in fact just aliases for other locales. Use + * {@link scanAliases()} to determine which of the locales + * are aliases. + */ + public function scanLocales($sourceDir) + { + $locales = glob($sourceDir.'/*.txt'); + + // Remove file extension and sort + array_walk($locales, function (&$locale) { $locale = basename($locale, '.txt'); }); + + // Remove non-locales + $locales = array_diff($locales, static::$blackList); + + sort($locales); + + return $locales; + } + + /** + * Returns all locale aliases found in the given directory. + * + * @param string $sourceDir The directory with ICU *.txt files. + * + * @return array An array with the locale aliases as keys and the aliased + * locales as values. + */ + public function scanAliases($sourceDir) + { + $locales = $this->scanLocales($sourceDir); + $aliases = array(); + + // Delete locales that are no aliases + foreach ($locales as $locale) { + $content = file_get_contents($sourceDir.'/'.$locale.'.txt'); + + // Aliases contain the text "%%ALIAS" followed by the aliased locale + if (preg_match('/"%%ALIAS"\{"([^"]+)"\}/', $content, $matches)) { + $aliases[$locale] = $matches[1]; + } + } + + return $aliases; + } + +} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php index 0692d6fe50a05..1c5b874cdd0d7 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/BundleTransformer.php @@ -40,13 +40,13 @@ public function addRule(TransformationRuleInterface $rule) /** * Runs the compilation with the given compilation context. * - * @param CompilationContextInterface $context The context storing information - * needed to run the compilation. + * @param CompilationContext $context The context storing information + * needed to run the compilation. * * @throws RuntimeException If any of the files to be compiled by the loaded * compilation rules does not exist. */ - public function compileBundles(CompilationContextInterface $context) + public function compileBundles(CompilationContext $context) { $filesystem = $context->getFilesystem(); $compiler = $context->getCompiler(); @@ -75,7 +75,7 @@ public function compileBundles(CompilationContextInterface $context) } } - public function createStubs(StubbingContextInterface $context) + public function createStubs(StubbingContext $context) { $filesystem = $context->getFilesystem(); $phpWriter = new PhpBundleWriter(); diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php index cdc1951b96bcc..bc6ede8aa401b 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContext.php @@ -13,13 +13,14 @@ use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface; +use Symfony\Component\Intl\ResourceBundle\Scanner\LocaleScanner; /** - * Default implementation of {@link CompilationContextInterface}. + * Stores contextual information for resource bundle compilation. * * @author Bernhard Schussek */ -class CompilationContext implements CompilationContextInterface +class CompilationContext { /** * @var string @@ -32,7 +33,7 @@ class CompilationContext implements CompilationContextInterface private $binaryDir; /** - * @var FileSystem + * @var Filesystem */ private $filesystem; @@ -46,17 +47,26 @@ class CompilationContext implements CompilationContextInterface */ private $icuVersion; - public function __construct($sourceDir, $binaryDir, Filesystem $filesystem, BundleCompilerInterface $compiler, $icuVersion) + /** + * @var LocaleScanner + */ + private $localeScanner; + + public function __construct($sourceDir, $binaryDir, Filesystem $filesystem, BundleCompilerInterface $compiler, $icuVersion, LocaleScanner $localeScanner = null) { $this->sourceDir = $sourceDir; $this->binaryDir = $binaryDir; $this->filesystem = $filesystem; $this->compiler = $compiler; $this->icuVersion = $icuVersion; + $this->localeScanner = $localeScanner ?: new LocaleScanner(); } /** - * {@inheritdoc} + * Returns the directory where the source versions of the resource bundles + * are stored. + * + * @return string An absolute path to a directory. */ public function getSourceDir() { @@ -64,7 +74,9 @@ public function getSourceDir() } /** - * {@inheritdoc} + * Returns the directory where the binary resource bundles are stored. + * + * @return string An absolute path to a directory. */ public function getBinaryDir() { @@ -72,7 +84,9 @@ public function getBinaryDir() } /** - * {@inheritdoc} + * Returns a tool for manipulating the filesystem. + * + * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. */ public function getFilesystem() { @@ -80,7 +94,9 @@ public function getFilesystem() } /** - * {@inheritdoc} + * Returns a resource bundle compiler. + * + * @return \Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface The loaded resource bundle compiler. */ public function getCompiler() { @@ -88,10 +104,22 @@ public function getCompiler() } /** - * {@inheritdoc} + * Returns the ICU version of the bundles being converted. + * + * @return string The ICU version string. */ public function getIcuVersion() { return $this->icuVersion; } + + /** + * Returns a locale scanner. + * + * @return \Symfony\Component\Intl\ResourceBundle\Scanner\LocaleScanner The locale scanner. + */ + public function getLocaleScanner() + { + return $this->localeScanner; + } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php deleted file mode 100644 index f05c28079a211..0000000000000 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/CompilationContextInterface.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\ResourceBundle\Transformer; - -/** - * Stores contextual information for resource bundle compilation. - * - * @author Bernhard Schussek - */ -interface CompilationContextInterface -{ - /** - * Returns the directory where the source versions of the resource bundles - * are stored. - * - * @return string An absolute path to a directory. - */ - public function getSourceDir(); - - /** - * Returns the directory where the binary resource bundles are stored. - * - * @return string An absolute path to a directory. - */ - public function getBinaryDir(); - - /** - * Returns a tool for manipulating the filesystem. - * - * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. - */ - public function getFilesystem(); - - /** - * Returns a resource bundle compiler. - * - * @return \Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompilerInterface The loaded resource bundle compiler. - */ - public function getCompiler(); - - /** - * Returns the ICU version of the bundles being converted. - * - * @return string The ICU version string. - */ - public function getIcuVersion(); -} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php index 95783b3b06a19..17062502089bb 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/CurrencyBundleTransformationRule.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; -use Symfony\Component\Intl\Intl; use Symfony\Component\Intl\ResourceBundle\CurrencyBundle; -use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; -use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\ResourceBundle\CurrencyBundleInterface; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext; +use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; use Symfony\Component\Intl\Util\IcuVersion; /** @@ -24,6 +26,16 @@ */ class CurrencyBundleTransformationRule implements TransformationRuleInterface { + /** + * @var CurrencyBundleInterface + */ + private $currencyBundle; + + public function __construct(CurrencyBundleInterface $currencyBundle) + { + $this->currencyBundle = $currencyBundle; + } + /** * {@inheritdoc} */ @@ -35,48 +47,140 @@ public function getBundleName() /** * {@inheritdoc} */ - public function beforeCompile(CompilationContextInterface $context) + public function beforeCompile(CompilationContext $context) { - // The currency data is contained in the locales and misc bundles + $tempDir = sys_get_temp_dir().'/icu-data-currencies'; + + $context->getFilesystem()->remove($tempDir); + $context->getFilesystem()->mkdir(array($tempDir, $tempDir.'/res')); + + // The currency data is contained in the locales and meta bundles // in ICU <= 4.2 if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) { - return array( - $context->getSourceDir() . '/misc/supplementalData.txt', - $context->getSourceDir() . '/locales' - ); + $context->getFilesystem()->mirror($context->getSourceDir().'/locales', $tempDir.'/txt'); + $context->getFilesystem()->copy($context->getSourceDir().'/misc/supplementalData.txt', $tempDir.'/txt/supplementalData.txt'); + } else { + $context->getFilesystem()->mirror($context->getSourceDir().'/curr', $tempDir.'/txt'); } - return $context->getSourceDir() . '/curr'; + $context->getCompiler()->compile($tempDir.'/txt', $tempDir.'/res'); + $context->getCompiler()->compile($context->getSourceDir().'/misc/currencyNumericCodes.txt', $tempDir.'/res'); + + $reader = new BinaryBundleReader(); + $writer = new TextBundleWriter(); + + // Collect supported locales of the bundle + $availableLocales = $context->getLocaleScanner()->scanLocales($tempDir.'/txt'); + + // Drop and regenerate txt files + $context->getFilesystem()->remove($tempDir.'/txt'); + $context->getFilesystem()->mkdir($tempDir.'/txt'); + + $currencies = array(); + + // Generate a text file for each locale + foreach ($availableLocales as $locale) { + $bundle = $reader->read($tempDir.'/res', $locale); + + if (isset($bundle['Currencies']) && null !== $bundle['Currencies']) { + $symbolNamePairs = iterator_to_array($bundle['Currencies']); + + // Remove the unknown currency + unset($symbolNamePairs['XXX']); + + // No other keys but "Currencies" are needed for now + $writer->write($tempDir.'/txt', $locale, array( + 'Version' => $bundle['Version'], + 'Currencies' => $symbolNamePairs, + )); + + // Add currencies to the list of known currencies + $currencies = array_merge($currencies, array_keys($symbolNamePairs)); + } + } + + // Remove duplicate currencies and sort + $currencies = array_unique($currencies); + sort($currencies); + + // Open resource bundles that contain currency metadata + $root = $reader->read($tempDir.'/res', 'root'); + $supplementalData = $reader->read($tempDir.'/res', 'supplementalData'); + $numericCodes = $reader->read($tempDir.'/res', 'currencyNumericCodes'); + + // Generate default currency names and symbols + $defaultSymbolNamePairs = array_map( + function ($currency) use ($root) { + if (isset($root['Currencies'][$currency]) && null !== $root['Currencies'][$currency]) { + return $root['Currencies'][$currency]; + } + + // by default both the symbol and the name equal the ISO code + return array($currency, $currency); + }, + $currencies + ); + + // Replace keys by currencies + $defaultSymbolNamePairs = array_combine($currencies, $defaultSymbolNamePairs); + + // Generate and sort the mapping from 3-letter codes to numeric codes + $alpha3ToNumericMapping = iterator_to_array($numericCodes['codeMap']); + + asort($alpha3ToNumericMapping); + + // Filter unknown currencies (e.g. "AYM") + $alpha3ToNumericMapping = array_intersect_key($alpha3ToNumericMapping, $defaultSymbolNamePairs); + + // Generate numeric code to 3-letter code mapping + $numericToAlpha3Mapping = array(); + + 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] = array(); + } + + $numericToAlpha3Mapping[$numeric][] = $alpha3; + } + + // Write the root resource bundle + $writer->write($tempDir.'/txt', 'root', array( + 'Version' => $root['Version'], + 'Currencies' => $defaultSymbolNamePairs, + 'CurrencyMeta' => $supplementalData['CurrencyMeta'], + 'Alpha3ToNumeric' => $alpha3ToNumericMapping, + 'NumericToAlpha3' => $numericToAlpha3Mapping, + )); + + // The temporary directory now contains all sources to be compiled + return $tempDir.'/txt'; } /** * {@inheritdoc} */ - public function afterCompile(CompilationContextInterface $context) + public function afterCompile(CompilationContext $context) { - // \ResourceBundle does not like locale names with uppercase chars, so rename - // the resource file - // See: http://bugs.php.net/bug.php?id=54025 - $fileName = $context->getBinaryDir() . '/curr/supplementalData.res'; - $fileNameLower = $context->getBinaryDir() . '/curr/supplementaldata.res'; - - $context->getFilesystem()->rename($fileName, $fileNameLower); + // Remove the temporary directory + //$context->getFilesystem()->remove(sys_get_temp_dir().'/icu-data-currencies-source'); } /** * {@inheritdoc} */ - public function beforeCreateStub(StubbingContextInterface $context) + public function beforeCreateStub(StubbingContext $context) { $currencies = array(); - $currencyBundle = Intl::getCurrencyBundle(); - foreach ($currencyBundle->getCurrencyNames('en') as $code => $name) { + foreach ($this->currencyBundle->getCurrencyNames('en') as $code => $name) { $currencies[$code] = array( CurrencyBundle::INDEX_NAME => $name, - CurrencyBundle::INDEX_SYMBOL => $currencyBundle->getCurrencySymbol($code, 'en'), - CurrencyBundle::INDEX_FRACTION_DIGITS => $currencyBundle->getFractionDigits($code), - CurrencyBundle::INDEX_ROUNDING_INCREMENT => $currencyBundle->getRoundingIncrement($code), + CurrencyBundle::INDEX_SYMBOL => $this->currencyBundle->getCurrencySymbol($code, 'en'), + CurrencyBundle::INDEX_FRACTION_DIGITS => $this->currencyBundle->getFractionDigits($code), + CurrencyBundle::INDEX_ROUNDING_INCREMENT => $this->currencyBundle->getRoundingIncrement($code), ); } @@ -88,7 +192,7 @@ public function beforeCreateStub(StubbingContextInterface $context) /** * {@inheritdoc} */ - public function afterCreateStub(StubbingContextInterface $context) + public function afterCreateStub(StubbingContext $context) { } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php index 5e6f901849dac..7fe329ca106e5 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LanguageBundleTransformationRule.php @@ -11,9 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; +use Symfony\Component\DependencyInjection\Tests\DefinitionDecoratorTest; +use Symfony\Component\Intl\Exception\RuntimeException; use Symfony\Component\Intl\Intl; -use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; -use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext; +use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; use Symfony\Component\Intl\Util\IcuVersion; /** @@ -23,6 +28,58 @@ */ class LanguageBundleTransformationRule implements TransformationRuleInterface { + /** + * Source: http://www-01.sil.org/iso639-3/codes.asp + * + * @var array + */ + private static $preferredAlpha2ToAlpha3Mapping = array( + 'ak' => 'aka', + 'ar' => 'ara', + 'ay' => 'aym', + 'az' => 'aze', + 'cr' => 'cre', + 'et' => 'est', + 'fa' => 'fas', + 'ff' => 'ful', + 'gn' => 'grn', + 'ik' => 'ipk', + 'iu' => 'iku', + 'kr' => 'kau', + 'kg' => 'kon', + 'kv' => 'kom', + 'ku' => 'kur', + 'lv' => 'lav', + 'mg' => 'mlg', + 'mn' => 'mon', + 'ms' => 'msa', + 'nb' => 'nob', + 'ne' => 'nep', + 'oj' => 'oji', + 'om' => 'orm', + 'or' => 'ori', + 'ps' => 'pus', + 'qu' => 'que', + 'ro' => 'ron', + 'sc' => 'srd', + 'sq' => 'sqi', + 'sw' => 'swa', + 'uz' => 'uzb', + 'yi' => 'yid', + 'za' => 'zha', + 'zh' => 'zho', + ); + + /** + * @var LanguageBundleInterface + */ + private $languageBundle; + + public function __construct(LanguageBundleInterface $languageBundle) + { + $this->languageBundle = $languageBundle; + } + /** * {@inheritdoc} */ @@ -34,38 +91,136 @@ public function getBundleName() /** * {@inheritdoc} */ - public function beforeCompile(CompilationContextInterface $context) + public function beforeCompile(CompilationContext $context) { + $tempDir = sys_get_temp_dir().'/icu-data-languages'; + // The language data is contained in the locales bundle in ICU <= 4.2 if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) { - return $context->getSourceDir() . '/locales'; + $sourceDir = $context->getSourceDir() . '/locales'; + } else { + $sourceDir = $context->getSourceDir() . '/lang'; } - return $context->getSourceDir() . '/lang'; + $context->getFilesystem()->remove($tempDir); + $context->getFilesystem()->mkdir(array($tempDir, $tempDir.'/res')); + $context->getFilesystem()->mirror($sourceDir, $tempDir.'/txt'); + + $context->getCompiler()->compile($tempDir.'/txt', $tempDir.'/res'); + $context->getCompiler()->compile($context->getSourceDir().'/misc/metadata.txt', $tempDir.'/res'); + + $reader = new BinaryBundleReader(); + $writer = new TextBundleWriter(); + + // Collect supported locales of the bundle + $availableLocales = $context->getLocaleScanner()->scanLocales($tempDir.'/txt'); + + // Drop and regenerate txt files + $context->getFilesystem()->remove($tempDir.'/txt'); + $context->getFilesystem()->mkdir($tempDir.'/txt'); + + $languages = array(); + + // Collect complete list of languages and scripts in all locales + foreach ($availableLocales as $locale) { + $bundle = $reader->read($tempDir.'/res', $locale); + + // isset() on \ResourceBundle returns true even if the value is null + if (isset($bundle['Languages']) && null !== $bundle['Languages']) { + $languageNames = iterator_to_array($bundle['Languages']); + + $writer->write($tempDir.'/txt', $locale, array( + 'Version' => $bundle['Version'], + 'Languages' => $languageNames, + )); + + $languages = array_merge($languages, array_keys($languageNames)); + } + } + + $languages = array_unique($languages); + sort($languages); + + $root = $reader->read($tempDir.'/res', 'root'); + + // Read the metadata bundle with the language aliases + $metadata = $reader->read($tempDir.'/res', 'metadata'); + + // Create the mapping from two-letter to three-letter codes + $aliases = $metadata['languageAlias']; + $alpha2ToAlpha3 = array(); + + foreach ($aliases as $alias => $language) { + if (2 === strlen($language) && 3 === strlen($alias)) { + if (isset(self::$preferredAlpha2ToAlpha3Mapping[$language])) { + // Validate to prevent typos + if (!isset($aliases[self::$preferredAlpha2ToAlpha3Mapping[$language]])) { + throw new RuntimeException( + 'The statically set three-letter mapping '. + self::$preferredAlpha2ToAlpha3Mapping[$language].' '. + 'for the language code '.$language.' seems to be '. + 'invalid. Typo?' + ); + } + + $alpha3 = self::$preferredAlpha2ToAlpha3Mapping[$language]; + + if ($language !== $aliases[$alpha3]) { + throw new RuntimeException( + 'The statically set three-letter mapping '.$alpha3.' '. + 'for the language code '.$language.' seems to be '. + 'an alias for '.$aliases[$alpha3].'. 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 '. + 'property $preferredAlpha2ToAlpha3Mapping.' + ); + } else { + $alpha2ToAlpha3[$language] = $alias; + } + } + } + + // Create root file with all available locales + $writer = new TextBundleWriter(); + $writer->write($tempDir.'/txt', 'root', array( + 'Version' => $root['Version'], + 'Languages' => array_combine($languages, $languages), + 'Aliases' => $metadata['languageAlias'], + 'Alpha2ToAlpha3' => $alpha2ToAlpha3, + )); + + return $tempDir.'/txt'; } /** * {@inheritdoc} */ - public function afterCompile(CompilationContextInterface $context) + public function afterCompile(CompilationContext $context) { + // Remove the temporary directory + $context->getFilesystem()->remove(sys_get_temp_dir().'/icu-data-languages'); } /** * {@inheritdoc} */ - public function beforeCreateStub(StubbingContextInterface $context) + public function beforeCreateStub(StubbingContext $context) { return array( - 'Languages' => Intl::getLanguageBundle()->getLanguageNames('en'), - 'Scripts' => Intl::getLanguageBundle()->getScriptNames('en'), + 'Languages' => $this->languageBundle->getLanguageNames('en'), ); } /** * {@inheritdoc} */ - public function afterCreateStub(StubbingContextInterface $context) + public function afterCreateStub(StubbingContext $context) { } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php index b2576d6eec826..4890524c2e77f 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/LocaleBundleTransformationRule.php @@ -11,10 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; -use Symfony\Component\Intl\Exception\RuntimeException; -use Symfony\Component\Intl\Intl; -use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; -use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; +use Symfony\Component\Intl\Locale; +use Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface; +use Symfony\Component\Intl\ResourceBundle\LocaleBundleInterface; +use Symfony\Component\Intl\ResourceBundle\RegionBundleInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext; use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; /** @@ -25,19 +29,25 @@ class LocaleBundleTransformationRule implements TransformationRuleInterface { /** - * @var \Symfony\Component\Intl\ResourceBundle\LanguageBundleInterface + * @var LocaleBundleInterface + */ + private $localeBundle; + + /** + * @var LanguageBundleInterface */ private $languageBundle; /** - * @var \Symfony\Component\Intl\ResourceBundle\RegionBundleInterface + * @var RegionBundleInterface */ private $regionBundle; - public function __construct() + public function __construct(LocaleBundleInterface $localeBundle, LanguageBundleInterface $languageBundle, RegionBundleInterface $regionBundle) { - $this->languageBundle = Intl::getLanguageBundle(); - $this->regionBundle = Intl::getRegionBundle(); + $this->localeBundle = $localeBundle; + $this->languageBundle = $languageBundle; + $this->regionBundle = $regionBundle; } /** @@ -51,14 +61,31 @@ public function getBundleName() /** * {@inheritdoc} */ - public function beforeCompile(CompilationContextInterface $context) + public function beforeCompile(CompilationContext $context) { - $tempDir = sys_get_temp_dir() . '/icu-data-locales'; + $tempDir = sys_get_temp_dir().'/icu-data-locales'; $context->getFilesystem()->remove($tempDir); $context->getFilesystem()->mkdir($tempDir); - $this->generateTextFiles($tempDir, $this->scanLocales($context)); + $locales = $context->getLocaleScanner()->scanLocales($context->getSourceDir().'/locales'); + $aliases = $context->getLocaleScanner()->scanAliases($context->getSourceDir().'/locales'); + + $writer = new TextBundleWriter(); + + $this->generateTextFiles($writer, $tempDir, $locales, $aliases); + + // Generate aliases, needed to enable proper fallback from alias to its + // target + foreach ($aliases as $alias => $aliasOf) { + $writer->write($tempDir, $alias, array('%%ALIAS' => $aliasOf)); + } + + // Create root file which maps locale codes to locale codes, for fallback + $writer->write($tempDir, 'root', array( + 'Locales' => array_combine($locales, $locales), + 'Aliases' => $aliases, + )); return $tempDir; } @@ -66,115 +93,84 @@ public function beforeCompile(CompilationContextInterface $context) /** * {@inheritdoc} */ - public function afterCompile(CompilationContextInterface $context) + public function afterCompile(CompilationContext $context) { - $context->getFilesystem()->remove(sys_get_temp_dir() . '/icu-data-locales'); + $context->getFilesystem()->remove(sys_get_temp_dir().'/icu-data-locales'); } /** * {@inheritdoc} */ - public function beforeCreateStub(StubbingContextInterface $context) + public function beforeCreateStub(StubbingContext $context) { return array( - 'Locales' => Intl::getLocaleBundle()->getLocaleNames('en'), + 'Locales' => $this->localeBundle->getLocaleNames('en'), ); } /** * {@inheritdoc} */ - public function afterCreateStub(StubbingContextInterface $context) + public function afterCreateStub(StubbingContext $context) { } - private function scanLocales(CompilationContextInterface $context) + private function generateTextFiles(TextBundleWriter $writer, $targetDirectory, array $locales, array $aliases) { - $tempDir = sys_get_temp_dir() . '/icu-data-locales-source'; - - $context->getFilesystem()->remove($tempDir); - $context->getFilesystem()->mkdir($tempDir); - - // Temporarily generate the resource bundles - $context->getCompiler()->compile($context->getSourceDir() . '/locales', $tempDir); - - // Discover the list of supported locales, which are the names of the resource - // bundles in the "locales" directory - $locales = glob($tempDir . '/*.res'); - - // Remove file extension and sort - array_walk($locales, function (&$locale) { $locale = basename($locale, '.res'); }); - sort($locales); - - // Delete unneeded locales - foreach ($locales as $key => $locale) { - // Delete all aliases from the list - // i.e., "az_AZ" is an alias for "az_Latn_AZ" - $content = file_get_contents($context->getSourceDir() . '/locales/' . $locale . '.txt'); - - // The key "%%ALIAS" is not accessible through the \ResourceBundle class, - // so look in the original .txt file instead - if (strpos($content, '%%ALIAS') !== false) { - unset($locales[$key]); - } - - // Delete locales that have no content (i.e. only "Version" key) - $bundle = new \ResourceBundle($locale, $tempDir); - - if (null === $bundle) { - throw new RuntimeException('The resource bundle for locale ' . $locale . ' could not be loaded from directory ' . $tempDir); - } - - // There seems to be no other way for identifying all keys in this specific - // resource bundle - if (array_keys(iterator_to_array($bundle)) === array('Version')) { - unset($locales[$key]); - } - } - - $context->getFilesystem()->remove($tempDir); - - return $locales; - } - - private function generateTextFiles($targetDirectory, array $locales) - { - $displayLocales = array_unique(array_merge( - $this->languageBundle->getLocales(), - $this->regionBundle->getLocales() - )); - - $txtWriter = new TextBundleWriter(); - - // Generate a list of locale names 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. - foreach ($displayLocales as $displayLocale) { - // Don't include ICU's root resource bundle - if ('root' === $displayLocale) { - continue; + // Flip to facilitate lookup + $locales = array_flip($locales); + + // Don't generate names for aliases (names will be generated for the + // locale they are duplicating) + $displayLocales = array_diff_key($locales, $aliases); + + // Since fallbacks are always shorter than their source, we can sort + // the display locales so that fallbacks are always processed before + // their variants + ksort($displayLocales); + + // Generate a list of (existing) locale fallbacks + $fallbackMapping = $this->generateFallbackMapping($displayLocales, $aliases); + + $localeNames = array(); + + // Generate locale names for all locales that have translations in + // at least the language or the region bundle + foreach ($displayLocales as $displayLocale => $_) { + $localeNames[$displayLocale] = array(); + + foreach ($locales as $locale => $__) { + 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. + if (null !== ($name = $this->generateLocaleName($locale, $displayLocale))) { + $localeNames[$displayLocale][$locale] = $name; + } + } catch (MissingResourceException $e) { + } catch (ResourceBundleNotFoundException $e) { + } } - $names = array(); - - foreach ($locales as $locale) { - // Don't include ICU's root resource bundle - if ($locale === 'root') { - continue; - } + // Compare names with the names of the fallback locales and only + // keep the differences + $fallback = $displayLocale; - if (null !== ($name = $this->generateLocaleName($locale, $displayLocale))) { - $names[$locale] = $name; - } + while (isset($fallbackMapping[$fallback])) { + $fallback = $fallbackMapping[$fallback]; + $localeNames[$displayLocale] = array_diff( + $localeNames[$displayLocale], + $localeNames[$fallback] + ); } - // If no names could be generated for the current locale, skip it - if (0 === count($names)) { + // If no names remain to be saved for the current locale, skip it + if (0 === count($localeNames[$displayLocale])) { continue; } - $txtWriter->write($targetDirectory, $displayLocale, array('Locales' => $names)); + $writer->write($targetDirectory, $displayLocale, array('Locales' => $localeNames[$displayLocale])); } } @@ -248,4 +244,29 @@ private function generateLocaleName($locale, $displayLocale) return $name; } + + private function generateFallbackMapping(array $displayLocales, array $aliases) + { + $mapping = array(); + + foreach ($displayLocales as $displayLocale => $_) { + $mapping[$displayLocale] = null; + $fallback = $displayLocale; + + // Recursively search for a fallback locale until one is found + while (null !== ($fallback = Locale::getFallback($fallback))) { + // Currently, no locale has an alias as fallback locale. + // If this starts to be the case, we need to add code here. + assert(!isset($aliases[$fallback])); + + // Check whether the fallback exists + if (isset($displayLocales[$fallback])) { + $mapping[$displayLocale] = $fallback; + break; + } + } + } + + return $mapping; + } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php index 52fdbed8c3384..5b34763216088 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/RegionBundleTransformationRule.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; use Symfony\Component\Intl\Intl; -use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; -use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\RegionBundleInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext; +use Symfony\Component\Intl\ResourceBundle\Writer\TextBundleWriter; use Symfony\Component\Intl\Util\IcuVersion; /** @@ -23,6 +26,16 @@ */ class RegionBundleTransformationRule implements TransformationRuleInterface { + /** + * @var RegionBundleInterface + */ + private $regionBundle; + + public function __construct(RegionBundleInterface $regionBundle) + { + $this->regionBundle = $regionBundle; + } + /** * {@inheritdoc} */ @@ -34,37 +47,79 @@ public function getBundleName() /** * {@inheritdoc} */ - public function beforeCompile(CompilationContextInterface $context) + public function beforeCompile(CompilationContext $context) { + $tempDir = sys_get_temp_dir().'/icu-data-regions'; + // The region data is contained in the locales bundle in ICU <= 4.2 if (IcuVersion::compare($context->getIcuVersion(), '4.2', '<=', 1)) { - return $context->getSourceDir() . '/locales'; + $sourceDir = $context->getSourceDir() . '/locales'; + } else { + $sourceDir = $context->getSourceDir() . '/region'; } - return $context->getSourceDir() . '/region'; + $context->getFilesystem()->remove($tempDir); + $context->getFilesystem()->mkdir(array($tempDir, $tempDir.'/res')); + $context->getFilesystem()->mirror($sourceDir, $tempDir.'/txt'); + + $context->getCompiler()->compile($tempDir.'/txt', $tempDir.'/res'); + + $meta = array( + 'AvailableLocales' => $context->getLocaleScanner()->scanLocales($tempDir.'/txt'), + 'Countries' => array(), + ); + + $reader = new BinaryBundleReader(); + + // Collect complete list of countries in all locales + foreach ($meta['AvailableLocales'] as $locale) { + $bundle = $reader->read($tempDir.'/res', $locale); + + // isset() on \ResourceBundle returns true even if the value is null + if (isset($bundle['Countries']) && null !== $bundle['Countries']) { + $meta['Countries'] = array_merge( + $meta['Countries'], + array_keys(iterator_to_array($bundle['Countries'])) + ); + } + } + + $meta['Countries'] = array_unique($meta['Countries']); + $meta['Countries'] = array_filter($meta['Countries'], function ($country) { + return !ctype_digit((string) $country); + }); + sort($meta['Countries']); + + // Create meta file with all available locales + $writer = new TextBundleWriter(); + $writer->write($tempDir.'/txt', 'meta', $meta, false); + + return $tempDir.'/txt'; } /** * {@inheritdoc} */ - public function afterCompile(CompilationContextInterface $context) + public function afterCompile(CompilationContext $context) { + // Remove the temporary directory + $context->getFilesystem()->remove(sys_get_temp_dir().'/icu-data-regions'); } /** * {@inheritdoc} */ - public function beforeCreateStub(StubbingContextInterface $context) + public function beforeCreateStub(StubbingContext $context) { return array( - 'Countries' => Intl::getRegionBundle()->getCountryNames('en'), + 'Countries' => $this->regionBundle->getCountryNames('en'), ); } /** * {@inheritdoc} */ - public function afterCreateStub(StubbingContextInterface $context) + public function afterCreateStub(StubbingContext $context) { } } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php index 3965e0d2b7e04..fe213e0f40624 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/Rule/TransformationRuleInterface.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Intl\ResourceBundle\Transformer\Rule; -use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContextInterface; -use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContextInterface; +use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; +use Symfony\Component\Intl\ResourceBundle\Transformer\StubbingContext; /** * Contains instruction for compiling a resource bundle. @@ -32,39 +32,39 @@ public function getBundleName(); * Runs instructions to be executed before compiling the sources of the * resource bundle. * - * @param CompilationContextInterface $context The contextual information of + * @param CompilationContext $context The contextual information of * the compilation. * * @return string[] The source directories/files of the bundle. */ - public function beforeCompile(CompilationContextInterface $context); + public function beforeCompile(CompilationContext $context); /** * Runs instructions to be executed after compiling the sources of the * resource bundle. * - * @param CompilationContextInterface $context The contextual information of + * @param CompilationContext $context The contextual information of * the compilation. */ - public function afterCompile(CompilationContextInterface $context); + public function afterCompile(CompilationContext $context); /** * Runs instructions to be executed before creating the stub version of the * resource bundle. * - * @param StubbingContextInterface $context The contextual information of + * @param StubbingContext $context The contextual information of * the compilation. * * @return mixed The data to include in the stub version. */ - public function beforeCreateStub(StubbingContextInterface $context); + public function beforeCreateStub(StubbingContext $context); /** * Runs instructions to be executed after creating the stub version of the * resource bundle. * - * @param StubbingContextInterface $context The contextual information of + * @param StubbingContext $context The contextual information of * the compilation. */ - public function afterCreateStub(StubbingContextInterface $context); + public function afterCreateStub(StubbingContext $context); } diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php index 25ab68dbfc9a2..ab9940097ee6a 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContext.php @@ -14,9 +14,11 @@ use Symfony\Component\Filesystem\Filesystem; /** + * Stores contextual information for resource bundle stub creation. + * * @author Bernhard Schussek */ -class StubbingContext implements StubbingContextInterface +class StubbingContext { /** * @var string @@ -47,7 +49,9 @@ public function __construct($binaryDir, $stubDir, Filesystem $filesystem, $icuVe } /** - * {@inheritdoc} + * Returns the directory where the binary resource bundles are stored. + * + * @return string An absolute path to a directory. */ public function getBinaryDir() { @@ -55,7 +59,9 @@ public function getBinaryDir() } /** - * {@inheritdoc} + * Returns the directory where the stub resource bundles are stored. + * + * @return string An absolute path to a directory. */ public function getStubDir() { @@ -63,7 +69,9 @@ public function getStubDir() } /** - * {@inheritdoc} + * Returns a tool for manipulating the filesystem. + * + * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. */ public function getFilesystem() { @@ -71,7 +79,9 @@ public function getFilesystem() } /** - * {@inheritdoc} + * Returns the ICU version of the bundles being converted. + * + * @return string The ICU version string. */ public function getIcuVersion() { diff --git a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php b/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php deleted file mode 100644 index dc49255620fff..0000000000000 --- a/src/Symfony/Component/Intl/ResourceBundle/Transformer/StubbingContextInterface.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\ResourceBundle\Transformer; - -/** - * @author Bernhard Schussek - */ -interface StubbingContextInterface -{ - /** - * Returns the directory where the binary resource bundles are stored. - * - * @return string An absolute path to a directory. - */ - public function getBinaryDir(); - - /** - * Returns the directory where the stub resource bundles are stored. - * - * @return string An absolute path to a directory. - */ - public function getStubDir(); - - /** - * Returns a tool for manipulating the filesystem. - * - * @return \Symfony\Component\Filesystem\Filesystem The filesystem manipulator. - */ - public function getFilesystem(); - - /** - * Returns the ICU version of the bundles being converted. - * - * @return string The ICU version string. - */ - public function getIcuVersion(); -} diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php b/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php index 9a4cccb461145..6bec8781e8e88 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/ArrayAccessibleResourceBundle.php @@ -30,16 +30,16 @@ public function __construct(\ResourceBundle $bundleImpl) $this->bundleImpl = $bundleImpl; } - public function get($offset, $fallback = null) + public function get($offset) { - $value = $this->bundleImpl->get($offset, $fallback); + $value = $this->bundleImpl->get($offset); return $value instanceof \ResourceBundle ? new static($value) : $value; } public function offsetExists($offset) { - return null !== $this->bundleImpl[$offset]; + return null !== $this->bundleImpl->get($offset); } public function offsetGet($offset) diff --git a/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php index e1feaa2ce0551..ece5ec29a96c4 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Util/RecursiveArrayAccess.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Intl\ResourceBundle\Util; +use Symfony\Component\Intl\Exception\OutOfBoundsException; + /** * @author Bernhard Schussek */ @@ -19,11 +21,16 @@ class RecursiveArrayAccess public static function get($array, array $indices) { foreach ($indices as $index) { - if (!$array instanceof \ArrayAccess && !is_array($array)) { - return null; + // Use array_key_exists() for arrays, isset() otherwise + if (is_array($array) && !array_key_exists($index, $array) || !is_array($array) && !isset($array[$index])) { + throw new OutOfBoundsException('The index '.$index.' does not exist.'); } - $array = $array[$index]; + if ($array instanceof \ArrayAccess) { + $array = $array->offsetGet($index); + } else { + $array = $array[$index]; + } } return $array; diff --git a/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php b/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php index 342ee2dc5cd1c..eb825af4d7426 100644 --- a/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php +++ b/src/Symfony/Component/Intl/ResourceBundle/Writer/TextBundleWriter.php @@ -11,11 +11,14 @@ namespace Symfony\Component\Intl\ResourceBundle\Writer; +use Symfony\Component\Intl\Exception\UnexpectedTypeException; + /** * Writes .txt resource bundles. * - * The resulting files can be converted to binary .res files using the - * {@link \Symfony\Component\Intl\ResourceBundle\Transformer\BundleCompiler}. + * The resulting files can be converted to binary .res files using a + * {@link \Symfony\Component\Intl\ResourceBundle\Transformer\BundleCompilerInterface} + * implementation. * * @author Bernhard Schussek * @@ -26,11 +29,11 @@ class TextBundleWriter implements BundleWriterInterface /** * {@inheritdoc} */ - public function write($path, $locale, $data) + public function write($path, $locale, $data, $fallback = true) { $file = fopen($path.'/'.$locale.'.txt', 'w'); - $this->writeResourceBundle($file, $locale, $data); + $this->writeResourceBundle($file, $locale, $data, $fallback); fclose($file); } @@ -41,14 +44,16 @@ public function write($path, $locale, $data) * @param resource $file The file handle to write to. * @param string $bundleName The name of the bundle. * @param mixed $value The value of the node. + * @param Boolean $fallback Whether the resource bundle should be merged + * with the fallback locale. * * @see http://source.icu-project.org/repos/icu/icuhtml/trunk/design/bnf_rb.txt */ - private function writeResourceBundle($file, $bundleName, $value) + private function writeResourceBundle($file, $bundleName, $value, $fallback) { fwrite($file, $bundleName); - $this->writeTable($file, $value, 0); + $this->writeTable($file, $value, 0, $fallback); fwrite($file, "\n"); } @@ -72,16 +77,25 @@ private function writeResource($file, $value, $indentation, $requireBraces = tru return; } + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + if (is_array($value)) { - if (count($value) === count(array_filter($value, 'is_int'))) { + $intValues = count($value) === count(array_filter($value, 'is_int')); + + $keys = array_keys($value); + $intKeys = count($value) === count(array_filter($keys, 'is_int')) && + // check that the keys are 0-indexed and ascending + array_sum($keys) === array_sum(range(0, count($keys)-1)); + + if ($intValues && $intKeys) { $this->writeIntVector($file, $value, $indentation); return; } - $keys = array_keys($value); - - if (count($keys) === count(array_filter($keys, 'is_int'))) { + if ($intKeys) { $this->writeArray($file, $value, $indentation); return; @@ -180,16 +194,35 @@ private function writeArray($file, array $value, $indentation) /** * Writes a "table" node. * - * @param resource $file The file handle to write to. - * @param array $value The value of the node. - * @param integer $indentation The number of levels to indent. + * @param resource $file The file handle to write to. + * @param array|\Traversable $value The value of the node. + * @param integer $indentation The number of levels to indent. + * @param Boolean $fallback Whether the table should be merged + * with the fallback locale. + * + * @throws UnexpectedTypeException When $value is not an array and not a + * \Traversable instance. */ - private function writeTable($file, array $value, $indentation) + private function writeTable($file, $value, $indentation, $fallback = true) { + if (!is_array($value) && !$value instanceof \Traversable) { + throw new UnexpectedTypeException($value, 'array or \Traversable'); + } + + if (!$fallback) { + fwrite($file, ":table(nofallback)"); + } + fwrite($file, "{\n"); foreach ($value as $key => $entry) { fwrite($file, str_repeat(' ', $indentation + 1)); + + // escape colons, otherwise they are interpreted as resource types + if (false !== strpos($key, ':') || false !== strpos($key, ' ')) { + $key = '"'.$key.'"'; + } + fwrite($file, $key); $this->writeResource($file, $entry, $indentation + 1); diff --git a/src/Symfony/Component/Intl/Resources/bin/common.php b/src/Symfony/Component/Intl/Resources/bin/common.php index 4fadbe82336b5..ca86f31f16b2c 100644 --- a/src/Symfony/Component/Intl/Resources/bin/common.php +++ b/src/Symfony/Component/Intl/Resources/bin/common.php @@ -67,3 +67,27 @@ function get_icu_version_from_genrb($genrb) return $matches[1]; } + +set_exception_handler(function (\Exception $exception) { + echo "\n"; + + $cause = $exception; + $root = true; + + while (null !== $cause) { + if (!$root) { + echo "Caused by\n"; + } + + echo get_class($cause).": ".$cause->getMessage()."\n"; + echo "\n"; + echo $cause->getFile().":".$cause->getLine()."\n"; + foreach ($cause->getTrace() as $trace) { + echo $trace['file'].":".$trace['line']."\n"; + } + echo "\n"; + + $cause = $cause->getPrevious(); + $root = false; + } +}); diff --git a/src/Symfony/Component/Intl/Resources/bin/create-stubs.php b/src/Symfony/Component/Intl/Resources/bin/create-stubs.php index d330d6b5fb5cf..b21599252a4f8 100644 --- a/src/Symfony/Component/Intl/Resources/bin/create-stubs.php +++ b/src/Symfony/Component/Intl/Resources/bin/create-stubs.php @@ -10,8 +10,14 @@ */ use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Icu\IcuCurrencyBundle; use Symfony\Component\Icu\IcuData; +use Symfony\Component\Icu\IcuLanguageBundle; +use Symfony\Component\Icu\IcuLocaleBundle; +use Symfony\Component\Icu\IcuRegionBundle; use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; use Symfony\Component\Intl\ResourceBundle\Transformer\BundleTransformer; use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\CurrencyBundleTransformationRule; use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\LanguageBundleTransformationRule; @@ -87,11 +93,22 @@ $icuVersionInIcuComponent ); +$reader = new StructuredBundleReader(new BinaryBundleReader()); + +$localeBundle = new IcuLocaleBundle($reader); +$languageBundle = new IcuLanguageBundle($reader); +$regionBundle = new IcuRegionBundle($reader); +$currencyBundle = new IcuCurrencyBundle($reader); + +// Make sure that the lookup of fallback locales follows locale aliases +// correctly (see setLocaleAliases()) +$reader->setLocaleAliases($localeBundle->getLocaleAliases()); + $transformer = new BundleTransformer(); -$transformer->addRule(new LanguageBundleTransformationRule()); -$transformer->addRule(new RegionBundleTransformationRule()); -$transformer->addRule(new CurrencyBundleTransformationRule()); -$transformer->addRule(new LocaleBundleTransformationRule()); +$transformer->addRule(new LanguageBundleTransformationRule($languageBundle)); +$transformer->addRule(new RegionBundleTransformationRule($regionBundle)); +$transformer->addRule(new CurrencyBundleTransformationRule($currencyBundle)); +$transformer->addRule(new LocaleBundleTransformationRule($localeBundle, $languageBundle, $regionBundle)); echo "Starting stub creation...\n"; diff --git a/src/Symfony/Component/Intl/Resources/bin/update-icu-component.php b/src/Symfony/Component/Intl/Resources/bin/update-icu-component.php index 2b94fe417fe43..213eeea70a6d2 100644 --- a/src/Symfony/Component/Intl/Resources/bin/update-icu-component.php +++ b/src/Symfony/Component/Intl/Resources/bin/update-icu-component.php @@ -9,9 +9,16 @@ * file that was distributed with this source code. */ +use Symfony\Component\Icu\IcuCurrencyBundle; use Symfony\Component\Icu\IcuData; +use Symfony\Component\Icu\IcuLanguageBundle; +use Symfony\Component\Icu\IcuLocaleBundle; +use Symfony\Component\Icu\IcuRegionBundle; use Symfony\Component\Intl\Intl; -use Symfony\Component\Intl\ResourceBundle\Compiler\BundleCompiler; +use Symfony\Component\Intl\ResourceBundle\Compiler\GenrbBundleCompiler; +use Symfony\Component\Intl\ResourceBundle\Reader\BinaryBundleReader; +use Symfony\Component\Intl\ResourceBundle\Reader\BundleEntryReader; +use Symfony\Component\Intl\ResourceBundle\Scanner\LocaleScanner; use Symfony\Component\Intl\ResourceBundle\Transformer\BundleTransformer; use Symfony\Component\Intl\ResourceBundle\Transformer\CompilationContext; use Symfony\Component\Intl\ResourceBundle\Transformer\Rule\CurrencyBundleTransformationRule; @@ -42,7 +49,7 @@ For running this script, the intl extension must be loaded and all vendors must have been installed through composer: - composer install --dev +composer install --dev MESSAGE ); @@ -170,15 +177,23 @@ $sourceDir . '/data', IcuData::getResourceDirectory(), $filesystem, - new BundleCompiler($genrb, $genrbEnv), - $icuVersionInDownload + new GenrbBundleCompiler($genrb, $genrbEnv), + $icuVersionInDownload, + new LocaleScanner() ); +$reader = new BundleEntryReader(new BinaryBundleReader()); + +$localeBundle = new IcuLocaleBundle($reader); +$languageBundle = new IcuLanguageBundle($reader); +$regionBundle = new IcuRegionBundle($reader); +$currencyBundle = new IcuCurrencyBundle($reader); + $transformer = new BundleTransformer(); -$transformer->addRule(new LanguageBundleTransformationRule()); -$transformer->addRule(new RegionBundleTransformationRule()); -$transformer->addRule(new CurrencyBundleTransformationRule()); -$transformer->addRule(new LocaleBundleTransformationRule()); +$transformer->addRule(new LanguageBundleTransformationRule($languageBundle)); +$transformer->addRule(new RegionBundleTransformationRule($regionBundle)); +$transformer->addRule(new CurrencyBundleTransformationRule($currencyBundle)); +$transformer->addRule(new LocaleBundleTransformationRule($localeBundle, $languageBundle, $regionBundle)); echo "Starting resource bundle compilation. This may take a while...\n"; diff --git a/src/Symfony/Component/Intl/Test/ConsistencyTestCase.php b/src/Symfony/Component/Intl/Test/ConsistencyTestCase.php new file mode 100644 index 0000000000000..0f123d658083e --- /dev/null +++ b/src/Symfony/Component/Intl/Test/ConsistencyTestCase.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Test; + +use Symfony\Component\Intl\Intl; + +/** + * @author Bernhard Schussek + */ +abstract class ConsistencyTestCase extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + \Locale::setDefault('en'); + } + + public function provideLocales() + { + return array_map( + function ($locale) { return array($locale); }, + $this->getLocales() + ); + } + + public function provideRootLocales() + { + return array_map( + function ($locale) { return array($locale); }, + $this->getRootLocales() + ); + } + + public function provideLocaleAliases() + { + return array_map( + function ($alias, $ofLocale) { return array($alias, $ofLocale); }, + array_keys($this->getLocaleAliases()), + $this->getLocaleAliases() + ); + } + + protected static function getLocales() + { + return array(); + } + + protected static function getRootLocales() + { + return array_filter(static::getLocales(), function ($locale) { + // no locales for which fallback is possible (e.g "en_GB") + return false === strpos($locale, '_'); + }); + } + + protected static function getLocaleAliases() + { + return array(); + } +} diff --git a/src/Symfony/Component/Intl/Test/LanguageBundleConsistencyTestCase.php b/src/Symfony/Component/Intl/Test/LanguageBundleConsistencyTestCase.php new file mode 100644 index 0000000000000..46ff99fc24637 --- /dev/null +++ b/src/Symfony/Component/Intl/Test/LanguageBundleConsistencyTestCase.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Test; + +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Locale; + +/** + * @author Bernhard Schussek + */ +abstract class LanguageBundleConsistencyTestCase extends ConsistencyTestCase +{ + protected static $localesWithoutTranslationForAnyLanguage = array(); + protected static $localesWithoutTranslationForThemselves = array(); + protected static $localesWithoutTranslationForLanguage = array(); + + protected static $localesWithoutTranslationForAnyScript = array(); + protected static $localesWithoutTranslationForScript = array(); + + public function provideLocalesWithScripts() + { + $localesWithoutScript = array_flip(static::$localesWithoutTranslationForAnyScript); + $aliasesWithoutScripts = array_intersect_assoc($this->getLocaleAliases(), static::$localesWithoutTranslationForAnyScript); + + // remove locales that have no "Scripts" block or are an alias to a locale + // without "Scripts" block + $locales = array_filter($this->getLocales(), function ($locale) use ($localesWithoutScript, $aliasesWithoutScripts) { + while (null !== $locale) { + if (isset($localesWithoutScript[$locale]) || isset($aliasesWithoutScripts[$locale])) { + return false; + } + + $locale = Locale::getFallback($locale); + } + + return true; + }); + + return array_map( + function ($locale) { return array($locale); }, + $locales + ); + } + + public function provideLocaleAliasesWithScripts() + { + $localesWithoutScript = array_flip(static::$localesWithoutTranslationForAnyScript); + + // Remove aliases that point to a locale without "Scripts" block + $aliases = array_filter($this->getLocaleAliases(), function ($targetLocale) use ($localesWithoutScript) { + while (null !== $targetLocale) { + if (isset($localesWithoutScript[$targetLocale])) { + return false; + } + + $targetLocale = Locale::getFallback($targetLocale); + } + + return true; + }); + + return array_map( + function ($alias, $ofLocale) { return array($alias, $ofLocale); }, + array_keys($aliases), + $aliases + ); + } + + public function testGetLanguageNames() + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getLanguageBundle()->getLanguageNames($displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForAnyLanguage, $untranslatedLocales); + } + + public function provideTestedLanguages() + { + return array_map( + function ($language) { return array($language); }, + array_keys(static::$localesWithoutTranslationForLanguage) + ); + } + + /** + * @dataProvider provideTestedLanguages + */ + public function testGetLanguageName($language) + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getLanguageBundle()->getLanguageName($language ?: $displayLocale, null, $displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, static::$localesWithoutTranslationForAnyLanguage, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForLanguage[$language], $untranslatedLocales); + } + + public function testGetScriptNames() + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getLanguageBundle()->getScriptNames($displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForAnyScript, $untranslatedLocales); + } + + public function provideTestedScripts() + { + return array_map( + function ($script) { return array($script); }, + array_keys(static::$localesWithoutTranslationForScript) + ); + } + + /** + * @dataProvider provideTestedScripts + */ + public function testGetScriptName($script) + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getLanguageBundle()->getScriptName($script, null, $displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, static::$localesWithoutTranslationForAnyScript, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForScript[$script], $untranslatedLocales); + } + + /** + * @dataProvider provideLocaleAliases + * @group locale-alias-based + */ + public function testGetLanguageNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + Intl::getLanguageBundle()->getLanguageNames($ofLocale), + Intl::getLanguageBundle()->getLanguageNames($alias) + ); + } + + /** + * @dataProvider provideLocaleAliasesWithScripts + * @group locale-alias-based + */ + public function testGetScriptNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + Intl::getLanguageBundle()->getScriptNames($ofLocale), + Intl::getLanguageBundle()->getScriptNames($alias) + ); + } + + /** + * @dataProvider provideLocales + */ + public function testGetLanguageNamesAndGetLanguageNameAreConsistent($displayLocale) + { + $names = Intl::getLanguageBundle()->getLanguageNames($displayLocale); + + foreach ($names as $language => $name) { + $this->assertSame($name, Intl::getLanguageBundle()->getLanguageName($language, null, $displayLocale)); + } + } + + /** + * @dataProvider provideLocalesWithScripts + */ + public function testGetScriptNamesAndGetScriptNameAreConsistent($displayLocale) + { + $names = Intl::getLanguageBundle()->getScriptNames($displayLocale); + + foreach ($names as $script => $name) { + $this->assertSame($name, Intl::getLanguageBundle()->getScriptName($script, null, $displayLocale)); + } + } +} diff --git a/src/Symfony/Component/Intl/Test/RegionBundleConsistencyTestCase.php b/src/Symfony/Component/Intl/Test/RegionBundleConsistencyTestCase.php new file mode 100644 index 0000000000000..17e0db28513a0 --- /dev/null +++ b/src/Symfony/Component/Intl/Test/RegionBundleConsistencyTestCase.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Test; + +use Symfony\Component\Intl\Exception\MissingResourceException; +use Symfony\Component\Intl\Intl; +use Symfony\Component\Intl\Locale; +use Symfony\Component\Intl\Test\ConsistencyTestCase; + +/** + * @author Bernhard Schussek + */ +abstract class RegionBundleConsistencyTestCase extends ConsistencyTestCase +{ + protected static $localesWithoutTranslationForAnyCountry = array(); + protected static $localesWithoutTranslationForCountry = array(); + + public function provideLocalesWithCountries() + { + $localesWithoutScript = array_flip(static::$localesWithoutTranslationForAnyCountry); + $aliasesWithoutCountries = array_intersect_assoc($this->getLocaleAliases(), static::$localesWithoutTranslationForAnyCountry); + + // remove locales that have no "Countries" block or are an alias to a locale + // without "Countries" block + $locales = array_filter($this->getLocales(), function ($locale) use ($localesWithoutScript, $aliasesWithoutCountries) { + while (null !== $locale) { + if (isset($localesWithoutScript[$locale]) || isset($aliasesWithoutCountries[$locale])) { + return false; + } + + $locale = Locale::getFallback($locale); + } + + return true; + }); + + return array_map( + function ($locale) { return array($locale); }, + $locales + ); + } + + public function provideLocaleAliasesWithCountries() + { + $localesWithoutScript = array_flip(static::$localesWithoutTranslationForAnyCountry); + + // Remove aliases that point to a locale without "Countries" block + $aliases = array_filter($this->getLocaleAliases(), function ($targetLocale) use ($localesWithoutScript) { + while (null !== $targetLocale) { + if (isset($localesWithoutScript[$targetLocale])) { + return false; + } + + $targetLocale = Locale::getFallback($targetLocale); + } + + return true; + }); + + return array_map( + function ($alias, $ofLocale) { return array($alias, $ofLocale); }, + array_keys($aliases), + $aliases + ); + } + + public function testGetCountryNames() + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getRegionBundle()->getCountryNames($displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForAnyCountry, $untranslatedLocales); + } + + public function provideTestedCountries() + { + return array_map( + function ($country) { return array($country); }, + array_keys(static::$localesWithoutTranslationForCountry) + ); + } + + /** + * @dataProvider provideTestedCountries + */ + public function testGetCountryName($country) + { + $translatedLocales = array(); + $rootLocales = $this->getRootLocales(); + + foreach ($rootLocales as $displayLocale) { + try { + Intl::getRegionBundle()->getCountryName($country, $displayLocale); + $translatedLocales[] = $displayLocale; + } catch (MissingResourceException $e) { + } + } + + $untranslatedLocales = array_diff($rootLocales, static::$localesWithoutTranslationForAnyCountry, $translatedLocales); + + sort($untranslatedLocales); + + $this->assertEquals(static::$localesWithoutTranslationForCountry[$country], $untranslatedLocales); + } + + /** + * @dataProvider provideLocaleAliasesWithCountries + * @group locale-alias-based + */ + public function testGetCountryNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + Intl::getRegionBundle()->getCountryNames($ofLocale), + Intl::getRegionBundle()->getCountryNames($alias) + ); + } + + /** + * @dataProvider provideLocalesWithCountries + */ + public function testGetCountryNamesAndGetCountryNameAreConsistent($displayLocale) + { + $names = Intl::getRegionBundle()->getCountryNames($displayLocale); + + foreach ($names as $country => $name) { + $this->assertSame($name, Intl::getRegionBundle()->getCountryName($country, $displayLocale)); + } + } +} diff --git a/src/Symfony/Component/Intl/Tests/CurrencyTest.php b/src/Symfony/Component/Intl/Tests/CurrencyTest.php new file mode 100644 index 0000000000000..c72e8578b0c25 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/CurrencyTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests; + +use Symfony\Component\Intl\Currency; + +/** + * @author Bernhard Schussek + */ +class CurrencyTest extends \PHPUnit_Framework_TestCase +{ + public function testGetSymbol() + { + $this->assertSame('€', Currency::getSymbol('EUR', 'en')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetSymbolFailsOnInvalidCurrency() + { + Currency::getSymbol('FOO'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetSymbolFailsOnInvalidDisplayLocale() + { + Currency::getSymbol('EUR', 'foo'); + } + + public function testGetName() + { + $this->assertSame('Euro', Currency::getName('EUR', 'en')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidCurrency() + { + Currency::getName('FOO'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidDisplayLocale() + { + Currency::getName('EUR', 'foo'); + } + + public function testGetNames() + { + $names = Currency::getNames('en'); + + $this->assertArrayHasKey('EUR', $names); + $this->assertSame('Euro', $names['EUR']); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNamesFailsOnInvalidDisplayLocale() + { + Currency::getNames('foo'); + } + + public function testGetFractionDigits() + { + $this->assertSame(2, Currency::getFractionDigits('EUR')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetFractionDigitsFailsOnInvalidCurrency() + { + Currency::getFractionDigits('FOO'); + } + + public function testGetRoundingIncrement() + { + $this->assertSame(0, Currency::getRoundingIncrement('EUR')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetRoundingIncrementFailsOnInvalidCurrency() + { + Currency::getRoundingIncrement('FOO'); + } + + public function testGetNumericCode() + { + $this->assertSame(978, Currency::getNumericCode('EUR')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNumericCodeFailsOnInvalidCurrency() + { + Currency::getNumericCode('FOO'); + } + + public function testForNumericCode() + { + $this->assertSame(array('EUR'), Currency::forNumericCode(978)); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testForNumericCodeFailsOnInvalidNumber() + { + Currency::forNumericCode(12345); + } +} diff --git a/src/Symfony/Component/Intl/Tests/DataProvider/AbstractCurrencyDataProviderTest.php b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractCurrencyDataProviderTest.php new file mode 100644 index 0000000000000..24f4970766bb4 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractCurrencyDataProviderTest.php @@ -0,0 +1,213 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\DataProvider; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractCurrencyDataProviderTest extends AbstractDataProviderTest +{ + protected static $currencies = array(); + + protected static $alpha3ToNumeric = array(); + + /** + * @var \Symfony\Component\Icu\CurrencyDataProvider + */ + protected $dataProvider; + + protected function setUp() + { + $this->dataProvider = $this->createDataProvider(); + } + + abstract protected function createDataProvider(); + + public function provideCurrencies() + { + return array_map( + function ($currency) { return array($currency); }, + static::$currencies + ); + } + + public function testGetCurrencies() + { + $this->assertEquals(static::$currencies, $this->dataProvider->getCurrencies()); + } + + /** + * @dataProvider provideLocales + */ + public function testGetNames($displayLocale) + { + $names = $this->dataProvider->getNames($displayLocale); + + $keys = array_keys($names); + + sort($keys); + + $this->assertEquals(static::$currencies, $keys); + + // Names should be sorted + $sortedNames = $names; + $collator = new \Collator($displayLocale); + $collator->asort($names); + + $this->assertSame($sortedNames, $names); + } + + /** + * @dataProvider provideLocaleAliases + * @group locale-alias-based + */ + public function testGetNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + $this->dataProvider->getNames($ofLocale), + $this->dataProvider->getNames($alias) + ); + } + + /** + * @dataProvider provideLocales + */ + public function testGetName($displayLocale) + { + $names = $this->dataProvider->getNames($displayLocale); + + foreach ($names as $currency => $name) { + $this->assertSame($name, $this->dataProvider->getName($currency, $displayLocale)); + } + } + + /** + * @dataProvider provideLocales + */ + public function testGetSymbol($displayLocale) + { + $names = $this->dataProvider->getNames($displayLocale); + + foreach ($names as $currency => $name) { + $this->assertGreaterThan(0, mb_strlen($this->dataProvider->getSymbol($currency, $displayLocale))); + } + } + + /** + * @dataProvider provideCurrencies + */ + public function testGetFractionDigits($currency) + { + $this->assertTrue(is_numeric($this->dataProvider->getFractionDigits($currency))); + } + + /** + * @dataProvider provideCurrencies + */ + public function testGetRoundingIncrement($currency) + { + $this->assertTrue(is_numeric($this->dataProvider->getRoundingIncrement($currency))); + } + + public function provideCurrenciesWithNumericEquivalent() + { + return array_map( + function ($value) { return array($value); }, + array_keys(static::$alpha3ToNumeric) + ); + } + + /** + * @dataProvider provideCurrenciesWithNumericEquivalent + */ + public function testGetNumericCode($currency) + { + $this->assertSame(static::$alpha3ToNumeric[$currency], $this->dataProvider->getNumericCode($currency)); + } + + public function provideCurrenciesWithoutNumericEquivalent() + { + return array_map( + function ($value) { return array($value); }, + array_diff(static::$currencies, array_keys(static::$alpha3ToNumeric)) + ); + } + + /** + * @dataProvider provideCurrenciesWithoutNumericEquivalent + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testGetNumericCodeFailsIfNoNumericEquivalent($currency) + { + $this->dataProvider->getNumericCode($currency); + } + + public function provideValidNumericCodes() + { + $numericToAlpha3 = $this->getNumericToAlpha3Mapping(); + + return array_map( + function ($numeric, $alpha3) { return array($numeric, $alpha3); }, + array_keys($numericToAlpha3), + $numericToAlpha3 + ); + } + + /** + * @dataProvider provideValidNumericCodes + */ + public function testForNumericCode($numeric, $expected) + { + $actual = $this->dataProvider->forNumericCode($numeric); + + // Make sure that a different array order doesn't break the test + sort($actual); + sort($expected); + + $this->assertEquals($expected, $actual); + } + + public function provideInvalidNumericCodes() + { + $validNumericCodes = array_keys($this->getNumericToAlpha3Mapping()); + $invalidNumericCodes = array_diff(range(0, 1000), $validNumericCodes); + + return array_map( + function ($value) { return array($value); }, + $invalidNumericCodes + ); + } + + /** + * @dataProvider provideInvalidNumericCodes + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testForNumericCodeFailsIfInvalidNumericCode($currency) + { + $this->dataProvider->forNumericCode($currency); + } + + private function getNumericToAlpha3Mapping() + { + $numericToAlpha3 = array(); + + foreach (static::$alpha3ToNumeric as $alpha3 => $numeric) { + if (!isset($numericToAlpha3[$numeric])) { + $numericToAlpha3[$numeric] = array(); + } + + $numericToAlpha3[$numeric][] = $alpha3; + } + + return $numericToAlpha3; + } +} diff --git a/src/Symfony/Component/Intl/Tests/DataProvider/AbstractDataProviderTest.php b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractDataProviderTest.php new file mode 100644 index 0000000000000..c9e50d989c6f1 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractDataProviderTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\DataProvider; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractDataProviderTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + \Locale::setDefault('en'); + } + + public function provideLocales() + { + return array_map( + function ($locale) { return array($locale); }, + $this->getLocales() + ); + } + + public function provideLocaleAliases() + { + return array_map( + function ($alias, $ofLocale) { return array($alias, $ofLocale); }, + array_keys($this->getLocaleAliases()), + $this->getLocaleAliases() + ); + } + + protected static function getLocales() + { + return array(); + } + + protected static function getLocaleAliases() + { + return array(); + } +} diff --git a/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLanguageDataProviderTest.php b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLanguageDataProviderTest.php new file mode 100644 index 0000000000000..f7253785fc45d --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLanguageDataProviderTest.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\DataProvider; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractLanguageDataProviderTest extends AbstractDataProviderTest +{ + protected static $languages = array(); + protected static $alpha2ToAlpha3 = array(); + + /** + * @var \Symfony\Component\Icu\LanguageDataProvider + */ + protected $dataProvider; + + protected function setUp() + { + $this->dataProvider = $this->createDataProvider(); + } + + abstract protected function createDataProvider(); + + public function testGetLanguages() + { + $this->assertEquals(static::$languages, $this->dataProvider->getLanguages()); + } + + /** + * @dataProvider provideLocales + */ + public function testGetNames($displayLocale) + { + $languages = array_keys($this->dataProvider->getNames($displayLocale)); + + sort($languages); + + $this->assertEquals(static::$languages, $languages); + } + + /** + * @dataProvider provideLocaleAliases + * @group locale-alias-based + */ + public function testGetNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + $this->dataProvider->getNames($ofLocale), + $this->dataProvider->getNames($alias) + ); + } + + /** + * @dataProvider provideLocales + */ + public function testGetName($displayLocale) + { + $names = $this->dataProvider->getNames($displayLocale); + + foreach ($names as $language => $name) { + $this->assertSame($name, $this->dataProvider->getName($language, $displayLocale)); + } + } + + public function provideLanguagesWithAlpha3Equivalent() + { + return array_map( + function ($value) { return array($value); }, + array_keys(static::$alpha2ToAlpha3) + ); + } + + /** + * @dataProvider provideLanguagesWithAlpha3Equivalent + */ + public function testGetAlpha3Code($language) + { + $this->assertSame(static::$alpha2ToAlpha3[$language], $this->dataProvider->getAlpha3Code($language)); + } + + public function provideLanguagesWithoutAlpha3Equivalent() + { + return array_map( + function ($value) { return array($value); }, + array_diff(static::$languages, array_keys(static::$alpha2ToAlpha3)) + ); + } + + /** + * @dataProvider provideLanguagesWithoutAlpha3Equivalent + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testGetAlpha3CodeFailsIfNoAlpha3Equivalent($currency) + { + $this->dataProvider->getAlpha3Code($currency); + } +} diff --git a/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLocaleDataProviderTest.php b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLocaleDataProviderTest.php new file mode 100644 index 0000000000000..5dbeb46e1ccd7 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/DataProvider/AbstractLocaleDataProviderTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\DataProvider; + +/** + * @author Bernhard Schussek + */ +abstract class AbstractLocaleDataProviderTest extends AbstractDataProviderTest +{ + /** + * @var \Symfony\Component\Icu\LocaleDataProvider + */ + protected $dataProvider; + + protected function setUp() + { + $this->dataProvider = $this->createDataProvider(); + } + + abstract protected function createDataProvider(); + + public function testGetLocales() + { + $this->assertEquals($this->getLocales(), $this->dataProvider->getLocales()); + } + + public function testGetLocaleAliases() + { + $this->assertEquals($this->getLocaleAliases(), $this->dataProvider->getAliases()); + } + + /** + * @dataProvider provideLocales + */ + public function testGetNames($displayLocale) + { + $locales = array_keys($this->dataProvider->getNames($displayLocale)); + + sort($locales); + + $this->assertEquals($this->getLocales(), $locales); + } + + /** + * @dataProvider provideLocaleAliases + * @group locale-alias-based + */ + public function testGetNamesSupportsAliases($alias, $ofLocale) + { + $this->assertEquals( + $this->dataProvider->getNames($ofLocale), + $this->dataProvider->getNames($alias) + ); + } + + /** + * @dataProvider provideLocales + */ + public function testGetName($displayLocale) + { + $names = $this->dataProvider->getNames($displayLocale); + + foreach ($names as $locale => $name) { + $this->assertSame($name, $this->dataProvider->getName($locale, $displayLocale)); + } + } +} diff --git a/src/Symfony/Component/Intl/Tests/LanguageTest.php b/src/Symfony/Component/Intl/Tests/LanguageTest.php new file mode 100644 index 0000000000000..02eb9ea7c5c0b --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/LanguageTest.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests; + +use Symfony\Component\Intl\Language; +use Symfony\Component\Intl\Locale; + +/** + * @author Bernhard Schussek + */ +class LanguageTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + Locale::setDefault('en'); + } + + public function existsProvider() + { + return array( + array(true, 'de'), + array(true, 'de_AT'), + // scripts are not supported + array(false, 'de_Latn_AT'), + // different casing is not supported + array(false, 'De_AT'), + // hyphens are not supported + array(false, 'de-AT'), + // aliases with individual translations are supported + array(true, 'mo'), + // ISO 936-2 is not supported if an equivalent exists in ISO 936-1 + array(false, 'deu'), + array(false, 'deu_AT'), + // country aliases are not supported + array(false, 'de_AUT'), + ); + } + + /** + * @dataProvider existsProvider + */ + public function testExists($exists, $language) + { + $this->assertSame($exists, Language::exists($language)); + } + + public function canonicalizationProvider() + { + return array( + array('EN-GB', 'en_GB'), + array('De_at', 'de_AT'), + // Scripts in languages are not supported and are interpreted + // as custom additional subtags + array('IT_Latn_IT', 'it_LATN_IT'), + // Aliases are converted + array('DEU', 'de'), + array('deu-CH', 'de_CH'), + // Aliases with individual translations are not converted + array('mo', 'mo'), + // Country aliases are converted + // TODO uncomment once the Region class is implemented + //array('de_AUT', 'de_AT'), + ); + } + + /** + * @dataProvider canonicalizationProvider + */ + public function testCanonicalize($language, $canonicalized) + { + $this->assertSame($canonicalized, Language::canonicalize($language)); + } + + public function testGetName() + { + $this->assertSame('German', Language::getName('de', 'en')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidLanguage() + { + Language::getName('FOO'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidDisplayLocale() + { + Language::getName('de', 'foo'); + } + + public function testGetNames() + { + $names = Language::getNames('en'); + + $this->assertArrayHasKey('de', $names); + $this->assertSame('German', $names['de']); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNamesFailsOnInvalidDisplayLocale() + { + Language::getNames('foo'); + } +} diff --git a/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php b/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php index 2a5d5d2db710d..1eb12a96fadf1 100644 --- a/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php +++ b/src/Symfony/Component/Intl/Tests/Locale/LocaleTest.php @@ -61,9 +61,9 @@ public function testGetDisplayLanguage() /** * @expectedException \Symfony\Component\Intl\Exception\MethodNotImplementedException */ - public function testGetDisplayName() + public function testGetName() { - $this->call('getDisplayName', 'pt-Latn-BR', 'en'); + $this->call('getName', 'pt-Latn-BR', 'en'); } /** diff --git a/src/Symfony/Component/Intl/Tests/LocaleTest.php b/src/Symfony/Component/Intl/Tests/LocaleTest.php new file mode 100644 index 0000000000000..bd4858e253fc3 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/LocaleTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests; + +use Symfony\Component\Intl\Locale; + +/** + * @author Bernhard Schussek + */ +class LocaleTest extends \PHPUnit_Framework_TestCase +{ + protected function setUp() + { + Locale::setDefault('en'); + } + + public function existsProvider() + { + return array( + array(true, 'de'), + array(true, 'de_AT'), + // scripts are supported in some cases + array(true, 'zh_Hant_TW'), + // but not in others + array(false, 'de_Latn_AT'), + // different casing is not supported + array(false, 'De_AT'), + // hyphens are not supported + array(false, 'de-AT'), + // aliases with individual translations are supported + array(true, 'mo'), + // ISO 936-2 is not supported if an equivalent exists in ISO 936-1 + array(false, 'deu'), + array(false, 'deu_AT'), + // country aliases are not supported + array(false, 'de_AUT'), + ); + } + + /** + * @dataProvider existsProvider + */ + public function testExists($exists, $language) + { + $this->assertSame($exists, Locale::exists($language)); + } + + public function testGetName() + { + $this->assertSame('English', Locale::getName('en', 'en')); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidLocale() + { + Locale::getName('foo'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNameFailsOnInvalidDisplayLocale() + { + Locale::getName('en', 'foo'); + } + + public function testGetNames() + { + $names = Locale::getNames('en'); + + $this->assertArrayHasKey('en', $names); + $this->assertSame('English', $names['en']); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + */ + public function testGetNamesFailsOnInvalidDisplayLocale() + { + Locale::getNames('foo'); + } + + public function testGetFallback() + { + $this->assertSame('fr', Locale::getFallback('fr_FR')); + } + + public function testGetFallbackForTopLevelLocale() + { + $this->assertSame('root', Locale::getFallback('en')); + } + + public function testGetFallbackForRoot() + { + $this->assertNull(Locale::getFallback('root')); + } +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/AbstractBundleTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/AbstractBundleTest.php deleted file mode 100644 index 6b07586572935..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/AbstractBundleTest.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle; - -/** - * @author Bernhard Schussek - */ -class AbstractBundleTest extends \PHPUnit_Framework_TestCase -{ - const RES_DIR = '/base/dirName'; - - /** - * @var \Symfony\Component\Intl\ResourceBundle\AbstractBundle - */ - private $bundle; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - protected function setUp() - { - $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); - $this->bundle = $this->getMockForAbstractClass( - 'Symfony\Component\Intl\ResourceBundle\AbstractBundle', - array(self::RES_DIR, $this->reader) - ); - - $this->bundle->expects($this->any()) - ->method('getDirectoryName') - ->will($this->returnValue('dirName')); - } - - public function testGetLocales() - { - $locales = array('de', 'en', 'fr'); - - $this->reader->expects($this->once()) - ->method('getLocales') - ->with(self::RES_DIR) - ->will($this->returnValue($locales)); - - $this->assertSame($locales, $this->bundle->getLocales()); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/CurrencyBundleTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/CurrencyBundleTest.php deleted file mode 100644 index b66a6727bfab8..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/CurrencyBundleTest.php +++ /dev/null @@ -1,98 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle; - -use Symfony\Component\Intl\ResourceBundle\CurrencyBundle; - -/** - * @author Bernhard Schussek - */ -class CurrencyBundleTest extends \PHPUnit_Framework_TestCase -{ - const RES_DIR = '/base/curr'; - - /** - * @var CurrencyBundle - */ - private $bundle; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - protected function setUp() - { - $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); - $this->bundle = new CurrencyBundle(self::RES_DIR, $this->reader); - } - - public function testGetCurrencySymbol() - { - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 1)) - ->will($this->returnValue('€')); - - $this->assertSame('€', $this->bundle->getCurrencySymbol('EUR', 'en')); - } - - public function testGetCurrencyName() - { - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 0)) - ->will($this->returnValue('Euro')); - - $this->assertSame('Euro', $this->bundle->getCurrencyName('EUR', 'en')); - } - - public function testGetCurrencyNames() - { - $sortedCurrencies = array( - 'USD' => array(0 => 'Dollar'), - 'EUR' => array(0 => 'Euro'), - ); - - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Currencies')) - ->will($this->returnValue($sortedCurrencies)); - - $sortedNames = array( - 'USD' => 'Dollar', - 'EUR' => 'Euro', - ); - - $this->assertSame($sortedNames, $this->bundle->getCurrencyNames('en')); - } - - public function testGetFractionDigits() - { - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 2)) - ->will($this->returnValue(123)); - - $this->assertSame(123, $this->bundle->getFractionDigits('EUR')); - } - - public function testGetRoundingIncrement() - { - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Currencies', 'EUR', 3)) - ->will($this->returnValue(123)); - - $this->assertSame(123, $this->bundle->getRoundingIncrement('EUR')); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/LanguageBundleTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/LanguageBundleTest.php index 96031fc7c01f0..dc0ee2b7d7466 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/LanguageBundleTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/LanguageBundleTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Intl\Tests\ResourceBundle; +use Symfony\Component\Intl\Exception\MissingResourceException; use Symfony\Component\Intl\ResourceBundle\LanguageBundle; /** @@ -38,46 +39,35 @@ protected function setUp() public function testGetLanguageName() { - $languages = array( - 'de' => 'German', - 'en' => 'English', - ); - $this->reader->expects($this->once()) ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Languages')) - ->will($this->returnValue($languages)); + ->with(self::RES_DIR, 'en', array('Languages', 'de')) + ->will($this->returnValue('German')); $this->assertSame('German', $this->bundle->getLanguageName('de', null, 'en')); } public function testGetLanguageNameWithRegion() { - $languages = array( - 'de' => 'German', - 'en' => 'English', - 'en_GB' => 'British English', - ); - $this->reader->expects($this->once()) ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Languages')) - ->will($this->returnValue($languages)); + ->with(self::RES_DIR, 'en', array('Languages', 'en_GB')) + ->will($this->returnValue('British English')); $this->assertSame('British English', $this->bundle->getLanguageName('en', 'GB', 'en')); } public function testGetLanguageNameWithUntranslatedRegion() { - $languages = array( - 'de' => 'German', - 'en' => 'English', - ); + $this->reader->expects($this->at(0)) + ->method('readEntry') + ->with(self::RES_DIR, 'en', array('Languages', 'en_US')) + ->will($this->throwException(new MissingResourceException())); - $this->reader->expects($this->once()) + $this->reader->expects($this->at(1)) ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Languages')) - ->will($this->returnValue($languages)); + ->with(self::RES_DIR, 'en', array('Languages', 'en')) + ->will($this->returnValue('English')); $this->assertSame('English', $this->bundle->getLanguageName('en', 'US', 'en')); } @@ -99,85 +89,24 @@ public function testGetLanguageNames() public function testGetScriptName() { - $data = array( - 'Languages' => array( - 'de' => 'German', - 'en' => 'English', - ), - 'Scripts' => array( - 'Latn' => 'latin', - 'Cyrl' => 'cyrillique', - ), - ); - - $this->reader->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - $this->assertSame('latin', $this->bundle->getScriptName('Latn', null, 'en')); - } - - public function testGetScriptNameIncludedInLanguage() - { - $data = array( - 'Languages' => array( - 'de' => 'German', - 'en' => 'English', - 'zh_Hans' => 'Simplified Chinese', - ), - 'Scripts' => array( - 'Latn' => 'latin', - 'Cyrl' => 'cyrillique', - ), - ); - - $this->reader->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - // Null because the script is included in the language anyway - $this->assertNull($this->bundle->getScriptName('Hans', 'zh', 'en')); - } - - public function testGetScriptNameIncludedInLanguageInBraces() - { - $data = array( - 'Languages' => array( - 'de' => 'German', - 'en' => 'English', - 'zh_Hans' => 'Chinese (simplified)', - ), - 'Scripts' => array( - 'Latn' => 'latin', - 'Cyrl' => 'cyrillique', - ), - ); - $this->reader->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->method('readEntry') + ->with(self::RES_DIR, 'en', array('Scripts', 'Latn')) + ->will($this->returnValue('Latin')); - $this->assertSame('simplified', $this->bundle->getScriptName('Hans', 'zh', 'en')); + $this->assertSame('Latin', $this->bundle->getScriptName('Latn', null, 'en')); } - public function testGetScriptNameNoScriptsBlock() + public function testGetScriptNameIncludedInLanguageBC() { - $data = array( - 'Languages' => array( - 'de' => 'German', - 'en' => 'English', - ), - ); - $this->reader->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); + ->method('readEntry') + ->with(self::RES_DIR, 'en', array('Scripts', 'Latn')) + ->will($this->returnValue('Latin')); - $this->assertNull($this->bundle->getScriptName('Latn', null, 'en')); + // the second argument once was used, but is now ignored since it + // doesn't make a difference anyway + $this->assertSame('Latin', $this->bundle->getScriptName('Latn', 'zh', 'en')); } public function testGetScriptNames() diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/LocaleBundleTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/LocaleBundleTest.php deleted file mode 100644 index ddfdc3d2485ff..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/LocaleBundleTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle; - -use Symfony\Component\Intl\ResourceBundle\LocaleBundle; - -/** - * @author Bernhard Schussek - */ -class LocaleBundleTest extends \PHPUnit_Framework_TestCase -{ - const RES_DIR = '/base/locales'; - - /** - * @var LocaleBundle - */ - private $bundle; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - protected function setUp() - { - $this->reader = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); - $this->bundle = new LocaleBundle(self::RES_DIR, $this->reader); - } - - public function testGetLocaleName() - { - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Locales', 'de_AT')) - ->will($this->returnValue('German (Austria)')); - - $this->assertSame('German (Austria)', $this->bundle->getLocaleName('de_AT', 'en')); - } - - public function testGetLocaleNames() - { - $sortedLocales = array( - 'en_IE' => 'English (Ireland)', - 'en_GB' => 'English (United Kingdom)', - 'en_US' => 'English (United States)', - ); - - $this->reader->expects($this->once()) - ->method('readEntry') - ->with(self::RES_DIR, 'en', array('Locales')) - ->will($this->returnValue($sortedLocales)); - - $this->assertSame($sortedLocales, $this->bundle->getLocaleNames('en')); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php deleted file mode 100644 index 2da7f90de49e3..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/AbstractBundleReaderTest.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; - -use Symfony\Component\Filesystem\Filesystem; - -/** - * @author Bernhard Schussek - */ -class AbstractBundleReaderTest extends \PHPUnit_Framework_TestCase -{ - private $directory; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $reader; - - protected function setUp() - { - $this->directory = sys_get_temp_dir() . '/AbstractBundleReaderTest/' . rand(1000, 9999); - $this->filesystem = new Filesystem(); - $this->reader = $this->getMockForAbstractClass('Symfony\Component\Intl\ResourceBundle\Reader\AbstractBundleReader'); - - $this->filesystem->mkdir($this->directory); - } - - protected function tearDown() - { - $this->filesystem->remove($this->directory); - } - - public function testGetLocales() - { - $this->filesystem->touch($this->directory . '/en.foo'); - $this->filesystem->touch($this->directory . '/de.foo'); - $this->filesystem->touch($this->directory . '/fr.foo'); - $this->filesystem->touch($this->directory . '/bo.txt'); - $this->filesystem->touch($this->directory . '/gu.bin'); - $this->filesystem->touch($this->directory . '/s.lol'); - - $this->reader->expects($this->any()) - ->method('getFileExtension') - ->will($this->returnValue('foo')); - - $sortedLocales = array('de', 'en', 'fr'); - - $this->assertSame($sortedLocales, $this->reader->getLocales($this->directory)); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php index 3aefbae7fd911..526424d1a3d19 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BinaryBundleReaderTest.php @@ -33,19 +33,61 @@ protected function setUp() public function testReadReturnsArrayAccess() { - $data = $this->reader->read(__DIR__ . '/Fixtures', 'en'); + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'ro'); $this->assertInstanceOf('\ArrayAccess', $data); $this->assertSame('Bar', $data['Foo']); $this->assertFalse(isset($data['ExistsNot'])); } + public function testReadFollowsAlias() + { + // "alias" = "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'alias'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bar', $data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + + public function testReadDoesNotFollowFallback() + { + // "ro_MD" -> "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'ro_MD'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bam', $data['Baz']); + $this->assertFalse(isset($data['Foo'])); + $this->assertNull($data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + + public function testReadDoesNotFollowFallbackAlias() + { + // "mo" = "ro_MD" -> "ro" + $data = $this->reader->read(__DIR__.'/Fixtures/res', 'mo'); + + $this->assertInstanceOf('\ArrayAccess', $data); + $this->assertSame('Bam', $data['Baz'], 'data from the aliased locale can be accessed'); + $this->assertFalse(isset($data['Foo'])); + $this->assertNull($data['Foo']); + $this->assertFalse(isset($data['ExistsNot'])); + } + /** - * @expectedException \Symfony\Component\Intl\Exception\RuntimeException + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException */ public function testReadFailsIfNonExistingLocale() { - $this->reader->read(__DIR__ . '/Fixtures', 'foo'); + $this->reader->read(__DIR__.'/Fixtures/res', 'foo'); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException + */ + public function testReadFailsIfNonExistingFallbackLocale() + { + $this->reader->read(__DIR__.'/Fixtures/res', 'ro_AT'); } /** @@ -53,6 +95,6 @@ public function testReadFailsIfNonExistingLocale() */ public function testReadFailsIfNonExistingDirectory() { - $this->reader->read(__DIR__ . '/foo', 'en'); + $this->reader->read(__DIR__.'/foo', 'ro'); } } diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BundleEntryReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BundleEntryReaderTest.php new file mode 100644 index 0000000000000..b7f806cbbb78b --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/BundleEntryReaderTest.php @@ -0,0 +1,365 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; + +use Symfony\Component\Intl\Exception\ResourceBundleNotFoundException; +use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; + +/** + * @author Bernhard Schussek + */ +class BundleEntryReaderTest extends \PHPUnit_Framework_TestCase +{ + const RES_DIR = '/res/dir'; + + /** + * @var StructuredBundleReader + */ + private $reader; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $readerImpl; + + private static $data = array( + 'Entries' => array( + 'Foo' => 'Bar', + 'Bar' => 'Baz', + ), + 'Foo' => 'Bar', + 'Version' => '2.0', + ); + + private static $fallbackData = array( + 'Entries' => array( + 'Foo' => 'Foo', + 'Bam' => 'Lah', + ), + 'Baz' => 'Foo', + 'Version' => '1.0', + ); + + private static $mergedData = array( + // no recursive merging -> too complicated + 'Entries' => array( + 'Foo' => 'Bar', + 'Bar' => 'Baz', + ), + 'Baz' => 'Foo', + 'Version' => '2.0', + 'Foo' => 'Bar', + ); + + protected function setUp() + { + $this->readerImpl = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); + $this->reader = new StructuredBundleReader($this->readerImpl); + } + + public function testForwardCallToRead() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); + + $this->assertSame(self::$data, $this->reader->read(self::RES_DIR, 'root')); + } + + public function testReadEntireDataFileIfNoIndicesGiven() + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(self::$data)); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$fallbackData)); + + $this->assertSame(self::$mergedData, $this->reader->readEntry(self::RES_DIR, 'en', array())); + } + + public function testReadExistingEntry() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); + + $this->assertSame('Bar', $this->reader->readEntry(self::RES_DIR, 'root', array('Entries', 'Foo'))); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testReadNonExistingEntry() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(self::$data)); + + $this->reader->readEntry(self::RES_DIR, 'root', array('Entries', 'NonExisting')); + } + + public function testFallbackIfEntryDoesNotExist() + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(self::$data)); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(self::$fallbackData)); + + $this->assertSame('Lah', $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'))); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testDontFallbackIfEntryDoesNotExistAndFallbackDisabled() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(self::$data)); + + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'), false); + } + + public function testFallbackIfLocaleDoesNotExist() + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->throwException(new ResourceBundleNotFoundException())); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(self::$fallbackData)); + + $this->assertSame('Lah', $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'))); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testDontFallbackIfLocaleDoesNotExistAndFallbackDisabled() + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->throwException(new ResourceBundleNotFoundException())); + + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Entries', 'Bam'), false); + } + + public function provideMergeableValues() + { + return array( + array('foo', null, 'foo'), + array(null, 'foo', 'foo'), + array(array('foo', 'bar'), null, array('foo', 'bar')), + array(array('foo', 'bar'), array(), array('foo', 'bar')), + array(null, array('baz'), array('baz')), + array(array(), array('baz'), array('baz')), + array(array('foo', 'bar'), array('baz'), array('baz', 'foo', 'bar')), + ); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeDataWithFallbackData($childData, $parentData, $result) + { + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue($childData)); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue($parentData)); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue($childData)); + } + + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en', array(), true)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testDontMergeDataIfFallbackDisabled($childData, $parentData, $result) + { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue($childData)); + + $this->assertSame($childData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array(), false)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeExistingEntryWithExistingFallbackEntry($childData, $parentData, $result) + { + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'root') + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } + + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'), true)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeNonExistingEntryWithExistingFallbackEntry($childData, $parentData, $result) + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => 'Baz'))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + + $this->assertSame($parentData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeExistingEntryWithNonExistingFallbackEntry($childData, $parentData, $result) + { + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => 'Bar'))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } + + $this->assertSame($childData, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + } + + /** + * @expectedException \Symfony\Component\Intl\Exception\MissingResourceException + */ + public function testFailIfEntryFoundNeitherInParentNorChild() + { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => 'Baz'))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => 'Bar'))); + + $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testMergeTraversables($childData, $parentData, $result) + { + $parentData = is_array($parentData) ? new \ArrayObject($parentData) : $parentData; + $childData = is_array($childData) ? new \ArrayObject($childData) : $childData; + + if (null === $childData || $childData instanceof \ArrayObject) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'en') + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'en_GB') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } + + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); + } + + /** + * @dataProvider provideMergeableValues + */ + public function testFollowLocaleAliases($childData, $parentData, $result) + { + $this->reader->setLocaleAliases(array('mo' => 'ro_MD')); + + if (null === $childData || is_array($childData)) { + $this->readerImpl->expects($this->at(0)) + ->method('read') + ->with(self::RES_DIR, 'ro_MD') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + + // Read fallback locale of aliased locale ("ro_MD" -> "ro") + $this->readerImpl->expects($this->at(1)) + ->method('read') + ->with(self::RES_DIR, 'ro') + ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); + } else { + $this->readerImpl->expects($this->once()) + ->method('read') + ->with(self::RES_DIR, 'ro_MD') + ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); + } + + $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'mo', array('Foo', 'Bar'), true)); + } +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh new file mode 100755 index 0000000000000..50513e7a946c3 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +if [ -z "$ICU_BUILD_DIR" ]; then + echo "Please set the ICU_BUILD_DIR environment variable" + exit +fi + +if [ ! -d "$ICU_BUILD_DIR" ]; then + echo "The directory $ICU_BUILD_DIR pointed at by ICU_BUILD_DIR does not exist" + exit +fi + +DIR=`dirname $0` + +rm $DIR/res/*.res + +LD_LIBRARY_PATH=$ICU_BUILD_DIR/lib $ICU_BUILD_DIR/bin/genrb -d $DIR/res $DIR/txt/*.txt diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php similarity index 100% rename from src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.php rename to src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/php/en.php diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res new file mode 100644 index 0000000000000..4f0ab7eaa3166 Binary files /dev/null and b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/alias.res differ diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res new file mode 100644 index 0000000000000..3f8911a7317ed Binary files /dev/null and b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/mo.res differ diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro.res similarity index 100% rename from src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.res rename to src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro.res diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro_MD.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro_MD.res new file mode 100644 index 0000000000000..c8b0810ec6b48 Binary files /dev/null and b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/ro_MD.res differ diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/root.res b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/root.res new file mode 100644 index 0000000000000..81ba7eaedb0f0 Binary files /dev/null and b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/res/root.res differ diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/alias.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/alias.txt new file mode 100644 index 0000000000000..d6e216f4cbc08 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/alias.txt @@ -0,0 +1,3 @@ +alias{ + "%%ALIAS"{"ro"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/mo.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/mo.txt new file mode 100644 index 0000000000000..3ce23bcc639d5 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/mo.txt @@ -0,0 +1,3 @@ +mo{ + "%%ALIAS"{"ro_MD"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro.txt similarity index 80% rename from src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.txt rename to src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro.txt index c788e996acb1d..80d28889cf391 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/en.txt +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro.txt @@ -1,3 +1,3 @@ -en{ +ro{ Foo{"Bar"} } diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro_MD.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro_MD.txt new file mode 100644 index 0000000000000..fcbb3bc07d538 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/ro_MD.txt @@ -0,0 +1,3 @@ +ro_MD{ + Baz{"Bam"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/root.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/root.txt new file mode 100644 index 0000000000000..4d8265997f712 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/Fixtures/txt/root.txt @@ -0,0 +1,6 @@ +root{ + /** + * so genrb doesn't issue warnings + */ + ___{""} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/PhpBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/PhpBundleReaderTest.php index 2fee35599fd43..c3411e9de015c 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/PhpBundleReaderTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/PhpBundleReaderTest.php @@ -30,7 +30,7 @@ protected function setUp() public function testReadReturnsArray() { - $data = $this->reader->read(__DIR__ . '/Fixtures', 'en'); + $data = $this->reader->read(__DIR__ . '/Fixtures/php', 'en'); $this->assertTrue(is_array($data)); $this->assertSame('Bar', $data['Foo']); @@ -38,11 +38,11 @@ public function testReadReturnsArray() } /** - * @expectedException \Symfony\Component\Intl\Exception\InvalidArgumentException + * @expectedException \Symfony\Component\Intl\Exception\ResourceBundleNotFoundException */ - public function testReadFailsIfLocaleOtherThanEn() + public function testReadFailsIfNonExistingLocale() { - $this->reader->read(__DIR__ . '/Fixtures', 'foo'); + $this->reader->read(__DIR__ . '/Fixtures/php', 'foo'); } /** diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php deleted file mode 100644 index 600236eb3ec56..0000000000000 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Reader/StructuredBundleReaderTest.php +++ /dev/null @@ -1,223 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; - -use Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReader; - -/** - * @author Bernhard Schussek - */ -class StructuredBundleReaderTest extends \PHPUnit_Framework_TestCase -{ - const RES_DIR = '/res/dir'; - - /** - * @var StructuredBundleReader - */ - private $reader; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - private $readerImpl; - - protected function setUp() - { - $this->readerImpl = $this->getMock('Symfony\Component\Intl\ResourceBundle\Reader\StructuredBundleReaderInterface'); - $this->reader = new StructuredBundleReader($this->readerImpl); - } - - public function testGetLocales() - { - $locales = array('en', 'de', 'fr'); - - $this->readerImpl->expects($this->once()) - ->method('getLocales') - ->with(self::RES_DIR) - ->will($this->returnValue($locales)); - - $this->assertSame($locales, $this->reader->getLocales(self::RES_DIR)); - } - - public function testRead() - { - $data = array('foo', 'bar'); - - $this->readerImpl->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - $this->assertSame($data, $this->reader->read(self::RES_DIR, 'en')); - } - - public function testReadEntryNoParams() - { - $data = array('foo', 'bar'); - - $this->readerImpl->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - $this->assertSame($data, $this->reader->readEntry(self::RES_DIR, 'en', array())); - } - - public function testReadEntryWithParam() - { - $data = array('Foo' => array('Bar' => 'Baz')); - - $this->readerImpl->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - $this->assertSame('Baz', $this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'))); - } - - public function testReadEntryWithUnresolvablePath() - { - $data = array('Foo' => 'Baz'); - - $this->readerImpl->expects($this->once()) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($data)); - - $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en', array('Foo', 'Bar'))); - } - - public function readMergedEntryProvider() - { - return array( - array('foo', null, 'foo'), - array(null, 'foo', 'foo'), - array(array('foo', 'bar'), null, array('foo', 'bar')), - array(array('foo', 'bar'), array(), array('foo', 'bar')), - array(null, array('baz'), array('baz')), - array(array(), array('baz'), array('baz')), - array(array('foo', 'bar'), array('baz'), array('baz', 'foo', 'bar')), - ); - } - - /** - * @dataProvider readMergedEntryProvider - */ - public function testReadMergedEntryNoParams($childData, $parentData, $result) - { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue($childData)); - - if (null === $childData || is_array($childData)) { - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue($parentData)); - } - - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array(), true)); - } - - /** - * @dataProvider readMergedEntryProvider - */ - public function testReadMergedEntryWithParams($childData, $parentData, $result) - { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); - - if (null === $childData || is_array($childData)) { - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); - } - - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); - } - - public function testReadMergedEntryWithUnresolvablePath() - { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => 'Baz'))); - - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => 'Bar'))); - - $this->assertNull($this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); - } - - public function testReadMergedEntryWithUnresolvablePathInParent() - { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => array('three'))))); - - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => 'Bar'))); - - $result = array('three'); - - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); - } - - public function testReadMergedEntryWithUnresolvablePathInChild() - { - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => 'Baz'))); - - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => array('Bar' => array('one', 'two'))))); - - $result = array('one', 'two'); - - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); - } - - /** - * @dataProvider readMergedEntryProvider - */ - public function testReadMergedEntryWithTraversables($childData, $parentData, $result) - { - $parentData = is_array($parentData) ? new \ArrayObject($parentData) : $parentData; - $childData = is_array($childData) ? new \ArrayObject($childData) : $childData; - - $this->readerImpl->expects($this->at(0)) - ->method('read') - ->with(self::RES_DIR, 'en_GB') - ->will($this->returnValue(array('Foo' => array('Bar' => $childData)))); - - if (null === $childData || $childData instanceof \ArrayObject) { - $this->readerImpl->expects($this->at(1)) - ->method('read') - ->with(self::RES_DIR, 'en') - ->will($this->returnValue(array('Foo' => array('Bar' => $parentData)))); - } - - $this->assertSame($result, $this->reader->readEntry(self::RES_DIR, 'en_GB', array('Foo', 'Bar'), true)); - } -} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Scanner/LocaleScannerTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Scanner/LocaleScannerTest.php new file mode 100644 index 0000000000000..2896bbf43b34d --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Scanner/LocaleScannerTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Intl\Tests\ResourceBundle\Reader; + +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Intl\ResourceBundle\Scanner\LocaleScanner; + +/** + * @author Bernhard Schussek + */ +class LocaleScannerTest extends \PHPUnit_Framework_TestCase +{ + private $directory; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LocaleScanner + */ + private $scanner; + + protected function setUp() + { + $this->directory = sys_get_temp_dir().'/LocaleScannerTest/'.rand(1000, 9999); + $this->filesystem = new Filesystem(); + $this->scanner = new LocaleScanner(); + + $this->filesystem->mkdir($this->directory); + + $this->filesystem->touch($this->directory.'/en.txt'); + $this->filesystem->touch($this->directory.'/en_alias.txt'); + $this->filesystem->touch($this->directory.'/de.txt'); + $this->filesystem->touch($this->directory.'/de_alias.txt'); + $this->filesystem->touch($this->directory.'/fr.txt'); + $this->filesystem->touch($this->directory.'/fr_alias.txt'); + $this->filesystem->touch($this->directory.'/root.txt'); + $this->filesystem->touch($this->directory.'/supplementalData.txt'); + $this->filesystem->touch($this->directory.'/supplementaldata.txt'); + $this->filesystem->touch($this->directory.'/meta.txt'); + + 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"}}'); + } + + protected function tearDown() + { + $this->filesystem->remove($this->directory); + } + + public function testScanLocales() + { + $sortedLocales = array('de', 'de_alias', 'en', 'en_alias', 'fr', 'fr_alias'); + + $this->assertSame($sortedLocales, $this->scanner->scanLocales($this->directory)); + } + + public function testScanAliases() + { + $sortedAliases = array('de_alias' => 'de', 'en_alias' => 'en', 'fr_alias' => 'fr'); + + $this->assertSame($sortedAliases, $this->scanner->scanAliases($this->directory)); + } +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt index 0ee0d7f2f5a35..deb8a9464d37d 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en.txt @@ -14,6 +14,16 @@ en{ 2, 3, } + IntVectorWithStringKeys{ + a:int{0} + b:int{1} + c:int{2} + } + TableWithIntKeys{ + 0:int{0} + 1:int{1} + 3:int{3} + } FalseBoolean{"false"} TrueBoolean{"true"} Null{""} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt new file mode 100644 index 0000000000000..85386f2074dc2 --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/en_nofallback.txt @@ -0,0 +1,3 @@ +en_nofallback:table(nofallback){ + Entry{"Value"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt new file mode 100644 index 0000000000000..6669bfdd8306e --- /dev/null +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/Fixtures/escaped.txt @@ -0,0 +1,4 @@ +escaped{ + "EntryWith:Colon"{"Value"} + "Entry With Spaces"{"Value"} +} diff --git a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php index cbe0c8d8bfc4b..05dfab7d63160 100644 --- a/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php +++ b/src/Symfony/Component/Intl/Tests/ResourceBundle/Writer/TextBundleWriterTest.php @@ -36,7 +36,7 @@ class TextBundleWriterTest extends \PHPUnit_Framework_TestCase protected function setUp() { $this->writer = new TextBundleWriter(); - $this->directory = sys_get_temp_dir() . '/TextBundleWriterTest/' . rand(1000, 9999); + $this->directory = sys_get_temp_dir().'/TextBundleWriterTest/'.rand(1000, 9999); $this->filesystem = new Filesystem(); $this->filesystem->mkdir($this->directory); @@ -54,6 +54,8 @@ public function testWrite() 'Array' => array('foo', 'bar', array('Key' => 'value')), 'Integer' => 5, 'IntVector' => array(0, 1, 2, 3), + 'IntVectorWithStringKeys' => array('a' => 0, 'b' => 1, 'c' => 2), + 'TableWithIntKeys' => array(0 => 0, 1 => 1, 3 => 3), 'FalseBoolean' => false, 'TrueBoolean' => true, 'Null' => null, @@ -62,6 +64,50 @@ public function testWrite() 'Entry2' => 'String', )); - $this->assertFileEquals(__DIR__ . '/Fixtures/en.txt', $this->directory . '/en.txt'); + $this->assertFileEquals(__DIR__.'/Fixtures/en.txt', $this->directory.'/en.txt'); + } + + public function testWriteTraversable() + { + $this->writer->write($this->directory, 'en', new \ArrayIterator(array( + 'Entry1' => new \ArrayIterator(array( + 'Array' => array('foo', 'bar', array('Key' => 'value')), + 'Integer' => 5, + 'IntVector' => array(0, 1, 2, 3), + 'IntVectorWithStringKeys' => array('a' => 0, 'b' => 1, 'c' => 2), + 'TableWithIntKeys' => array(0 => 0, 1 => 1, 3 => 3), + 'FalseBoolean' => false, + 'TrueBoolean' => true, + 'Null' => null, + 'Float' => 1.23, + )), + 'Entry2' => 'String', + ))); + + $this->assertFileEquals(__DIR__.'/Fixtures/en.txt', $this->directory.'/en.txt'); + } + + public function testWriteNoFallback() + { + $data = array( + 'Entry' => 'Value' + ); + + $this->writer->write($this->directory, 'en_nofallback', $data, $fallback = false); + + $this->assertFileEquals(__DIR__.'/Fixtures/en_nofallback.txt', $this->directory.'/en_nofallback.txt'); + } + + public function testEscapeKeysIfNecessary() + { + $this->writer->write($this->directory, 'escaped', array( + // Keys with colons must be escaped, otherwise the part after the + // colon is interpreted as resource type + 'EntryWith:Colon' => 'Value', + // Keys with spaces must be escaped + 'Entry With Spaces' => 'Value', + )); + + $this->assertFileEquals(__DIR__.'/Fixtures/escaped.txt', $this->directory.'/escaped.txt'); } }