diff --git a/composer.json b/composer.json index 8a14d570b..30695421e 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "symfony/polyfill-intl-normalizer": "^1.20", "symfony/process": "4.4.30", "symfony/routing": "4.4.30", - "symfony/translation": "^4.4.41", + "symfony/translation": "^5.3", "web-auth/webauthn-lib": "^3.1" }, "extra": { diff --git a/composer.lock b/composer.lock index fbb79dd10..8fe166470 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "734ce02a770960178c3a2ffb071546ce", + "content-hash": "f34732753ec3bce77fcd4016db99a381", "packages": [ { "name": "aws/aws-crt-php", @@ -6240,43 +6240,46 @@ }, { "name": "symfony/translation", - "version": "v4.4.41", + "version": "v5.3.14", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331" + "reference": "945066809dc18f6e26123098e1b6e1d7a948660b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", + "url": "https://api.github.com/repos/symfony/translation/zipball/945066809dc18f6e26123098e1b6e1d7a948660b", + "reference": "945066809dc18f6e26123098e1b6e1d7a948660b", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1.6|^2" + "symfony/translation-contracts": "^2.3" }, "conflict": { - "symfony/config": "<3.4", - "symfony/dependency-injection": "<3.4", - "symfony/http-kernel": "<4.4", - "symfony/yaml": "<3.4" + "symfony/config": "<4.4", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "1.0|2.0" + "symfony/translation-implementation": "2.3" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/finder": "~2.8|~3.0|~4.0|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^3.4|^4.0|^5.0", + "symfony/config": "^4.4|^5.0", + "symfony/console": "^4.4|^5.0", + "symfony/dependency-injection": "^5.0", + "symfony/finder": "^4.4|^5.0", + "symfony/http-kernel": "^5.0", + "symfony/intl": "^4.4|^5.0", + "symfony/polyfill-intl-icu": "^1.21", "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/yaml": "^4.4|^5.0" }, "suggest": { "psr/log-implementation": "To use logging capability in translator", @@ -6285,6 +6288,9 @@ }, "type": "library", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, @@ -6309,7 +6315,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v4.4.41" + "source": "https://github.com/symfony/translation/tree/v5.3.14" }, "funding": [ { @@ -6325,7 +6331,7 @@ "type": "tidelift" } ], - "time": "2022-04-21T07:22:34+00:00" + "time": "2022-01-03T19:49:08+00:00" }, { "name": "symfony/translation-contracts", diff --git a/composer/InstalledVersions.php b/composer/InstalledVersions.php index c6b54af7b..51e734a77 100644 --- a/composer/InstalledVersions.php +++ b/composer/InstalledVersions.php @@ -98,7 +98,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { - return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; } } @@ -119,7 +119,7 @@ public static function isInstalled($packageName, $includeDevRequirements = true) */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { - $constraint = $parser->parseConstraints($constraint); + $constraint = $parser->parseConstraints((string) $constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); @@ -328,7 +328,9 @@ private static function getInstalled() if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { - $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } @@ -340,12 +342,17 @@ private static function getInstalled() // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { - self::$installed = require __DIR__ . '/installed.php'; + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; } else { self::$installed = array(); } } - $installed[] = self::$installed; + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } return $installed; } diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index cca9a2c0c..809fef646 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -3159,6 +3159,9 @@ 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => $vendorDir . '/symfony/translation/Catalogue/MergeOperation.php', 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => $vendorDir . '/symfony/translation/Catalogue/OperationInterface.php', 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => $vendorDir . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => $vendorDir . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => $vendorDir . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => $vendorDir . '/symfony/translation/Command/TranslationTrait.php', 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => $vendorDir . '/symfony/translation/Command/XliffLintCommand.php', 'Symfony\\Component\\Translation\\DataCollectorTranslator' => $vendorDir . '/symfony/translation/DataCollectorTranslator.php', 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => $vendorDir . '/symfony/translation/DataCollector/TranslationDataCollector.php', @@ -3179,23 +3182,26 @@ 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => $vendorDir . '/symfony/translation/Dumper/XliffFileDumper.php', 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => $vendorDir . '/symfony/translation/Dumper/YamlFileDumper.php', 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/translation/Exception/IncompleteDsnException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/translation/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => $vendorDir . '/symfony/translation/Exception/InvalidResourceException.php', 'Symfony\\Component\\Translation\\Exception\\LogicException' => $vendorDir . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => $vendorDir . '/symfony/translation/Exception/MissingRequiredOptionException.php', 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => $vendorDir . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => $vendorDir . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ProviderExceptionInterface.php', 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => $vendorDir . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/translation/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => $vendorDir . '/symfony/translation/Extractor/AbstractFileExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => $vendorDir . '/symfony/translation/Extractor/ChainExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => $vendorDir . '/symfony/translation/Extractor/ExtractorInterface.php', 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => $vendorDir . '/symfony/translation/Extractor/PhpExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => $vendorDir . '/symfony/translation/Extractor/PhpStringTokenParser.php', - 'Symfony\\Component\\Translation\\Formatter\\ChoiceMessageFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => $vendorDir . '/symfony/translation/Formatter/IntlFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/IntlFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => $vendorDir . '/symfony/translation/Formatter/MessageFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/MessageFormatterInterface.php', 'Symfony\\Component\\Translation\\IdentityTranslator' => $vendorDir . '/symfony/translation/IdentityTranslator.php', - 'Symfony\\Component\\Translation\\Interval' => $vendorDir . '/symfony/translation/Interval.php', 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => $vendorDir . '/symfony/translation/Loader/ArrayLoader.php', 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => $vendorDir . '/symfony/translation/Loader/CsvFileLoader.php', 'Symfony\\Component\\Translation\\Loader\\FileLoader' => $vendorDir . '/symfony/translation/Loader/FileLoader.php', @@ -3213,14 +3219,23 @@ 'Symfony\\Component\\Translation\\LoggingTranslator' => $vendorDir . '/symfony/translation/LoggingTranslator.php', 'Symfony\\Component\\Translation\\MessageCatalogue' => $vendorDir . '/symfony/translation/MessageCatalogue.php', 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => $vendorDir . '/symfony/translation/MessageCatalogueInterface.php', - 'Symfony\\Component\\Translation\\MessageSelector' => $vendorDir . '/symfony/translation/MessageSelector.php', 'Symfony\\Component\\Translation\\MetadataAwareInterface' => $vendorDir . '/symfony/translation/MetadataAwareInterface.php', - 'Symfony\\Component\\Translation\\PluralizationRules' => $vendorDir . '/symfony/translation/PluralizationRules.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => $vendorDir . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => $vendorDir . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => $vendorDir . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => $vendorDir . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => $vendorDir . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => $vendorDir . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => $vendorDir . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => $vendorDir . '/symfony/translation/PseudoLocalizationTranslator.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => $vendorDir . '/symfony/translation/Reader/TranslationReader.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => $vendorDir . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => $vendorDir . '/symfony/translation/TranslatableMessage.php', 'Symfony\\Component\\Translation\\Translator' => $vendorDir . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => $vendorDir . '/symfony/translation/TranslatorBag.php', 'Symfony\\Component\\Translation\\TranslatorBagInterface' => $vendorDir . '/symfony/translation/TranslatorBagInterface.php', - 'Symfony\\Component\\Translation\\TranslatorInterface' => $vendorDir . '/symfony/translation/TranslatorInterface.php', 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => $vendorDir . '/symfony/translation/Util/ArrayConverter.php', 'Symfony\\Component\\Translation\\Util\\XliffUtils' => $vendorDir . '/symfony/translation/Util/XliffUtils.php', 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => $vendorDir . '/symfony/translation/Writer/TranslationWriter.php', diff --git a/composer/autoload_files.php b/composer/autoload_files.php index 1a7440d5c..228f75855 100644 --- a/composer/autoload_files.php +++ b/composer/autoload_files.php @@ -122,4 +122,5 @@ 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php', '2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php', '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', ); diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 6f053a257..5b793a06b 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -123,6 +123,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', '2c102faa651ef8ea5874edb585946bce' => __DIR__ . '/..' . '/swiftmailer/swiftmailer/lib/swift_required.php', '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', ); public static $prefixLengthsPsr4 = array ( @@ -3831,6 +3832,9 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/MergeOperation.php', 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => __DIR__ . '/..' . '/symfony/translation/Catalogue/OperationInterface.php', 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationTrait.php', 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => __DIR__ . '/..' . '/symfony/translation/Command/XliffLintCommand.php', 'Symfony\\Component\\Translation\\DataCollectorTranslator' => __DIR__ . '/..' . '/symfony/translation/DataCollectorTranslator.php', 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => __DIR__ . '/..' . '/symfony/translation/DataCollector/TranslationDataCollector.php', @@ -3851,23 +3855,26 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/XliffFileDumper.php', 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/YamlFileDumper.php', 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/translation/Exception/IncompleteDsnException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidArgumentException.php', 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidResourceException.php', 'Symfony\\Component\\Translation\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => __DIR__ . '/..' . '/symfony/translation/Exception/MissingRequiredOptionException.php', 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderExceptionInterface.php', 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/translation/Exception/UnsupportedSchemeException.php', 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/AbstractFileExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/ChainExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => __DIR__ . '/..' . '/symfony/translation/Extractor/ExtractorInterface.php', 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpExtractor.php', 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpStringTokenParser.php', - 'Symfony\\Component\\Translation\\Formatter\\ChoiceMessageFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatterInterface.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatter.php', 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatterInterface.php', 'Symfony\\Component\\Translation\\IdentityTranslator' => __DIR__ . '/..' . '/symfony/translation/IdentityTranslator.php', - 'Symfony\\Component\\Translation\\Interval' => __DIR__ . '/..' . '/symfony/translation/Interval.php', 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/ArrayLoader.php', 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/CsvFileLoader.php', 'Symfony\\Component\\Translation\\Loader\\FileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/FileLoader.php', @@ -3885,14 +3892,23 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'Symfony\\Component\\Translation\\LoggingTranslator' => __DIR__ . '/..' . '/symfony/translation/LoggingTranslator.php', 'Symfony\\Component\\Translation\\MessageCatalogue' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogue.php', 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogueInterface.php', - 'Symfony\\Component\\Translation\\MessageSelector' => __DIR__ . '/..' . '/symfony/translation/MessageSelector.php', 'Symfony\\Component\\Translation\\MetadataAwareInterface' => __DIR__ . '/..' . '/symfony/translation/MetadataAwareInterface.php', - 'Symfony\\Component\\Translation\\PluralizationRules' => __DIR__ . '/..' . '/symfony/translation/PluralizationRules.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => __DIR__ . '/..' . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => __DIR__ . '/..' . '/symfony/translation/PseudoLocalizationTranslator.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReader.php', 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => __DIR__ . '/..' . '/symfony/translation/TranslatableMessage.php', 'Symfony\\Component\\Translation\\Translator' => __DIR__ . '/..' . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => __DIR__ . '/..' . '/symfony/translation/TranslatorBag.php', 'Symfony\\Component\\Translation\\TranslatorBagInterface' => __DIR__ . '/..' . '/symfony/translation/TranslatorBagInterface.php', - 'Symfony\\Component\\Translation\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/translation/TranslatorInterface.php', 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => __DIR__ . '/..' . '/symfony/translation/Util/ArrayConverter.php', 'Symfony\\Component\\Translation\\Util\\XliffUtils' => __DIR__ . '/..' . '/symfony/translation/Util/XliffUtils.php', 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => __DIR__ . '/..' . '/symfony/translation/Writer/TranslationWriter.php', diff --git a/composer/installed.json b/composer/installed.json index e5b567244..fc42416ce 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -6507,54 +6507,60 @@ }, { "name": "symfony/translation", - "version": "v4.4.41", - "version_normalized": "4.4.41.0", + "version": "v5.3.14", + "version_normalized": "5.3.14.0", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331" + "reference": "945066809dc18f6e26123098e1b6e1d7a948660b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", - "reference": "dcb67eae126e74507e0b4f0b9ac6ef35b37c3331", + "url": "https://api.github.com/repos/symfony/translation/zipball/945066809dc18f6e26123098e1b6e1d7a948660b", + "reference": "945066809dc18f6e26123098e1b6e1d7a948660b", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", - "symfony/translation-contracts": "^1.1.6|^2" + "symfony/translation-contracts": "^2.3" }, "conflict": { - "symfony/config": "<3.4", - "symfony/dependency-injection": "<3.4", - "symfony/http-kernel": "<4.4", - "symfony/yaml": "<3.4" + "symfony/config": "<4.4", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "1.0|2.0" + "symfony/translation-implementation": "2.3" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/finder": "~2.8|~3.0|~4.0|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^3.4|^4.0|^5.0", + "symfony/config": "^4.4|^5.0", + "symfony/console": "^4.4|^5.0", + "symfony/dependency-injection": "^5.0", + "symfony/finder": "^4.4|^5.0", + "symfony/http-kernel": "^5.0", + "symfony/intl": "^4.4|^5.0", + "symfony/polyfill-intl-icu": "^1.21", "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/yaml": "^4.4|^5.0" }, "suggest": { "psr/log-implementation": "To use logging capability in translator", "symfony/config": "", "symfony/yaml": "" }, - "time": "2022-04-21T07:22:34+00:00", + "time": "2022-01-03T19:49:08+00:00", "type": "library", "installation-source": "dist", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, @@ -6579,7 +6585,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v4.4.41" + "source": "https://github.com/symfony/translation/tree/v5.3.14" }, "funding": [ { diff --git a/composer/installed.php b/composer/installed.php index 361bbd4b7..b33bacff4 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -1,9 +1,9 @@ array( 'name' => 'nextcloud/3rdparty', - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', - 'reference' => '0eb92e4624ac87a31a767628853dca656e99db17', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -326,9 +326,9 @@ 'dev_requirement' => false, ), 'nextcloud/3rdparty' => array( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', - 'reference' => '0eb92e4624ac87a31a767628853dca656e99db17', + 'pretty_version' => '1.0.0+no-version-set', + 'version' => '1.0.0.0', + 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -911,9 +911,9 @@ 'dev_requirement' => false, ), 'symfony/translation' => array( - 'pretty_version' => 'v4.4.41', - 'version' => '4.4.41.0', - 'reference' => 'dcb67eae126e74507e0b4f0b9ac6ef35b37c3331', + 'pretty_version' => 'v5.3.14', + 'version' => '5.3.14.0', + 'reference' => '945066809dc18f6e26123098e1b6e1d7a948660b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/translation', 'aliases' => array(), @@ -931,7 +931,7 @@ 'symfony/translation-implementation' => array( 'dev_requirement' => false, 'provided' => array( - 0 => '1.0|2.0', + 0 => '2.3', ), ), 'thecodingmachine/safe' => array( diff --git a/symfony/translation/Catalogue/AbstractOperation.php b/symfony/translation/Catalogue/AbstractOperation.php index 4953563db..98d42e5b6 100644 --- a/symfony/translation/Catalogue/AbstractOperation.php +++ b/symfony/translation/Catalogue/AbstractOperation.php @@ -26,6 +26,10 @@ */ abstract class AbstractOperation implements OperationInterface { + public const OBSOLETE_BATCH = 'obsolete'; + public const NEW_BATCH = 'new'; + public const ALL_BATCH = 'all'; + protected $source; protected $target; protected $result; @@ -79,7 +83,18 @@ public function __construct(MessageCatalogueInterface $source, MessageCatalogueI public function getDomains() { if (null === $this->domains) { - $this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains()))); + $domains = []; + foreach ([$this->source, $this->target] as $catalogue) { + foreach ($catalogue->getDomains() as $domain) { + $domains[$domain] = $domain; + + if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) { + $domains[$domainIcu] = $domainIcu; + } + } + } + + $this->domains = array_values($domains); } return $this->domains; @@ -88,49 +103,49 @@ public function getDomains() /** * {@inheritdoc} */ - public function getMessages($domain) + public function getMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['all'])) { + if (!isset($this->messages[$domain][self::ALL_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['all']; + return $this->messages[$domain][self::ALL_BATCH]; } /** * {@inheritdoc} */ - public function getNewMessages($domain) + public function getNewMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['new'])) { + if (!isset($this->messages[$domain][self::NEW_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['new']; + return $this->messages[$domain][self::NEW_BATCH]; } /** * {@inheritdoc} */ - public function getObsoleteMessages($domain) + public function getObsoleteMessages(string $domain) { if (!\in_array($domain, $this->getDomains())) { throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); } - if (!isset($this->messages[$domain]['obsolete'])) { + if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) { $this->processDomain($domain); } - return $this->messages[$domain]['obsolete']; + return $this->messages[$domain][self::OBSOLETE_BATCH]; } /** @@ -147,11 +162,42 @@ public function getResult() return $this->result; } + /** + * @param self::*_BATCH $batch + */ + public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void + { + // If MessageFormatter class does not exists, intl domains are not supported. + if (!class_exists(\MessageFormatter::class)) { + return; + } + + foreach ($this->getDomains() as $domain) { + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + switch ($batch) { + case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break; + case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break; + case self::ALL_BATCH: $messages = $this->getMessages($domain); break; + default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)); + } + + if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) { + continue; + } + + $result = $this->getResult(); + $allIntlMessages = $result->all($intlDomain); + $currentMessages = array_diff_key($messages, $result->all($domain)); + $result->replace($currentMessages, $domain); + $result->replace($allIntlMessages + $messages, $intlDomain); + } + } + /** * Performs operation on source and target catalogues for the given domain and * stores the results. * * @param string $domain The domain which the operation will be performed for */ - abstract protected function processDomain($domain); + abstract protected function processDomain(string $domain); } diff --git a/symfony/translation/Catalogue/MergeOperation.php b/symfony/translation/Catalogue/MergeOperation.php index d55542c0c..87db2fb03 100644 --- a/symfony/translation/Catalogue/MergeOperation.php +++ b/symfony/translation/Catalogue/MergeOperation.php @@ -27,7 +27,7 @@ class MergeOperation extends AbstractOperation /** * {@inheritdoc} */ - protected function processDomain($domain) + protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], diff --git a/symfony/translation/Catalogue/OperationInterface.php b/symfony/translation/Catalogue/OperationInterface.php index 87d888efb..9ffac88d2 100644 --- a/symfony/translation/Catalogue/OperationInterface.php +++ b/symfony/translation/Catalogue/OperationInterface.php @@ -44,29 +44,23 @@ public function getDomains(); /** * Returns all valid messages ('all') after operation. * - * @param string $domain - * * @return array */ - public function getMessages($domain); + public function getMessages(string $domain); /** * Returns new messages ('new') after operation. * - * @param string $domain - * * @return array */ - public function getNewMessages($domain); + public function getNewMessages(string $domain); /** * Returns obsolete messages ('obsolete') after operation. * - * @param string $domain - * * @return array */ - public function getObsoleteMessages($domain); + public function getObsoleteMessages(string $domain); /** * Returns resulting catalogue ('result'). diff --git a/symfony/translation/Catalogue/TargetOperation.php b/symfony/translation/Catalogue/TargetOperation.php index c8b065512..682b5752f 100644 --- a/symfony/translation/Catalogue/TargetOperation.php +++ b/symfony/translation/Catalogue/TargetOperation.php @@ -28,7 +28,7 @@ class TargetOperation extends AbstractOperation /** * {@inheritdoc} */ - protected function processDomain($domain) + protected function processDomain(string $domain) { $this->messages[$domain] = [ 'all' => [], diff --git a/symfony/translation/Command/TranslationPullCommand.php b/symfony/translation/Command/TranslationPullCommand.php new file mode 100644 index 000000000..bf0ab6567 --- /dev/null +++ b/symfony/translation/Command/TranslationPullCommand.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +final class TranslationPullCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:pull'; + protected static $defaultDescription = 'Pull translations from a given provider.'; + + private $providerCollection; + private $writer; + private $reader; + private $defaultLocale; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) + { + $this->providerCollection = $providerCollection; + $this->writer = $writer; + $this->reader = $reader; + $this->defaultLocale = $defaultLocale; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providerCollection->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), + new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pulls translations from the given provider. Only +new translations are pulled, existing ones are not overwritten. + +You can overwrite existing translations (and remove the missing ones on local side) by using the --force flag: + + php %command.full_name% --force provider + +Full example: + + php %command.full_name% provider --force --domains=messages --domains=validators --locales=en + +This command pulls all translations associated with the messages and validators domains for the en locale. +Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. +Local translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $provider = $this->providerCollection->get($input->getArgument('provider')); + $force = $input->getOption('force'); + $intlIcu = $input->getOption('intl-icu'); + $locales = $input->getOption('locales') ?: $this->enabledLocales; + $domains = $input->getOption('domains'); + $format = $input->getOption('format'); + $xliffVersion = '1.2'; + + if ($intlIcu && !$force) { + $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); + } + + switch ($format) { + case 'xlf20': $xliffVersion = '2.0'; + // no break + case 'xlf12': $format = 'xlf'; + } + + $writeOptions = [ + 'path' => end($this->transPaths), + 'xliff_version' => $xliffVersion, + 'default_locale' => $this->defaultLocale, + ]; + + if (!$domains) { + $domains = $provider->getDomains(); + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($force) { + foreach ($providerTranslations->getCatalogues() as $catalogue) { + $operation = new TargetOperation((new MessageCatalogue($catalogue->getLocale())), $catalogue); + if ($intlIcu) { + $operation->moveMessagesToIntlDomainsIfPossible(); + } + $this->writer->write($operation->getResult(), $format, $writeOptions); + } + + $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + // Append pulled translations to local ones. + $localTranslations->addBag($providerTranslations->diff($localTranslations)); + + foreach ($localTranslations->getCatalogues() as $catalogue) { + $this->writer->write($catalogue, $format, $writeOptions); + } + + $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } +} diff --git a/symfony/translation/Command/TranslationPushCommand.php b/symfony/translation/Command/TranslationPushCommand.php new file mode 100644 index 000000000..ad6b67694 --- /dev/null +++ b/symfony/translation/Command/TranslationPushCommand.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +final class TranslationPushCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:push'; + protected static $defaultDescription = 'Push translations to a given provider.'; + + private $providers; + private $reader; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) + { + $this->providers = $providers; + $this->reader = $reader; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providers->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), + new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pushes translations to the given provider. Only new +translations are pushed, existing ones are not overwritten. + +You can overwrite existing translations by using the --force flag: + + php %command.full_name% --force provider + +You can delete provider translations which are not present locally by using the --delete-missing flag: + + php %command.full_name% --delete-missing provider + +Full example: + + php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en + +This command pushes all translations associated with the messages and validators domains for the en locale. +Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. +Provider translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $provider = $this->providers->get($input->getArgument('provider')); + + if (!$this->enabledLocales) { + throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); + } + + $io = new SymfonyStyle($input, $output); + $domains = $input->getOption('domains'); + $locales = $input->getOption('locales'); + $force = $input->getOption('force'); + $deleteMissing = $input->getOption('delete-missing'); + + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + if (!$domains) { + $domains = $this->getDomainsFromTranslatorBag($localTranslations); + } + + if (!$deleteMissing && $force) { + $provider->write($localTranslations); + + $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($deleteMissing) { + $provider->delete($providerTranslations->diff($localTranslations)); + + $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + // Read provider translations again, after missing translations deletion, + // to avoid push freshly deleted translations. + $providerTranslations = $provider->read($domains, $locales); + } + + $translationsToWrite = $localTranslations->diff($providerTranslations); + + if ($force) { + $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); + } + + $provider->write($translationsToWrite); + + $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array + { + $domains = []; + + foreach ($translatorBag->getCatalogues() as $catalogue) { + $domains += $catalogue->getDomains(); + } + + return array_unique($domains); + } +} diff --git a/symfony/translation/Command/TranslationTrait.php b/symfony/translation/Command/TranslationTrait.php new file mode 100644 index 000000000..eafaffd3f --- /dev/null +++ b/symfony/translation/Command/TranslationTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @internal + */ +trait TranslationTrait +{ + private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag + { + $bag = new TranslatorBag(); + + foreach ($locales as $locale) { + $catalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $this->reader->read($path, $catalogue); + } + + if ($domains) { + foreach ($domains as $domain) { + $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); + } + } else { + $bag->addCatalogue($catalogue); + } + } + + return $bag; + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } +} diff --git a/symfony/translation/Command/XliffLintCommand.php b/symfony/translation/Command/XliffLintCommand.php index 80d45bf53..4117d87c8 100644 --- a/symfony/translation/Command/XliffLintCommand.php +++ b/symfony/translation/Command/XliffLintCommand.php @@ -31,6 +31,7 @@ class XliffLintCommand extends Command { protected static $defaultName = 'lint:xliff'; + protected static $defaultDescription = 'Lint an XLIFF file and outputs encountered errors'; private $format; private $displayCorrectFiles; @@ -53,7 +54,7 @@ public function __construct(string $name = null, callable $directoryIteratorProv protected function configure() { $this - ->setDescription('Lint an XLIFF file and outputs encountered errors') + ->setDescription(self::$defaultDescription) ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') ->setHelp(<<display($io, [$this->validate(file_get_contents('php://stdin'))]); } - // @deprecated to be removed in 5.0 if (!$filenames) { - if (0 !== ftell(\STDIN)) { - throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); - } - - @trigger_error('Piping content from STDIN to the "lint:xliff" command without passing the dash symbol "-" as argument is deprecated since Symfony 4.4.', \E_USER_DEPRECATED); - - return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); + throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; diff --git a/symfony/translation/DataCollector/TranslationDataCollector.php b/symfony/translation/DataCollector/TranslationDataCollector.php index e4f0b3a5a..f8480adba 100644 --- a/symfony/translation/DataCollector/TranslationDataCollector.php +++ b/symfony/translation/DataCollector/TranslationDataCollector.php @@ -21,7 +21,7 @@ /** * @author Abdellatif Ait boudad * - * @final since Symfony 4.4 + * @final */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -47,10 +47,8 @@ public function lateCollect() /** * {@inheritdoc} - * - * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response/*, \Throwable $exception = null*/) + public function collect(Request $request, Response $response, \Throwable $exception = null) { $this->data['locale'] = $this->translator->getLocale(); $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); @@ -102,7 +100,7 @@ public function getLocale() } /** - * @internal since Symfony 4.2 + * @internal */ public function getFallbackLocales() { diff --git a/symfony/translation/DataCollectorTranslator.php b/symfony/translation/DataCollectorTranslator.php index 68acf1561..c7d359754 100644 --- a/symfony/translation/DataCollectorTranslator.php +++ b/symfony/translation/DataCollectorTranslator.php @@ -13,14 +13,13 @@ use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface, WarmableInterface +class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface { public const MESSAGE_DEFINED = 0; public const MESSAGE_MISSING = 1; @@ -36,13 +35,10 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorIn /** * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface */ - public function __construct($translator) + public function __construct(TranslatorInterface $translator) { - if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { - throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator))); + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; @@ -51,9 +47,9 @@ public function __construct($translator) /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - $trans = $this->translator->trans($id, $parameters, $domain, $locale); + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->collectMessage($locale, $domain, $id, $trans, $parameters); return $trans; @@ -61,54 +57,48 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul /** * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) + public function setLocale(string $locale) { - if ($this->translator instanceof TranslatorInterface) { - $trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } else { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - - $this->collectMessage($locale, $domain, $id, $trans, ['%count%' => $number] + $parameters); - - return $trans; + $this->translator->setLocale($locale); } /** * {@inheritdoc} */ - public function setLocale($locale) + public function getLocale() { - $this->translator->setLocale($locale); + return $this->translator->getLocale(); } /** * {@inheritdoc} */ - public function getLocale() + public function getCatalogue(string $locale = null) { - return $this->translator->getLocale(); + return $this->translator->getCatalogue($locale); } /** * {@inheritdoc} */ - public function getCatalogue($locale = null) + public function getCatalogues(): array { - return $this->translator->getCatalogue($locale); + return $this->translator->getCatalogues(); } /** * {@inheritdoc} + * + * @return string[] */ - public function warmUp($cacheDir) + public function warmUp(string $cacheDir) { if ($this->translator instanceof WarmableInterface) { - $this->translator->warmUp($cacheDir); + return (array) $this->translator->warmUp($cacheDir); } + + return []; } /** @@ -128,7 +118,7 @@ public function getFallbackLocales() /** * Passes through all unknown calls onto the translator object. */ - public function __call($method, $args) + public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } @@ -141,13 +131,12 @@ public function getCollectedMessages() return $this->messages; } - private function collectMessage(?string $locale, ?string $domain, ?string $id, string $translation, ?array $parameters = []) + private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []) { if (null === $domain) { $domain = 'messages'; } - $id = (string) $id; $catalogue = $this->translator->getCatalogue($locale); $locale = $catalogue->getLocale(); $fallbackLocale = null; diff --git a/symfony/translation/DependencyInjection/TranslationDumperPass.php b/symfony/translation/DependencyInjection/TranslationDumperPass.php index 930f36d72..6d78342bc 100644 --- a/symfony/translation/DependencyInjection/TranslationDumperPass.php +++ b/symfony/translation/DependencyInjection/TranslationDumperPass.php @@ -25,6 +25,10 @@ class TranslationDumperPass implements CompilerPassInterface public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper') { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->writerServiceId = $writerServiceId; $this->dumperTag = $dumperTag; } diff --git a/symfony/translation/DependencyInjection/TranslationExtractorPass.php b/symfony/translation/DependencyInjection/TranslationExtractorPass.php index d08b2ba5f..fab6b20ae 100644 --- a/symfony/translation/DependencyInjection/TranslationExtractorPass.php +++ b/symfony/translation/DependencyInjection/TranslationExtractorPass.php @@ -26,6 +26,10 @@ class TranslationExtractorPass implements CompilerPassInterface public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->extractorServiceId = $extractorServiceId; $this->extractorTag = $extractorTag; } diff --git a/symfony/translation/DependencyInjection/TranslatorPass.php b/symfony/translation/DependencyInjection/TranslatorPass.php index ed4a840d8..c6a1306eb 100644 --- a/symfony/translation/DependencyInjection/TranslatorPass.php +++ b/symfony/translation/DependencyInjection/TranslatorPass.php @@ -26,6 +26,10 @@ class TranslatorPass implements CompilerPassInterface public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->readerServiceId = $readerServiceId; $this->loaderTag = $loaderTag; @@ -68,7 +72,7 @@ public function process(ContainerBuilder $container) return; } - $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(2)); + $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); if ($container->hasDefinition($this->debugCommandServiceId)) { $definition = $container->getDefinition($this->debugCommandServiceId); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); diff --git a/symfony/translation/DependencyInjection/TranslatorPathsPass.php b/symfony/translation/DependencyInjection/TranslatorPathsPass.php index 37c8c5719..85b0fa480 100644 --- a/symfony/translation/DependencyInjection/TranslatorPathsPass.php +++ b/symfony/translation/DependencyInjection/TranslatorPathsPass.php @@ -33,6 +33,10 @@ class TranslatorPathsPass extends AbstractRecursivePass public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->debugCommandServiceId = $debugCommandServiceId; $this->updateCommandServiceId = $updateCommandServiceId; @@ -82,7 +86,7 @@ public function process(ContainerBuilder $container) } } - protected function processValue($value, $isRoot = false) + protected function processValue($value, bool $isRoot = false) { if ($value instanceof Reference) { if ((string) $value === $this->translatorServiceId) { diff --git a/symfony/translation/Dumper/CsvFileDumper.php b/symfony/translation/Dumper/CsvFileDumper.php index 8f7b032fa..0c8589af8 100644 --- a/symfony/translation/Dumper/CsvFileDumper.php +++ b/symfony/translation/Dumper/CsvFileDumper.php @@ -26,7 +26,7 @@ class CsvFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $handle = fopen('php://memory', 'r+'); @@ -43,11 +43,8 @@ public function formatCatalogue(MessageCatalogue $messages, $domain, array $opti /** * Sets the delimiter and escape character for CSV. - * - * @param string $delimiter Delimiter character - * @param string $enclosure Enclosure character */ - public function setCsvControl($delimiter = ';', $enclosure = '"') + public function setCsvControl(string $delimiter = ';', string $enclosure = '"') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; diff --git a/symfony/translation/Dumper/DumperInterface.php b/symfony/translation/Dumper/DumperInterface.php index 445b70189..7cdaef515 100644 --- a/symfony/translation/Dumper/DumperInterface.php +++ b/symfony/translation/Dumper/DumperInterface.php @@ -26,5 +26,5 @@ interface DumperInterface * * @param array $options Options that are used by the dumper */ - public function dump(MessageCatalogue $messages, $options = []); + public function dump(MessageCatalogue $messages, array $options = []); } diff --git a/symfony/translation/Dumper/FileDumper.php b/symfony/translation/Dumper/FileDumper.php index 87d0c69d7..e257e7224 100644 --- a/symfony/translation/Dumper/FileDumper.php +++ b/symfony/translation/Dumper/FileDumper.php @@ -37,31 +37,15 @@ abstract class FileDumper implements DumperInterface * * @param string $relativePathTemplate A template for the relative paths to files */ - public function setRelativePathTemplate($relativePathTemplate) + public function setRelativePathTemplate(string $relativePathTemplate) { $this->relativePathTemplate = $relativePathTemplate; } - /** - * Sets backup flag. - * - * @param bool $backup - * - * @deprecated since Symfony 4.1 - */ - public function setBackup($backup) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), \E_USER_DEPRECATED); - - if (false !== $backup) { - throw new \LogicException('The backup feature is no longer supported.'); - } - } - /** * {@inheritdoc} */ - public function dump(MessageCatalogue $messages, $options = []) + public function dump(MessageCatalogue $messages, array $options = []) { if (!\array_key_exists('path', $options)) { throw new InvalidArgumentException('The file dumper needs a path option.'); @@ -103,11 +87,9 @@ public function dump(MessageCatalogue $messages, $options = []) /** * Transforms a domain of a message catalogue to its string representation. * - * @param string $domain - * * @return string representation */ - abstract public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []); + abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []); /** * Gets the file extension of the dumper. diff --git a/symfony/translation/Dumper/IcuResFileDumper.php b/symfony/translation/Dumper/IcuResFileDumper.php index 829e0d0c2..cdc59913b 100644 --- a/symfony/translation/Dumper/IcuResFileDumper.php +++ b/symfony/translation/Dumper/IcuResFileDumper.php @@ -28,7 +28,7 @@ class IcuResFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $data = $indexes = $resources = ''; diff --git a/symfony/translation/Dumper/IniFileDumper.php b/symfony/translation/Dumper/IniFileDumper.php index 45ff9614b..93c900a4a 100644 --- a/symfony/translation/Dumper/IniFileDumper.php +++ b/symfony/translation/Dumper/IniFileDumper.php @@ -23,7 +23,7 @@ class IniFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = ''; diff --git a/symfony/translation/Dumper/JsonFileDumper.php b/symfony/translation/Dumper/JsonFileDumper.php index c6c3163b2..34c0b5694 100644 --- a/symfony/translation/Dumper/JsonFileDumper.php +++ b/symfony/translation/Dumper/JsonFileDumper.php @@ -23,7 +23,7 @@ class JsonFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; diff --git a/symfony/translation/Dumper/MoFileDumper.php b/symfony/translation/Dumper/MoFileDumper.php index 5a96dacec..54d0da875 100644 --- a/symfony/translation/Dumper/MoFileDumper.php +++ b/symfony/translation/Dumper/MoFileDumper.php @@ -24,7 +24,7 @@ class MoFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $sources = $targets = $sourceOffsets = $targetOffsets = ''; $offsets = []; diff --git a/symfony/translation/Dumper/PhpFileDumper.php b/symfony/translation/Dumper/PhpFileDumper.php index e77afc2fb..6163b5297 100644 --- a/symfony/translation/Dumper/PhpFileDumper.php +++ b/symfony/translation/Dumper/PhpFileDumper.php @@ -23,7 +23,7 @@ class PhpFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { return "all($domain), true).";\n"; } diff --git a/symfony/translation/Dumper/PoFileDumper.php b/symfony/translation/Dumper/PoFileDumper.php index 1a723f2d0..0d822818c 100644 --- a/symfony/translation/Dumper/PoFileDumper.php +++ b/symfony/translation/Dumper/PoFileDumper.php @@ -23,7 +23,7 @@ class PoFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $output = 'msgid ""'."\n"; $output .= 'msgstr ""'."\n"; diff --git a/symfony/translation/Dumper/QtFileDumper.php b/symfony/translation/Dumper/QtFileDumper.php index 79a64b243..406e9f006 100644 --- a/symfony/translation/Dumper/QtFileDumper.php +++ b/symfony/translation/Dumper/QtFileDumper.php @@ -23,7 +23,7 @@ class QtFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; diff --git a/symfony/translation/Dumper/XliffFileDumper.php b/symfony/translation/Dumper/XliffFileDumper.php index 37e162aa9..f7dbdcddf 100644 --- a/symfony/translation/Dumper/XliffFileDumper.php +++ b/symfony/translation/Dumper/XliffFileDumper.php @@ -24,7 +24,7 @@ class XliffFileDumper extends FileDumper /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { $xliffVersion = '1.2'; if (\array_key_exists('xliff_version', $options)) { diff --git a/symfony/translation/Dumper/YamlFileDumper.php b/symfony/translation/Dumper/YamlFileDumper.php index 520ae3340..0b21e8c83 100644 --- a/symfony/translation/Dumper/YamlFileDumper.php +++ b/symfony/translation/Dumper/YamlFileDumper.php @@ -33,7 +33,7 @@ public function __construct(string $extension = 'yml') /** * {@inheritdoc} */ - public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []) + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []) { if (!class_exists(Yaml::class)) { throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); diff --git a/symfony/translation/Exception/IncompleteDsnException.php b/symfony/translation/Exception/IncompleteDsnException.php new file mode 100644 index 000000000..cb0ce027e --- /dev/null +++ b/symfony/translation/Exception/IncompleteDsnException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +class IncompleteDsnException extends InvalidArgumentException +{ + public function __construct(string $message, string $dsn = null, \Throwable $previous = null) + { + if ($dsn) { + $message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message; + } + + parent::__construct($message, 0, $previous); + } +} diff --git a/symfony/translation/Exception/MissingRequiredOptionException.php b/symfony/translation/Exception/MissingRequiredOptionException.php new file mode 100644 index 000000000..2b5f80806 --- /dev/null +++ b/symfony/translation/Exception/MissingRequiredOptionException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Oskar Stark + */ +class MissingRequiredOptionException extends IncompleteDsnException +{ + public function __construct(string $option, string $dsn = null, \Throwable $previous = null) + { + $message = sprintf('The option "%s" is required but missing.', $option); + + parent::__construct($message, $dsn, $previous); + } +} diff --git a/symfony/translation/Exception/ProviderException.php b/symfony/translation/Exception/ProviderException.php new file mode 100644 index 000000000..659c6d772 --- /dev/null +++ b/symfony/translation/Exception/ProviderException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + * + * @experimental in 5.3 + */ +class ProviderException extends RuntimeException implements ProviderExceptionInterface +{ + private $response; + private $debug; + + public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null) + { + $this->response = $response; + $this->debug .= $response->getInfo('debug') ?? ''; + + parent::__construct($message, $code, $previous); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } + + public function getDebug(): string + { + return $this->debug; + } +} diff --git a/symfony/translation/Exception/ProviderExceptionInterface.php b/symfony/translation/Exception/ProviderExceptionInterface.php new file mode 100644 index 000000000..8cf1c51c3 --- /dev/null +++ b/symfony/translation/Exception/ProviderExceptionInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Fabien Potencier + * + * @experimental in 5.3 + */ +interface ProviderExceptionInterface extends ExceptionInterface +{ + /* + * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface + */ + public function getDebug(): string; +} diff --git a/symfony/translation/Exception/UnsupportedSchemeException.php b/symfony/translation/Exception/UnsupportedSchemeException.php new file mode 100644 index 000000000..7fbaa8f04 --- /dev/null +++ b/symfony/translation/Exception/UnsupportedSchemeException.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Component\Translation\Bridge; +use Symfony\Component\Translation\Provider\Dsn; + +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'crowdin' => [ + 'class' => Bridge\Crowdin\CrowdinProviderFactory::class, + 'package' => 'symfony/crowdin-translation-provider', + ], + 'loco' => [ + 'class' => Bridge\Loco\LocoProviderFactory::class, + 'package' => 'symfony/loco-translation-provider', + ], + 'lokalise' => [ + 'class' => Bridge\Lokalise\LokaliseProviderFactory::class, + 'package' => 'symfony/lokalise-translation-provider', + ], + ]; + + public function __construct(Dsn $dsn, string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/symfony/translation/Extractor/AbstractFileExtractor.php b/symfony/translation/Extractor/AbstractFileExtractor.php index 618df7324..729dd1781 100644 --- a/symfony/translation/Extractor/AbstractFileExtractor.php +++ b/symfony/translation/Extractor/AbstractFileExtractor.php @@ -49,13 +49,11 @@ private function toSplFileInfo(string $file): \SplFileInfo } /** - * @param string $file - * * @return bool * * @throws InvalidArgumentException */ - protected function isFile($file) + protected function isFile(string $file) { if (!is_file($file)) { throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file)); @@ -65,11 +63,9 @@ protected function isFile($file) } /** - * @param string $file - * * @return bool */ - abstract protected function canBeExtracted($file); + abstract protected function canBeExtracted(string $file); /** * @param string|array $resource Files, a file or a directory diff --git a/symfony/translation/Extractor/ChainExtractor.php b/symfony/translation/Extractor/ChainExtractor.php index 2683f5d24..95dcf157c 100644 --- a/symfony/translation/Extractor/ChainExtractor.php +++ b/symfony/translation/Extractor/ChainExtractor.php @@ -29,10 +29,8 @@ class ChainExtractor implements ExtractorInterface /** * Adds a loader to the translation extractor. - * - * @param string $format The format of the loader */ - public function addExtractor($format, ExtractorInterface $extractor) + public function addExtractor(string $format, ExtractorInterface $extractor) { $this->extractors[$format] = $extractor; } @@ -40,7 +38,7 @@ public function addExtractor($format, ExtractorInterface $extractor) /** * {@inheritdoc} */ - public function setPrefix($prefix) + public function setPrefix(string $prefix) { foreach ($this->extractors as $extractor) { $extractor->setPrefix($prefix); diff --git a/symfony/translation/Extractor/ExtractorInterface.php b/symfony/translation/Extractor/ExtractorInterface.php index 34e6b2d6e..e1db8a9c8 100644 --- a/symfony/translation/Extractor/ExtractorInterface.php +++ b/symfony/translation/Extractor/ExtractorInterface.php @@ -30,8 +30,6 @@ public function extract($resource, MessageCatalogue $catalogue); /** * Sets the prefix that should be used for new found messages. - * - * @param string $prefix The prefix */ - public function setPrefix($prefix); + public function setPrefix(string $prefix); } diff --git a/symfony/translation/Extractor/PhpExtractor.php b/symfony/translation/Extractor/PhpExtractor.php index 32389c677..c5efb5f3b 100644 --- a/symfony/translation/Extractor/PhpExtractor.php +++ b/symfony/translation/Extractor/PhpExtractor.php @@ -50,25 +50,83 @@ class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface ], [ '->', - 'transChoice', + 'trans', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ',', self::METHOD_ARGUMENTS_TOKEN, ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', self::METHOD_ARGUMENTS_TOKEN, ',', self::DOMAIN_TOKEN, ], [ - '->', - 'trans', + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', '(', self::MESSAGE_TOKEN, ], [ - '->', - 'transChoice', + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 't', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 't', '(', self::MESSAGE_TOKEN, ], @@ -90,7 +148,7 @@ public function extract($resource, MessageCatalogue $catalog) /** * {@inheritdoc} */ - public function setPrefix($prefix) + public function setPrefix(string $prefix) { $this->prefix = $prefix; } @@ -207,17 +265,9 @@ private function getValue(\Iterator $tokenIterator) /** * Extracts trans message from PHP tokens. - * - * @param array $tokens - * @param string $filename */ - protected function parseTokens($tokens, MessageCatalogue $catalog/*, string $filename*/) + protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) { - if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "string $filename" argument in version 5.0, not defining it is deprecated since Symfony 4.3.', __METHOD__), \E_USER_DEPRECATED); - } - $filename = 2 < \func_num_args() ? func_get_arg(2) : ''; - $tokenIterator = new \ArrayIterator($tokens); for ($key = 0; $key < $tokenIterator->count(); ++$key) { @@ -265,13 +315,11 @@ protected function parseTokens($tokens, MessageCatalogue $catalog/*, string $fil } /** - * @param string $file - * * @return bool * * @throws \InvalidArgumentException */ - protected function canBeExtracted($file) + protected function canBeExtracted(string $file) { return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION); } diff --git a/symfony/translation/Extractor/PhpStringTokenParser.php b/symfony/translation/Extractor/PhpStringTokenParser.php index f468fe5ea..1d82caf26 100644 --- a/symfony/translation/Extractor/PhpStringTokenParser.php +++ b/symfony/translation/Extractor/PhpStringTokenParser.php @@ -67,7 +67,7 @@ class PhpStringTokenParser * * @return string The parsed string */ - public static function parse($str) + public static function parse(string $str) { $bLength = 0; if ('b' === $str[0]) { @@ -93,7 +93,7 @@ public static function parse($str) * * @return string String with escape sequences parsed */ - public static function parseEscapeSequences($str, $quote) + public static function parseEscapeSequences(string $str, string $quote = null) { if (null !== $quote) { $str = str_replace('\\'.$quote, $quote, $str); @@ -127,7 +127,7 @@ private static function parseCallback(array $matches): string * * @return string Parsed string */ - public static function parseDocString($startToken, $str) + public static function parseDocString(string $startToken, string $str) { // strip last newline (thanks tokenizer for sticking it into the string!) $str = preg_replace('~(\r\n|\n|\r)$~', '', $str); diff --git a/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php b/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php deleted file mode 100644 index 621dbb292..000000000 --- a/symfony/translation/Formatter/ChoiceMessageFormatterInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation\Formatter; - -/** - * @author Abdellatif Ait boudad - * - * @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead - */ -interface ChoiceMessageFormatterInterface -{ - /** - * Formats a localized message pattern with given arguments. - * - * @param string $message The message (may also be an object that can be cast to string) - * @param int $number The number to use to find the indice of the message - * @param string $locale The message locale - * @param array $parameters An array of parameters for the message - * - * @return string - */ - public function choiceFormat($message, $number, $locale, array $parameters = []); -} diff --git a/symfony/translation/Formatter/MessageFormatter.php b/symfony/translation/Formatter/MessageFormatter.php index 785d6f226..040796483 100644 --- a/symfony/translation/Formatter/MessageFormatter.php +++ b/symfony/translation/Formatter/MessageFormatter.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Translation\MessageSelector; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols @@ -22,7 +20,7 @@ class_exists(IntlFormatter::class); /** * @author Abdellatif Ait boudad */ -class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface, ChoiceMessageFormatterInterface +class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface { private $translator; private $intlFormatter; @@ -30,14 +28,8 @@ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterf /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ - public function __construct($translator = null, IntlFormatterInterface $intlFormatter = null) + public function __construct(TranslatorInterface $translator = null, IntlFormatterInterface $intlFormatter = null) { - if ($translator instanceof MessageSelector) { - $translator = new IdentityTranslator($translator); - } elseif (null !== $translator && !$translator instanceof TranslatorInterface && !$translator instanceof LegacyTranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } - $this->translator = $translator ?? new IdentityTranslator(); $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } @@ -45,7 +37,7 @@ public function __construct($translator = null, IntlFormatterInterface $intlForm /** * {@inheritdoc} */ - public function format($message, $locale, array $parameters = []) + public function format(string $message, string $locale, array $parameters = []) { if ($this->translator instanceof TranslatorInterface) { return $this->translator->trans($message, $parameters, null, $locale); @@ -61,22 +53,4 @@ public function formatIntl(string $message, string $locale, array $parameters = { return $this->intlFormatter->formatIntl($message, $locale, $parameters); } - - /** - * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use format() with a %count% parameter instead - */ - public function choiceFormat($message, $number, $locale, array $parameters = []) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the format() one instead with a %%count%% parameter.', __METHOD__), \E_USER_DEPRECATED); - - $parameters = ['%count%' => $number] + $parameters; - - if ($this->translator instanceof TranslatorInterface) { - return $this->format($message, $locale, $parameters); - } - - return $this->format($this->translator->transChoice($message, $number, [], null, $locale), $locale, $parameters); - } } diff --git a/symfony/translation/Formatter/MessageFormatterInterface.php b/symfony/translation/Formatter/MessageFormatterInterface.php index 370c05586..b85dbfd11 100644 --- a/symfony/translation/Formatter/MessageFormatterInterface.php +++ b/symfony/translation/Formatter/MessageFormatterInterface.php @@ -26,5 +26,5 @@ interface MessageFormatterInterface * * @return string */ - public function format($message, $locale, array $parameters = []); + public function format(string $message, string $locale, array $parameters = []); } diff --git a/symfony/translation/IdentityTranslator.php b/symfony/translation/IdentityTranslator.php index 864339615..46875edf2 100644 --- a/symfony/translation/IdentityTranslator.php +++ b/symfony/translation/IdentityTranslator.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Translation; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -20,58 +20,7 @@ * * @author Fabien Potencier */ -class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface +class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface { - use TranslatorTrait { - trans as private doTrans; - setLocale as private doSetLocale; - } - - private $selector; - - public function __construct(MessageSelector $selector = null) - { - $this->selector = $selector; - - if (__CLASS__ !== static::class) { - @trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); - } - } - - /** - * {@inheritdoc} - */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) - { - return $this->doTrans($id, $parameters, $domain, $locale); - } - - /** - * {@inheritdoc} - */ - public function setLocale($locale) - { - $this->doSetLocale($locale); - } - - /** - * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter - */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ($this->selector) { - return strtr($this->selector->choose((string) $id, $number, $locale ?: $this->getLocale()), $parameters); - } - - return $this->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } - - private function getPluralizationRule(float $number, string $locale): int - { - return PluralizationRules::get($number, $locale, false); - } + use TranslatorTrait; } diff --git a/symfony/translation/Interval.php b/symfony/translation/Interval.php deleted file mode 100644 index bb6b164e8..000000000 --- a/symfony/translation/Interval.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', Interval::class), \E_USER_DEPRECATED); - -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * Tests if a given number belongs to a given math interval. - * - * An interval can represent a finite set of numbers: - * - * {1,2,3,4} - * - * An interval can represent numbers between two numbers: - * - * [1, +Inf] - * ]-1,2[ - * - * The left delimiter can be [ (inclusive) or ] (exclusive). - * The right delimiter can be [ (exclusive) or ] (inclusive). - * Beside numbers, you can use -Inf and +Inf for the infinite. - * - * @author Fabien Potencier - * - * @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation - * @deprecated since Symfony 4.2, use IdentityTranslator instead - */ -class Interval -{ - /** - * Tests if the given number is in the math interval. - * - * @param int $number A number - * @param string $interval An interval - * - * @return bool - * - * @throws InvalidArgumentException - */ - public static function test($number, $interval) - { - $interval = trim($interval); - - if (!preg_match('/^'.self::getIntervalRegexp().'$/x', $interval, $matches)) { - throw new InvalidArgumentException(sprintf('"%s" is not a valid interval.', $interval)); - } - - if ($matches[1]) { - foreach (explode(',', $matches[2]) as $n) { - if ($number == $n) { - return true; - } - } - } else { - $leftNumber = self::convertNumber($matches['left']); - $rightNumber = self::convertNumber($matches['right']); - - return - ('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) - && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) - ; - } - - return false; - } - - /** - * Returns a Regexp that matches valid intervals. - * - * @return string A Regexp (without the delimiters) - */ - public static function getIntervalRegexp() - { - return <<[\[\]]) - \s* - (?P-Inf|\-?\d+(\.\d+)?) - \s*,\s* - (?P\+?Inf|\-?\d+(\.\d+)?) - \s* - (?P[\[\]]) -EOF; - } - - private static function convertNumber(string $number): float - { - if ('-Inf' === $number) { - return log(0); - } elseif ('+Inf' === $number || 'Inf' === $number) { - return -log(0); - } - - return (float) $number; - } -} diff --git a/symfony/translation/Loader/ArrayLoader.php b/symfony/translation/Loader/ArrayLoader.php index 2e9a4285e..0758da8b5 100644 --- a/symfony/translation/Loader/ArrayLoader.php +++ b/symfony/translation/Loader/ArrayLoader.php @@ -23,7 +23,7 @@ class ArrayLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { $resource = $this->flatten($resource); $catalogue = new MessageCatalogue($locale); diff --git a/symfony/translation/Loader/CsvFileLoader.php b/symfony/translation/Loader/CsvFileLoader.php index db17bd563..8d5d4db9a 100644 --- a/symfony/translation/Loader/CsvFileLoader.php +++ b/symfony/translation/Loader/CsvFileLoader.php @@ -27,7 +27,7 @@ class CsvFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $messages = []; @@ -55,12 +55,8 @@ protected function loadResource($resource) /** * Sets the delimiter, enclosure, and escape character for CSV. - * - * @param string $delimiter Delimiter character - * @param string $enclosure Enclosure character - * @param string $escape Escape character */ - public function setCsvControl($delimiter = ';', $enclosure = '"', $escape = '\\') + public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\') { $this->delimiter = $delimiter; $this->enclosure = $enclosure; diff --git a/symfony/translation/Loader/FileLoader.php b/symfony/translation/Loader/FileLoader.php index 42c687d22..4725ea6d6 100644 --- a/symfony/translation/Loader/FileLoader.php +++ b/symfony/translation/Loader/FileLoader.php @@ -23,7 +23,7 @@ abstract class FileLoader extends ArrayLoader /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); @@ -55,11 +55,9 @@ public function load($resource, $locale, $domain = 'messages') } /** - * @param string $resource - * * @return array * * @throws InvalidResourceException if stream content has an invalid format */ - abstract protected function loadResource($resource); + abstract protected function loadResource(string $resource); } diff --git a/symfony/translation/Loader/IcuDatFileLoader.php b/symfony/translation/Loader/IcuDatFileLoader.php index dfdabc9d4..2a1aecc60 100644 --- a/symfony/translation/Loader/IcuDatFileLoader.php +++ b/symfony/translation/Loader/IcuDatFileLoader.php @@ -26,7 +26,7 @@ class IcuDatFileLoader extends IcuResFileLoader /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource.'.dat')) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); diff --git a/symfony/translation/Loader/IcuResFileLoader.php b/symfony/translation/Loader/IcuResFileLoader.php index 126556fee..64bbd3e14 100644 --- a/symfony/translation/Loader/IcuResFileLoader.php +++ b/symfony/translation/Loader/IcuResFileLoader.php @@ -26,7 +26,7 @@ class IcuResFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); @@ -75,7 +75,7 @@ public function load($resource, $locale, $domain = 'messages') * * @return array the flattened ResourceBundle */ - protected function flatten(\ResourceBundle $rb, array &$messages = [], $path = null) + protected function flatten(\ResourceBundle $rb, array &$messages = [], string $path = null) { foreach ($rb as $key => $value) { $nodePath = $path ? $path.'.'.$key : $key; diff --git a/symfony/translation/Loader/IniFileLoader.php b/symfony/translation/Loader/IniFileLoader.php index 11d9b272e..7398f7778 100644 --- a/symfony/translation/Loader/IniFileLoader.php +++ b/symfony/translation/Loader/IniFileLoader.php @@ -21,7 +21,7 @@ class IniFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { return parse_ini_file($resource, true); } diff --git a/symfony/translation/Loader/JsonFileLoader.php b/symfony/translation/Loader/JsonFileLoader.php index 8a8996b0d..5aefba072 100644 --- a/symfony/translation/Loader/JsonFileLoader.php +++ b/symfony/translation/Loader/JsonFileLoader.php @@ -23,7 +23,7 @@ class JsonFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $messages = []; if ($data = file_get_contents($resource)) { diff --git a/symfony/translation/Loader/LoaderInterface.php b/symfony/translation/Loader/LoaderInterface.php index 1785402d9..2073f2b24 100644 --- a/symfony/translation/Loader/LoaderInterface.php +++ b/symfony/translation/Loader/LoaderInterface.php @@ -34,5 +34,5 @@ interface LoaderInterface * @throws NotFoundResourceException when the resource cannot be found * @throws InvalidResourceException when the resource cannot be loaded */ - public function load($resource, $locale, $domain = 'messages'); + public function load($resource, string $locale, string $domain = 'messages'); } diff --git a/symfony/translation/Loader/MoFileLoader.php b/symfony/translation/Loader/MoFileLoader.php index ce8611cdf..7fa2f2dcb 100644 --- a/symfony/translation/Loader/MoFileLoader.php +++ b/symfony/translation/Loader/MoFileLoader.php @@ -41,7 +41,7 @@ class MoFileLoader extends FileLoader * * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $stream = fopen($resource, 'r'); diff --git a/symfony/translation/Loader/PhpFileLoader.php b/symfony/translation/Loader/PhpFileLoader.php index c361d5293..85f10902b 100644 --- a/symfony/translation/Loader/PhpFileLoader.php +++ b/symfony/translation/Loader/PhpFileLoader.php @@ -23,7 +23,7 @@ class PhpFileLoader extends FileLoader /** * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN))) { self::$cache = null; diff --git a/symfony/translation/Loader/PoFileLoader.php b/symfony/translation/Loader/PoFileLoader.php index 5e460fbfb..ee143e203 100644 --- a/symfony/translation/Loader/PoFileLoader.php +++ b/symfony/translation/Loader/PoFileLoader.php @@ -60,7 +60,7 @@ class PoFileLoader extends FileLoader * * {@inheritdoc} */ - protected function loadResource($resource) + protected function loadResource(string $resource) { $stream = fopen($resource, 'r'); diff --git a/symfony/translation/Loader/QtFileLoader.php b/symfony/translation/Loader/QtFileLoader.php index 29567789e..9cf2fe976 100644 --- a/symfony/translation/Loader/QtFileLoader.php +++ b/symfony/translation/Loader/QtFileLoader.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** @@ -27,8 +28,12 @@ class QtFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.'); + } + if (!stream_is_local($resource)) { throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); } diff --git a/symfony/translation/Loader/XliffFileLoader.php b/symfony/translation/Loader/XliffFileLoader.php index 35e2d9c10..5c9794a54 100644 --- a/symfony/translation/Loader/XliffFileLoader.php +++ b/symfony/translation/Loader/XliffFileLoader.php @@ -12,9 +12,12 @@ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; use Symfony\Component\Config\Util\XmlUtils; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\XliffUtils; @@ -28,38 +31,53 @@ class XliffFileLoader implements LoaderInterface /** * {@inheritdoc} */ - public function load($resource, $locale, $domain = 'messages') + public function load($resource, string $locale, string $domain = 'messages') { - if (!stream_is_local($resource)) { - throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); } - if (!file_exists($resource)) { - throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + if (!$this->isXmlString($resource)) { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + if (!is_file($resource)) { + throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); + } + } + + try { + if ($this->isXmlString($resource)) { + $dom = XmlUtils::parse($resource); + } else { + $dom = XmlUtils::loadFile($resource); + } + } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); + } + + if ($errors = XliffUtils::validateSchema($dom)) { + throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); } $catalogue = new MessageCatalogue($locale); - $this->extract($resource, $catalogue, $domain); + $this->extract($dom, $catalogue, $domain); - if (class_exists(FileResource::class)) { + if (is_file($resource) && class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } - private function extract($resource, MessageCatalogue $catalogue, string $domain) + private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain) { - try { - $dom = XmlUtils::loadFile($resource); - } catch (\InvalidArgumentException $e) { - throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); - } - $xliffVersion = XliffUtils::getVersionNumber($dom); - if ($errors = XliffUtils::validateSchema($dom)) { - throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); - } if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); @@ -135,11 +153,12 @@ private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, s foreach ($xml->xpath('//xliff:unit') as $unit) { foreach ($unit->segment as $segment) { - $source = $segment->source; + $attributes = $unit->attributes(); + $source = $attributes['name'] ?? $segment->source; // If the xlf file has another encoding specified, try to convert it because // simple_xml will always return utf-8 encoded values - $target = $this->utf8ToCharset((string) ($segment->target ?? $source), $encoding); + $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); $catalogue->set((string) $source, $target, $domain); @@ -205,4 +224,9 @@ private function parseNotesMetadata(\SimpleXMLElement $noteElement = null, strin return $notes; } + + private function isXmlString(string $resource): bool + { + return 0 === strpos($resource, 'yamlParser) { if (!class_exists(\Symfony\Component\Yaml\Parser::class)) { diff --git a/symfony/translation/LoggingTranslator.php b/symfony/translation/LoggingTranslator.php index d7b6fc4b3..bb93435c3 100644 --- a/symfony/translation/LoggingTranslator.php +++ b/symfony/translation/LoggingTranslator.php @@ -13,14 +13,13 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterface, TranslatorBagInterface +class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var TranslatorInterface|TranslatorBagInterface @@ -32,13 +31,10 @@ class LoggingTranslator implements TranslatorInterface, LegacyTranslatorInterfac /** * @param TranslatorInterface $translator The translator must implement TranslatorBagInterface */ - public function __construct($translator, LoggerInterface $logger) + public function __construct(TranslatorInterface $translator, LoggerInterface $logger) { - if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); - } if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { - throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', \get_class($translator))); + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); } $this->translator = $translator; @@ -48,9 +44,9 @@ public function __construct($translator, LoggerInterface $logger) /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - $trans = $this->translator->trans($id, $parameters, $domain, $locale); + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->log($id, $domain, $locale); return $trans; @@ -58,28 +54,8 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul /** * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ($this->translator instanceof TranslatorInterface) { - $trans = $this->translator->trans($id, ['%count%' => $number] + $parameters, $domain, $locale); - } else { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - - $this->log($id, $domain, $locale); - - return $trans; - } - - /** - * {@inheritdoc} - */ - public function setLocale($locale) + public function setLocale(string $locale) { $prev = $this->translator->getLocale(); $this->translator->setLocale($locale); @@ -101,11 +77,19 @@ public function getLocale() /** * {@inheritdoc} */ - public function getCatalogue($locale = null) + public function getCatalogue(string $locale = null) { return $this->translator->getCatalogue($locale); } + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return $this->translator->getCatalogues(); + } + /** * Gets the fallback locales. * @@ -123,7 +107,7 @@ public function getFallbackLocales() /** * Passes through all unknown calls onto the translator object. */ - public function __call($method, $args) + public function __call(string $method, array $args) { return $this->translator->{$method}(...$args); } @@ -131,13 +115,12 @@ public function __call($method, $args) /** * Logs for missing translations. */ - private function log(?string $id, ?string $domain, ?string $locale) + private function log(string $id, ?string $domain, ?string $locale) { if (null === $domain) { $domain = 'messages'; } - $id = (string) $id; $catalogue = $this->translator->getCatalogue($locale); if ($catalogue->defines($id, $domain)) { return; diff --git a/symfony/translation/MessageCatalogue.php b/symfony/translation/MessageCatalogue.php index 6e6b9fe0e..ff49b5a97 100644 --- a/symfony/translation/MessageCatalogue.php +++ b/symfony/translation/MessageCatalogue.php @@ -29,12 +29,8 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf /** * @param array $messages An array of messages classified by domain */ - public function __construct(?string $locale, array $messages = []) + public function __construct(string $locale, array $messages = []) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" to the first argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED); - } - $this->locale = $locale; $this->messages = $messages; } @@ -67,7 +63,7 @@ public function getDomains() /** * {@inheritdoc} */ - public function all($domain = null) + public function all(string $domain = null) { if (null !== $domain) { // skip messages merge if intl-icu requested explicitly @@ -95,7 +91,7 @@ public function all($domain = null) /** * {@inheritdoc} */ - public function set($id, $translation, $domain = 'messages') + public function set(string $id, string $translation, string $domain = 'messages') { $this->add([$id => $translation], $domain); } @@ -103,7 +99,7 @@ public function set($id, $translation, $domain = 'messages') /** * {@inheritdoc} */ - public function has($id, $domain = 'messages') + public function has(string $id, string $domain = 'messages') { if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return true; @@ -119,7 +115,7 @@ public function has($id, $domain = 'messages') /** * {@inheritdoc} */ - public function defines($id, $domain = 'messages') + public function defines(string $id, string $domain = 'messages') { return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); } @@ -127,7 +123,7 @@ public function defines($id, $domain = 'messages') /** * {@inheritdoc} */ - public function get($id, $domain = 'messages') + public function get(string $id, string $domain = 'messages') { if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; @@ -147,7 +143,7 @@ public function get($id, $domain = 'messages') /** * {@inheritdoc} */ - public function replace($messages, $domain = 'messages') + public function replace(array $messages, string $domain = 'messages') { unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); @@ -157,7 +153,7 @@ public function replace($messages, $domain = 'messages') /** * {@inheritdoc} */ - public function add($messages, $domain = 'messages') + public function add(array $messages, string $domain = 'messages') { if (!isset($this->messages[$domain])) { $this->messages[$domain] = []; @@ -261,7 +257,7 @@ public function addResource(ResourceInterface $resource) /** * {@inheritdoc} */ - public function getMetadata($key = '', $domain = 'messages') + public function getMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { return $this->metadata; @@ -283,7 +279,7 @@ public function getMetadata($key = '', $domain = 'messages') /** * {@inheritdoc} */ - public function setMetadata($key, $value, $domain = 'messages') + public function setMetadata(string $key, $value, string $domain = 'messages') { $this->metadata[$domain][$key] = $value; } @@ -291,7 +287,7 @@ public function setMetadata($key, $value, $domain = 'messages') /** * {@inheritdoc} */ - public function deleteMetadata($key = '', $domain = 'messages') + public function deleteMetadata(string $key = '', string $domain = 'messages') { if ('' == $domain) { $this->metadata = []; diff --git a/symfony/translation/MessageCatalogueInterface.php b/symfony/translation/MessageCatalogueInterface.php index 853ce7031..5d83bd8a9 100644 --- a/symfony/translation/MessageCatalogueInterface.php +++ b/symfony/translation/MessageCatalogueInterface.php @@ -45,7 +45,7 @@ public function getDomains(); * * @return array An array of messages */ - public function all($domain = null); + public function all(string $domain = null); /** * Sets a message translation. @@ -54,7 +54,7 @@ public function all($domain = null); * @param string $translation The messages translation * @param string $domain The domain name */ - public function set($id, $translation, $domain = 'messages'); + public function set(string $id, string $translation, string $domain = 'messages'); /** * Checks if a message has a translation. @@ -64,7 +64,7 @@ public function set($id, $translation, $domain = 'messages'); * * @return bool true if the message has a translation, false otherwise */ - public function has($id, $domain = 'messages'); + public function has(string $id, string $domain = 'messages'); /** * Checks if a message has a translation (it does not take into account the fallback mechanism). @@ -74,7 +74,7 @@ public function has($id, $domain = 'messages'); * * @return bool true if the message has a translation, false otherwise */ - public function defines($id, $domain = 'messages'); + public function defines(string $id, string $domain = 'messages'); /** * Gets a message translation. @@ -84,7 +84,7 @@ public function defines($id, $domain = 'messages'); * * @return string The message translation */ - public function get($id, $domain = 'messages'); + public function get(string $id, string $domain = 'messages'); /** * Sets translations for a given domain. @@ -92,7 +92,7 @@ public function get($id, $domain = 'messages'); * @param array $messages An array of translations * @param string $domain The domain name */ - public function replace($messages, $domain = 'messages'); + public function replace(array $messages, string $domain = 'messages'); /** * Adds translations for a given domain. @@ -100,7 +100,7 @@ public function replace($messages, $domain = 'messages'); * @param array $messages An array of translations * @param string $domain The domain name */ - public function add($messages, $domain = 'messages'); + public function add(array $messages, string $domain = 'messages'); /** * Merges translations from the given Catalogue into the current one. diff --git a/symfony/translation/MessageSelector.php b/symfony/translation/MessageSelector.php deleted file mode 100644 index 38de357a5..000000000 --- a/symfony/translation/MessageSelector.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\Translation; - -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', MessageSelector::class), \E_USER_DEPRECATED); - -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * MessageSelector. - * - * @author Fabien Potencier - * @author Bernhard Schussek - * - * @deprecated since Symfony 4.2, use IdentityTranslator instead. - */ -class MessageSelector -{ - /** - * Given a message with different plural translations separated by a - * pipe (|), this method returns the correct portion of the message based - * on the given number, locale and the pluralization rules in the message - * itself. - * - * The message supports two different types of pluralization rules: - * - * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples - * indexed: There is one apple|There are %count% apples - * - * The indexed solution can also contain labels (e.g. one: There is one apple). - * This is purely for making the translations more clear - it does not - * affect the functionality. - * - * The two methods can also be mixed: - * {0} There are no apples|one: There is one apple|more: There are %count% apples - * - * @param string $message The message being translated - * @param int|float $number The number of items represented for the message - * @param string $locale The locale to use for choosing - * - * @return string - * - * @throws InvalidArgumentException - */ - public function choose($message, $number, $locale) - { - $parts = []; - if (preg_match('/^\|++$/', $message)) { - $parts = explode('|', $message); - } elseif (preg_match_all('/(?:\|\||[^\|])++/', $message, $matches)) { - $parts = $matches[0]; - } - - $explicitRules = []; - $standardRules = []; - foreach ($parts as $part) { - $part = trim(str_replace('||', '|', $part)); - - if (preg_match('/^(?P'.Interval::getIntervalRegexp().')\s*(?P.*?)$/xs', $part, $matches)) { - $explicitRules[$matches['interval']] = $matches['message']; - } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { - $standardRules[] = $matches[1]; - } else { - $standardRules[] = $part; - } - } - - // try to match an explicit rule, then fallback to the standard ones - foreach ($explicitRules as $interval => $m) { - if (Interval::test($number, $interval)) { - return $m; - } - } - - $position = PluralizationRules::get($number, $locale); - - if (!isset($standardRules[$position])) { - // when there's exactly one rule given, and that rule is a standard - // rule, use this rule - if (1 === \count($parts) && isset($standardRules[0])) { - return $standardRules[0]; - } - - throw new InvalidArgumentException(sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $message, $locale, $number)); - } - - return $standardRules[$position]; - } -} diff --git a/symfony/translation/MetadataAwareInterface.php b/symfony/translation/MetadataAwareInterface.php index e93c6fbc7..2216eed94 100644 --- a/symfony/translation/MetadataAwareInterface.php +++ b/symfony/translation/MetadataAwareInterface.php @@ -25,30 +25,22 @@ interface MetadataAwareInterface * domain and then by key. Passing an empty key will return an array with all * metadata for the given domain. * - * @param string $key The key - * @param string $domain The domain name - * * @return mixed The value that was set or an array with the domains/keys or null */ - public function getMetadata($key = '', $domain = 'messages'); + public function getMetadata(string $key = '', string $domain = 'messages'); /** * Adds metadata to a message domain. * - * @param string $key The key - * @param mixed $value The value - * @param string $domain The domain name + * @param mixed $value */ - public function setMetadata($key, $value, $domain = 'messages'); + public function setMetadata(string $key, $value, string $domain = 'messages'); /** * Deletes metadata for the given key and domain. * * Passing an empty domain will delete all metadata. Passing an empty key will * delete all metadata for the given domain. - * - * @param string $key The key - * @param string $domain The domain name */ - public function deleteMetadata($key = '', $domain = 'messages'); + public function deleteMetadata(string $key = '', string $domain = 'messages'); } diff --git a/symfony/translation/PluralizationRules.php b/symfony/translation/PluralizationRules.php deleted file mode 100644 index e69ceabc1..000000000 --- a/symfony/translation/PluralizationRules.php +++ /dev/null @@ -1,221 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -/** - * Returns the plural rules for a given locale. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use IdentityTranslator instead - */ -class PluralizationRules -{ - private static $rules = []; - - /** - * Returns the plural position to use for the given locale and number. - * - * @param float $number The number - * @param string $locale The locale - * - * @return int The plural position - */ - public static function get($number, $locale/*, bool $triggerDeprecation = true*/) - { - $number = abs($number); - - if (3 > \func_num_args() || func_get_arg(2)) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); - } - - if ('pt_BR' === $locale) { - // temporary set a locale for brazilian - $locale = 'xbr'; - } - - if ('en_US_POSIX' !== $locale && \strlen($locale) > 3) { - $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); - } - - if (isset(self::$rules[$locale])) { - $return = self::$rules[$locale]($number); - - if (!\is_int($return) || $return < 0) { - return 0; - } - - return $return; - } - - /* - * The plural rules are derived from code of the Zend Framework (2010-09-25), - * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). - * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) - */ - switch ($locale) { - case 'az': - case 'bo': - case 'dz': - case 'id': - case 'ja': - case 'jv': - case 'ka': - case 'km': - case 'kn': - case 'ko': - case 'ms': - case 'th': - case 'tr': - case 'vi': - case 'zh': - return 0; - - case 'af': - case 'bn': - case 'bg': - case 'ca': - case 'da': - case 'de': - case 'el': - case 'en': - case 'en_US_POSIX': - case 'eo': - case 'es': - case 'et': - case 'eu': - case 'fa': - case 'fi': - case 'fo': - case 'fur': - case 'fy': - case 'gl': - case 'gu': - case 'ha': - case 'he': - case 'hu': - case 'is': - case 'it': - case 'ku': - case 'lb': - case 'ml': - case 'mn': - case 'mr': - case 'nah': - case 'nb': - case 'ne': - case 'nl': - case 'nn': - case 'no': - case 'oc': - case 'om': - case 'or': - case 'pa': - case 'pap': - case 'ps': - case 'pt': - case 'so': - case 'sq': - case 'sv': - case 'sw': - case 'ta': - case 'te': - case 'tk': - case 'ur': - case 'zu': - return (1 == $number) ? 0 : 1; - - case 'am': - case 'bh': - case 'fil': - case 'fr': - case 'gun': - case 'hi': - case 'hy': - case 'ln': - case 'mg': - case 'nso': - case 'xbr': - case 'ti': - case 'wa': - return ($number < 2) ? 0 : 1; - - case 'be': - case 'bs': - case 'hr': - case 'ru': - case 'sh': - case 'sr': - case 'uk': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'cs': - case 'sk': - return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); - - case 'ga': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); - - case 'lt': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'sl': - return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); - - case 'mk': - return (1 == $number % 10) ? 0 : 1; - - case 'mt': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); - - case 'lv': - return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); - - case 'pl': - return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); - - case 'cy': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); - - case 'ro': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); - - case 'ar': - return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); - - default: - return 0; - } - } - - /** - * Overrides the default plural rule for a given locale. - * - * @param callable $rule A PHP callable - * @param string $locale The locale - */ - public static function set(callable $rule, $locale) - { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), \E_USER_DEPRECATED); - - if ('pt_BR' === $locale) { - // temporary set a locale for brazilian - $locale = 'xbr'; - } - - if (\strlen($locale) > 3) { - $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); - } - - self::$rules[$locale] = $rule; - } -} diff --git a/symfony/translation/Provider/AbstractProviderFactory.php b/symfony/translation/Provider/AbstractProviderFactory.php new file mode 100644 index 000000000..17442fde8 --- /dev/null +++ b/symfony/translation/Provider/AbstractProviderFactory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; + +abstract class AbstractProviderFactory implements ProviderFactoryInterface +{ + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); + } + + /** + * @return string[] + */ + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + if (null === $user = $dsn->getUser()) { + throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn()); + } + + return $user; + } + + protected function getPassword(Dsn $dsn): string + { + if (null === $password = $dsn->getPassword()) { + throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); + } + + return $password; + } +} diff --git a/symfony/translation/Provider/Dsn.php b/symfony/translation/Provider/Dsn.php new file mode 100644 index 000000000..820cabfb3 --- /dev/null +++ b/symfony/translation/Provider/Dsn.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\MissingRequiredOptionException; + +/** + * @author Fabien Potencier + * @author Oskar Stark + */ +final class Dsn +{ + private $scheme; + private $host; + private $user; + private $password; + private $port; + private $path; + private $options; + private $originalDsn; + + public function __construct(string $dsn) + { + $this->originalDsn = $dsn; + + if (false === $parsedDsn = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn)); + } + + if (!isset($parsedDsn['scheme'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn)); + } + $this->scheme = $parsedDsn['scheme']; + + if (!isset($parsedDsn['host'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn)); + } + $this->host = $parsedDsn['host']; + + $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; + $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; + $this->port = $parsedDsn['port'] ?? null; + $this->path = $parsedDsn['path'] ?? null; + parse_str($parsedDsn['query'] ?? '', $this->options); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } + + public function getRequiredOption(string $key) + { + if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) { + throw new MissingRequiredOptionException($key); + } + + return $this->options[$key]; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getOriginalDsn(): string + { + return $this->originalDsn; + } +} diff --git a/symfony/translation/Provider/FilteringProvider.php b/symfony/translation/Provider/FilteringProvider.php new file mode 100644 index 000000000..0307cdacf --- /dev/null +++ b/symfony/translation/Provider/FilteringProvider.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * Filters domains and locales between the Translator config values and those specific to each provider. + * + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +class FilteringProvider implements ProviderInterface +{ + private $provider; + private $locales; + private $domains; + + public function __construct(ProviderInterface $provider, array $locales, array $domains = []) + { + $this->provider = $provider; + $this->locales = $locales; + $this->domains = $domains; + } + + public function __toString(): string + { + return (string) $this->provider; + } + + /** + * {@inheritdoc} + */ + public function write(TranslatorBagInterface $translatorBag): void + { + $this->provider->write($translatorBag); + } + + public function read(array $domains, array $locales): TranslatorBag + { + $domains = !$this->domains ? $domains : array_intersect($this->domains, $domains); + $locales = array_intersect($this->locales, $locales); + + return $this->provider->read($domains, $locales); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + $this->provider->delete($translatorBag); + } + + public function getDomains(): array + { + return $this->domains; + } +} diff --git a/symfony/translation/Provider/NullProvider.php b/symfony/translation/Provider/NullProvider.php new file mode 100644 index 000000000..785fcaa60 --- /dev/null +++ b/symfony/translation/Provider/NullProvider.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +class NullProvider implements ProviderInterface +{ + public function __toString(): string + { + return 'null'; + } + + public function write(TranslatorBagInterface $translatorBag, bool $override = false): void + { + } + + public function read(array $domains, array $locales): TranslatorBag + { + return new TranslatorBag(); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + } +} diff --git a/symfony/translation/Provider/NullProviderFactory.php b/symfony/translation/Provider/NullProviderFactory.php new file mode 100644 index 000000000..6ddbd8572 --- /dev/null +++ b/symfony/translation/Provider/NullProviderFactory.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +final class NullProviderFactory extends AbstractProviderFactory +{ + public function create(Dsn $dsn): ProviderInterface + { + if ('null' === $dsn->getScheme()) { + return new NullProvider(); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/symfony/translation/Provider/ProviderFactoryInterface.php b/symfony/translation/Provider/ProviderFactoryInterface.php new file mode 100644 index 000000000..3fd4494b4 --- /dev/null +++ b/symfony/translation/Provider/ProviderFactoryInterface.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\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +interface ProviderFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): ProviderInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/symfony/translation/Provider/ProviderInterface.php b/symfony/translation/Provider/ProviderInterface.php new file mode 100644 index 000000000..a32193f29 --- /dev/null +++ b/symfony/translation/Provider/ProviderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +interface ProviderInterface +{ + public function __toString(): string; + + /** + * Translations available in the TranslatorBag only must be created. + * Translations available in both the TranslatorBag and on the provider + * must be overwritten. + * Translations available on the provider only must be kept. + */ + public function write(TranslatorBagInterface $translatorBag): void; + + public function read(array $domains, array $locales): TranslatorBag; + + public function delete(TranslatorBagInterface $translatorBag): void; +} diff --git a/symfony/translation/Provider/TranslationProviderCollection.php b/symfony/translation/Provider/TranslationProviderCollection.php new file mode 100644 index 000000000..9963cb9f8 --- /dev/null +++ b/symfony/translation/Provider/TranslationProviderCollection.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +final class TranslationProviderCollection +{ + private $providers; + + /** + * @param array $providers + */ + public function __construct(iterable $providers) + { + $this->providers = []; + foreach ($providers as $name => $provider) { + $this->providers[$name] = $provider; + } + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->providers)).']'; + } + + public function has(string $name): bool + { + return isset($this->providers[$name]); + } + + public function get(string $name): ProviderInterface + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this)); + } + + return $this->providers[$name]; + } + + public function keys(): array + { + return array_keys($this->providers); + } +} diff --git a/symfony/translation/Provider/TranslationProviderCollectionFactory.php b/symfony/translation/Provider/TranslationProviderCollectionFactory.php new file mode 100644 index 000000000..43f4a344c --- /dev/null +++ b/symfony/translation/Provider/TranslationProviderCollectionFactory.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + * + * @experimental in 5.3 + */ +class TranslationProviderCollectionFactory +{ + private $factories; + private $enabledLocales; + + /** + * @param ProviderFactoryInterface[] $factories + */ + public function __construct(iterable $factories, array $enabledLocales) + { + $this->factories = $factories; + $this->enabledLocales = $enabledLocales; + } + + public function fromConfig(array $config): TranslationProviderCollection + { + $providers = []; + foreach ($config as $name => $currentConfig) { + $providers[$name] = $this->fromDsnObject( + new Dsn($currentConfig['dsn']), + !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], + !$currentConfig['domains'] ? [] : $currentConfig['domains'] + ); + } + + return new TranslationProviderCollection($providers); + } + + public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return new FilteringProvider($factory->create($dsn), $locales, $domains); + } + } + + throw new UnsupportedSchemeException($dsn); + } +} diff --git a/symfony/translation/PseudoLocalizationTranslator.php b/symfony/translation/PseudoLocalizationTranslator.php new file mode 100644 index 000000000..49f122eb8 --- /dev/null +++ b/symfony/translation/PseudoLocalizationTranslator.php @@ -0,0 +1,364 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This translator should only be used in a development environment. + */ +final class PseudoLocalizationTranslator implements TranslatorInterface +{ + private const EXPANSION_CHARACTER = '~'; + + private $translator; + private $accents; + private $expansionFactor; + private $brackets; + private $parseHTML; + private $localizableHTMLAttributes; + + /** + * Available options: + * * accents: + * type: boolean + * default: true + * description: replace ASCII characters of the translated string with accented versions or similar characters + * example: if true, "foo" => "ƒöö". + * + * * expansion_factor: + * type: float + * default: 1 + * validation: it must be greater than or equal to 1 + * description: expand the translated string by the given factor with spaces and tildes + * example: if 2, "foo" => "~foo ~" + * + * * brackets: + * type: boolean + * default: true + * description: wrap the translated string with brackets + * example: if true, "foo" => "[foo]" + * + * * parse_html: + * type: boolean + * default: false + * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML + * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo
bar" => "foo
bar
" + * + * * localizable_html_attributes: + * type: string[] + * default: [] + * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true + * example: if ["title"], and with the "accents" option set to true, "Profile" => "Þŕöƒîļé" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. + */ + public function __construct(TranslatorInterface $translator, array $options = []) + { + $this->translator = $translator; + $this->accents = $options['accents'] ?? true; + + if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { + throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); + } + + $this->brackets = $options['brackets'] ?? true; + + $this->parseHTML = $options['parse_html'] ?? false; + if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { + $this->parseHTML = false; + } + + $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null) + { + $trans = ''; + $visibleText = ''; + + foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { + if ($visible) { + $visibleText .= $text; + } + + if (!$localizable) { + $trans .= $text; + + continue; + } + + $this->addAccents($trans, $text); + } + + $this->expand($trans, $visibleText); + + $this->addBrackets($trans); + + return $trans; + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + private function getParts(string $originalTrans): array + { + if (!$this->parseHTML) { + return [[true, true, $originalTrans]]; + } + + $html = mb_convert_encoding($originalTrans, 'HTML-ENTITIES', mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); + + $useInternalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument(); + $dom->loadHTML(''.$html.''); + + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + + return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); + } + + private function parseNode(\DOMNode $node): array + { + $parts = []; + + foreach ($node->childNodes as $childNode) { + if (!$childNode instanceof \DOMElement) { + $parts[] = [true, true, $childNode->nodeValue]; + + continue; + } + + $parts[] = [false, false, '<'.$childNode->tagName]; + + /** @var \DOMAttr $attribute */ + foreach ($childNode->attributes as $attribute) { + $parts[] = [false, false, ' '.$attribute->nodeName.'="']; + + $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); + foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { + if ('' === $match) { + continue; + } + + $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; + } + + $parts[] = [false, false, '"']; + } + + $parts[] = [false, false, '>']; + + $parts = array_merge($parts, $this->parseNode($childNode, $parts)); + + $parts[] = [false, false, 'tagName.'>']; + } + + return $parts; + } + + private function addAccents(string &$trans, string $text): void + { + $trans .= $this->accents ? strtr($text, [ + ' ' => ' ', + '!' => '¡', + '"' => '″', + '#' => '♯', + '$' => '€', + '%' => '‰', + '&' => '⅋', + '\'' => '´', + '(' => '{', + ')' => '}', + '*' => '⁎', + '+' => '⁺', + ',' => '،', + '-' => '‐', + '.' => '·', + '/' => '⁄', + '0' => '⓪', + '1' => '①', + '2' => '②', + '3' => '③', + '4' => '④', + '5' => '⑤', + '6' => '⑥', + '7' => '⑦', + '8' => '⑧', + '9' => '⑨', + ':' => '∶', + ';' => '⁏', + '<' => '≤', + '=' => '≂', + '>' => '≥', + '?' => '¿', + '@' => '՞', + 'A' => 'Å', + 'B' => 'Ɓ', + 'C' => 'Ç', + 'D' => 'Ð', + 'E' => 'É', + 'F' => 'Ƒ', + 'G' => 'Ĝ', + 'H' => 'Ĥ', + 'I' => 'Î', + 'J' => 'Ĵ', + 'K' => 'Ķ', + 'L' => 'Ļ', + 'M' => 'Ṁ', + 'N' => 'Ñ', + 'O' => 'Ö', + 'P' => 'Þ', + 'Q' => 'Ǫ', + 'R' => 'Ŕ', + 'S' => 'Š', + 'T' => 'Ţ', + 'U' => 'Û', + 'V' => 'Ṽ', + 'W' => 'Ŵ', + 'X' => 'Ẋ', + 'Y' => 'Ý', + 'Z' => 'Ž', + '[' => '⁅', + '\\' => '∖', + ']' => '⁆', + '^' => '˄', + '_' => '‿', + '`' => '‵', + 'a' => 'å', + 'b' => 'ƀ', + 'c' => 'ç', + 'd' => 'ð', + 'e' => 'é', + 'f' => 'ƒ', + 'g' => 'ĝ', + 'h' => 'ĥ', + 'i' => 'î', + 'j' => 'ĵ', + 'k' => 'ķ', + 'l' => 'ļ', + 'm' => 'ɱ', + 'n' => 'ñ', + 'o' => 'ö', + 'p' => 'þ', + 'q' => 'ǫ', + 'r' => 'ŕ', + 's' => 'š', + 't' => 'ţ', + 'u' => 'û', + 'v' => 'ṽ', + 'w' => 'ŵ', + 'x' => 'ẋ', + 'y' => 'ý', + 'z' => 'ž', + '{' => '(', + '|' => '¦', + '}' => ')', + '~' => '˞', + ]) : $text; + } + + private function expand(string &$trans, string $visibleText): void + { + if (1.0 >= $this->expansionFactor) { + return; + } + + $visibleLength = $this->strlen($visibleText); + $missingLength = (int) (ceil($visibleLength * $this->expansionFactor)) - $visibleLength; + if ($this->brackets) { + $missingLength -= 2; + } + + if (0 >= $missingLength) { + return; + } + + $words = []; + $wordsCount = 0; + foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { + $wordLength = $this->strlen($word); + + if ($wordLength >= $missingLength) { + continue; + } + + if (!isset($words[$wordLength])) { + $words[$wordLength] = 0; + } + + ++$words[$wordLength]; + ++$wordsCount; + } + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + arsort($words, \SORT_NUMERIC); + + $longestWordLength = max(array_keys($words)); + + while (true) { + $r = mt_rand(1, $wordsCount); + + foreach ($words as $length => $count) { + $r -= $count; + if ($r <= 0) { + break; + } + } + + $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); + + $missingLength -= $length + 1; + + if (0 === $missingLength) { + return; + } + + while ($longestWordLength >= $missingLength) { + $wordsCount -= $words[$longestWordLength]; + unset($words[$longestWordLength]); + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + $longestWordLength = max(array_keys($words)); + } + } + } + + private function addBrackets(string &$trans): void + { + if (!$this->brackets) { + return; + } + + $trans = '['.$trans.']'; + } + + private function strlen(string $s): int + { + return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); + } +} diff --git a/symfony/translation/Reader/TranslationReader.php b/symfony/translation/Reader/TranslationReader.php index 2b9834521..9e51b15b5 100644 --- a/symfony/translation/Reader/TranslationReader.php +++ b/symfony/translation/Reader/TranslationReader.php @@ -34,7 +34,7 @@ class TranslationReader implements TranslationReaderInterface * * @param string $format The format of the loader */ - public function addLoader($format, LoaderInterface $loader) + public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } @@ -42,7 +42,7 @@ public function addLoader($format, LoaderInterface $loader) /** * {@inheritdoc} */ - public function read($directory, MessageCatalogue $catalogue) + public function read(string $directory, MessageCatalogue $catalogue) { if (!is_dir($directory)) { return; diff --git a/symfony/translation/Reader/TranslationReaderInterface.php b/symfony/translation/Reader/TranslationReaderInterface.php index 0b2ad332a..bc37204f9 100644 --- a/symfony/translation/Reader/TranslationReaderInterface.php +++ b/symfony/translation/Reader/TranslationReaderInterface.php @@ -22,8 +22,6 @@ interface TranslationReaderInterface { /** * Reads translation messages from a directory to the catalogue. - * - * @param string $directory */ - public function read($directory, MessageCatalogue $catalogue); + public function read(string $directory, MessageCatalogue $catalogue); } diff --git a/symfony/translation/Resources/data/parents.json b/symfony/translation/Resources/data/parents.json index 32a33cdaf..288f16300 100644 --- a/symfony/translation/Resources/data/parents.json +++ b/symfony/translation/Resources/data/parents.json @@ -54,7 +54,6 @@ "en_MS": "en_001", "en_MT": "en_001", "en_MU": "en_001", - "en_MV": "en_001", "en_MW": "en_001", "en_MY": "en_001", "en_NA": "en_001", @@ -117,8 +116,6 @@ "es_UY": "es_419", "es_VE": "es_419", "ff_Adlm": "root", - "hi_Latn": "en_IN", - "ks_Deva": "root", "nb": "no", "nn": "no", "pa_Arab": "root", diff --git a/symfony/translation/Resources/functions.php b/symfony/translation/Resources/functions.php new file mode 100644 index 000000000..901d2f87e --- /dev/null +++ b/symfony/translation/Resources/functions.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +if (!\function_exists(t::class)) { + /** + * @author Nate Wiebe + */ + function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage + { + return new TranslatableMessage($message, $parameters, $domain); + } +} diff --git a/symfony/translation/TranslatableMessage.php b/symfony/translation/TranslatableMessage.php new file mode 100644 index 000000000..82ae6d724 --- /dev/null +++ b/symfony/translation/TranslatableMessage.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Nate Wiebe + */ +class TranslatableMessage implements TranslatableInterface +{ + private $message; + private $parameters; + private $domain; + + public function __construct(string $message, array $parameters = [], string $domain = null) + { + $this->message = $message; + $this->parameters = $parameters; + $this->domain = $domain; + } + + public function __toString(): string + { + return $this->getMessage(); + } + + public function getMessage(): string + { + return $this->message; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function trans(TranslatorInterface $translator, string $locale = null): string + { + return $translator->trans($this->getMessage(), $this->getParameters(), $this->getDomain(), $locale); + } +} diff --git a/symfony/translation/Translator.php b/symfony/translation/Translator.php index 5eb0183cb..ccdf88f11 100644 --- a/symfony/translation/Translator.php +++ b/symfony/translation/Translator.php @@ -15,21 +15,22 @@ use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; -use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\LoaderInterface; -use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; +// Help opcache.preload discover always-needed symbols +class_exists(MessageCatalogue::class); + /** * @author Fabien Potencier */ -class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface +class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var MessageCatalogueInterface[] @@ -88,13 +89,9 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran /** * @throws InvalidArgumentException If a locale contains invalid characters */ - public function __construct(?string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) + public function __construct(string $locale, MessageFormatterInterface $formatter = null, string $cacheDir = null, bool $debug = false, array $cacheVary = []) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } - - $this->setLocale($locale, false); + $this->setLocale($locale); if (null === $formatter) { $formatter = new MessageFormatter(); @@ -117,7 +114,7 @@ public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFa * * @param string $format The name of the loader (@see addResource()) */ - public function addLoader($format, LoaderInterface $loader) + public function addLoader(string $format, LoaderInterface $loader) { $this->loaders[$format] = $loader; } @@ -127,21 +124,15 @@ public function addLoader($format, LoaderInterface $loader) * * @param string $format The name of the loader (@see addLoader()) * @param mixed $resource The resource name - * @param string $locale The locale - * @param string $domain The domain * * @throws InvalidArgumentException If the locale contains invalid characters */ - public function addResource($format, $resource, $locale, $domain = null) + public function addResource(string $format, $resource, string $locale, string $domain = null) { if (null === $domain) { $domain = 'messages'; } - if (null === $locale) { - @trigger_error(sprintf('Passing "null" to the third argument of the "%s" method has been deprecated since Symfony 4.4 and will throw an error in 5.0.', __METHOD__), \E_USER_DEPRECATED); - } - $this->assertValidLocale($locale); $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; @@ -157,12 +148,8 @@ public function addResource($format, $resource, $locale, $domain = null) /** * {@inheritdoc} */ - public function setLocale($locale) + public function setLocale(string $locale) { - if (null === $locale && (2 > \func_num_args() || func_get_arg(1))) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } - $this->assertValidLocale($locale); $this->locale = $locale; } @@ -186,9 +173,6 @@ public function setFallbackLocales(array $locales) $this->catalogues = []; foreach ($locales as $locale) { - if (null === $locale) { - @trigger_error(sprintf('Passing "null" as the $locale argument to %s() is deprecated since Symfony 4.4.', __METHOD__), \E_USER_DEPRECATED); - } $this->assertValidLocale($locale); } @@ -198,11 +182,9 @@ public function setFallbackLocales(array $locales) /** * Gets the fallback locales. * - * @internal since Symfony 4.2 - * - * @return array The fallback locales + * @internal */ - public function getFallbackLocales() + public function getFallbackLocales(): array { return $this->fallbackLocales; } @@ -210,9 +192,9 @@ public function getFallbackLocales() /** * {@inheritdoc} */ - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null) { - if ('' === $id = (string) $id) { + if (null === $id || '' === $id) { return ''; } @@ -231,7 +213,11 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul } } - if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { + $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX); + if ($this->hasIntlFormatter + && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX) + || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len))) + ) { return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters); } @@ -240,47 +226,8 @@ public function trans($id, array $parameters = [], $domain = null, $locale = nul /** * {@inheritdoc} - * - * @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); - - if ('' === $id = (string) $id) { - return ''; - } - - if (!$this->formatter instanceof ChoiceMessageFormatterInterface) { - throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter))); - } - - if (null === $domain) { - $domain = 'messages'; - } - - $catalogue = $this->getCatalogue($locale); - $locale = $catalogue->getLocale(); - while (!$catalogue->defines($id, $domain)) { - if ($cat = $catalogue->getFallbackCatalogue()) { - $catalogue = $cat; - $locale = $catalogue->getLocale(); - } else { - break; - } - } - - if ($this->hasIntlFormatter && $catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { - return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, ['%count%' => $number] + $parameters); - } - - return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters); - } - - /** - * {@inheritdoc} - */ - public function getCatalogue($locale = null) + public function getCatalogue(string $locale = null) { if (!$locale) { $locale = $this->getLocale(); @@ -295,6 +242,14 @@ public function getCatalogue($locale = null) return $this->catalogues[$locale]; } + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + /** * Gets the loaders. * @@ -305,10 +260,7 @@ protected function getLoaders() return $this->loaders; } - /** - * @param string $locale - */ - protected function loadCatalogue($locale) + protected function loadCatalogue(string $locale) { if (null === $this->cacheDir) { $this->initializeCatalogue($locale); @@ -317,10 +269,7 @@ protected function loadCatalogue($locale) } } - /** - * @param string $locale - */ - protected function initializeCatalogue($locale) + protected function initializeCatalogue(string $locale) { $this->assertValidLocale($locale); @@ -456,7 +405,7 @@ private function loadFallbackCatalogues(string $locale): void } } - protected function computeFallbackLocales($locale) + protected function computeFallbackLocales(string $locale) { if (null === $this->parentLocales) { $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); @@ -502,13 +451,11 @@ protected function computeFallbackLocales($locale) /** * Asserts that the locale is valid, throws an Exception if not. * - * @param string $locale Locale to tests - * * @throws InvalidArgumentException If the locale contains invalid characters */ - protected function assertValidLocale($locale) + protected function assertValidLocale(string $locale) { - if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', (string) $locale)) { + if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); } } diff --git a/symfony/translation/TranslatorBag.php b/symfony/translation/TranslatorBag.php new file mode 100644 index 000000000..c6555782f --- /dev/null +++ b/symfony/translation/TranslatorBag.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Catalogue\AbstractOperation; +use Symfony\Component\Translation\Catalogue\TargetOperation; + +final class TranslatorBag implements TranslatorBagInterface +{ + /** @var MessageCatalogue[] */ + private $catalogues = []; + + public function addCatalogue(MessageCatalogue $catalogue): void + { + if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) { + $catalogue->addCatalogue($existingCatalogue); + } + + $this->catalogues[$catalogue->getLocale()] = $catalogue; + } + + public function addBag(TranslatorBagInterface $bag): void + { + foreach ($bag->getCatalogues() as $catalogue) { + $this->addCatalogue($catalogue); + } + } + + /** + * {@inheritdoc} + */ + public function getCatalogue(string $locale = null) + { + if (null === $locale || !isset($this->catalogues[$locale])) { + $this->catalogues[$locale] = new MessageCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + + public function diff(TranslatorBagInterface $diffBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) { + $diff->addCatalogue($catalogue); + + continue; + } + + $operation = new TargetOperation($diffCatalogue, $catalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH); + $newCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $newCatalogue->add($operation->getNewMessages($domain), $domain); + } + + $diff->addCatalogue($newCatalogue); + } + + return $diff; + } + + public function intersect(TranslatorBagInterface $intersectBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) { + continue; + } + + $operation = new TargetOperation($catalogue, $intersectCatalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH); + $obsoleteCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $obsoleteCatalogue->add($operation->getObsoleteMessages($domain), $domain); + } + + $diff->addCatalogue($obsoleteCatalogue); + } + + return $diff; + } +} diff --git a/symfony/translation/TranslatorBagInterface.php b/symfony/translation/TranslatorBagInterface.php index 5e49e2ddc..422897735 100644 --- a/symfony/translation/TranslatorBagInterface.php +++ b/symfony/translation/TranslatorBagInterface.php @@ -16,6 +16,8 @@ /** * TranslatorBagInterface. * + * @method MessageCatalogueInterface[] getCatalogues() Returns all catalogues of the instance + * * @author Abdellatif Ait boudad */ interface TranslatorBagInterface @@ -29,5 +31,5 @@ interface TranslatorBagInterface * * @throws InvalidArgumentException If the locale contains invalid characters */ - public function getCatalogue($locale = null); + public function getCatalogue(string $locale = null); } diff --git a/symfony/translation/TranslatorInterface.php b/symfony/translation/TranslatorInterface.php deleted file mode 100644 index f677d2455..000000000 --- a/symfony/translation/TranslatorInterface.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Contracts\Translation\LocaleAwareInterface; - -/** - * TranslatorInterface. - * - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use Symfony\Contracts\Translation\TranslatorInterface instead - */ -interface TranslatorInterface extends LocaleAwareInterface -{ - /** - * Translates the given message. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function trans($id, array $parameters = [], $domain = null, $locale = null); - - /** - * Translates the given choice message by choosing a translation according to a number. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param int $number The number to use to find the index of the message - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null); - - /** - * Sets the current locale. - * - * @param string $locale The locale - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function setLocale($locale); - - /** - * Returns the current locale. - * - * @return string The locale - */ - public function getLocale(); -} diff --git a/symfony/translation/Util/ArrayConverter.php b/symfony/translation/Util/ArrayConverter.php index 22c602e71..acfbfc363 100644 --- a/symfony/translation/Util/ArrayConverter.php +++ b/symfony/translation/Util/ArrayConverter.php @@ -84,7 +84,7 @@ private static function &getElementByPath(array &$tree, array $parts) return $elem; } - private static function cancelExpand(array &$tree, $prefix, array $node) + private static function cancelExpand(array &$tree, string $prefix, array $node) { $prefix .= '.'; diff --git a/symfony/translation/Writer/TranslationWriter.php b/symfony/translation/Writer/TranslationWriter.php index 83f6fd615..0a349b824 100644 --- a/symfony/translation/Writer/TranslationWriter.php +++ b/symfony/translation/Writer/TranslationWriter.php @@ -27,30 +27,12 @@ class TranslationWriter implements TranslationWriterInterface /** * Adds a dumper to the writer. - * - * @param string $format The format of the dumper */ - public function addDumper($format, DumperInterface $dumper) + public function addDumper(string $format, DumperInterface $dumper) { $this->dumpers[$format] = $dumper; } - /** - * Disables dumper backup. - * - * @deprecated since Symfony 4.1 - */ - public function disableBackup() - { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), \E_USER_DEPRECATED); - - foreach ($this->dumpers as $dumper) { - if (method_exists($dumper, 'setBackup')) { - $dumper->setBackup(false); - } - } - } - /** * Obtains the list of supported formats. * @@ -69,7 +51,7 @@ public function getFormats() * * @throws InvalidArgumentException */ - public function write(MessageCatalogue $catalogue, $format, $options = []) + public function write(MessageCatalogue $catalogue, string $format, array $options = []) { if (!isset($this->dumpers[$format])) { throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format)); diff --git a/symfony/translation/Writer/TranslationWriterInterface.php b/symfony/translation/Writer/TranslationWriterInterface.php index f7c56bed0..43213097e 100644 --- a/symfony/translation/Writer/TranslationWriterInterface.php +++ b/symfony/translation/Writer/TranslationWriterInterface.php @@ -29,5 +29,5 @@ interface TranslationWriterInterface * * @throws InvalidArgumentException */ - public function write(MessageCatalogue $catalogue, $format, $options = []); + public function write(MessageCatalogue $catalogue, string $format, array $options = []); }