Repository: symfony/translation Branch: 8.1 Commit: ba0bc436a49e Files: 259 Total size: 759.8 KB Directory structure: gitextract_etp_7s_u/ ├── .gitattributes ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── close-pull-request.yml ├── .gitignore ├── CHANGELOG.md ├── Catalogue/ │ ├── AbstractOperation.php │ ├── MergeOperation.php │ ├── OperationInterface.php │ └── TargetOperation.php ├── CatalogueMetadataAwareInterface.php ├── Command/ │ ├── TranslationLintCommand.php │ ├── TranslationPullCommand.php │ ├── TranslationPushCommand.php │ ├── TranslationTrait.php │ └── XliffLintCommand.php ├── DataCollector/ │ └── TranslationDataCollector.php ├── DataCollectorTranslator.php ├── DependencyInjection/ │ ├── DataCollectorTranslatorPass.php │ ├── LoggingTranslatorPass.php │ ├── TranslationDumperPass.php │ ├── TranslationExtractorPass.php │ ├── TranslatorPass.php │ └── TranslatorPathsPass.php ├── Dumper/ │ ├── CsvFileDumper.php │ ├── DumperInterface.php │ ├── FileDumper.php │ ├── IcuResFileDumper.php │ ├── IniFileDumper.php │ ├── JsonFileDumper.php │ ├── MoFileDumper.php │ ├── PhpFileDumper.php │ ├── PoFileDumper.php │ ├── QtFileDumper.php │ ├── XliffFileDumper.php │ └── YamlFileDumper.php ├── Exception/ │ ├── ExceptionInterface.php │ ├── IncompleteDsnException.php │ ├── InvalidArgumentException.php │ ├── InvalidResourceException.php │ ├── LogicException.php │ ├── MissingRequiredOptionException.php │ ├── NotFoundResourceException.php │ ├── ProviderException.php │ ├── ProviderExceptionInterface.php │ ├── RuntimeException.php │ └── UnsupportedSchemeException.php ├── Extractor/ │ ├── AbstractFileExtractor.php │ ├── ChainExtractor.php │ ├── ExtractorInterface.php │ ├── PhpAstExtractor.php │ └── Visitor/ │ ├── AbstractVisitor.php │ ├── ConstraintVisitor.php │ ├── TransMethodVisitor.php │ └── TranslatableMessageVisitor.php ├── Formatter/ │ ├── IntlFormatter.php │ ├── IntlFormatterInterface.php │ ├── MessageFormatter.php │ └── MessageFormatterInterface.php ├── IdentityTranslator.php ├── LICENSE ├── Loader/ │ ├── ArrayLoader.php │ ├── CsvFileLoader.php │ ├── FileLoader.php │ ├── IcuDatFileLoader.php │ ├── IcuResFileLoader.php │ ├── IniFileLoader.php │ ├── JsonFileLoader.php │ ├── LoaderInterface.php │ ├── MoFileLoader.php │ ├── PhpFileLoader.php │ ├── PoFileLoader.php │ ├── QtFileLoader.php │ ├── XliffFileLoader.php │ └── YamlFileLoader.php ├── LocaleFallbackProvider.php ├── LocaleSwitcher.php ├── LoggingTranslator.php ├── MessageCatalogue.php ├── MessageCatalogueInterface.php ├── MetadataAwareInterface.php ├── Provider/ │ ├── AbstractProviderFactory.php │ ├── Dsn.php │ ├── FilteringProvider.php │ ├── NullProvider.php │ ├── NullProviderFactory.php │ ├── ProviderFactoryInterface.php │ ├── ProviderInterface.php │ ├── TranslationProviderCollection.php │ └── TranslationProviderCollectionFactory.php ├── PseudoLocalizationTranslator.php ├── README.md ├── Reader/ │ ├── TranslationReader.php │ └── TranslationReaderInterface.php ├── Resources/ │ ├── bin/ │ │ └── translation-status.php │ ├── data/ │ │ ├── parents.json │ │ └── parents.php │ ├── functions.php │ └── schemas/ │ ├── xliff-core-1.2-transitional.xsd │ ├── xliff-core-2.0.xsd │ ├── xliff-core-2.2.xsd │ └── xml.xsd ├── StaticMessage.php ├── Test/ │ ├── AbstractProviderFactoryTestCase.php │ ├── IncompleteDsnTestTrait.php │ └── ProviderTestCase.php ├── Tests/ │ ├── Catalogue/ │ │ ├── AbstractOperationTestCase.php │ │ ├── MergeOperationTest.php │ │ ├── MessageCatalogueTest.php │ │ └── TargetOperationTest.php │ ├── Command/ │ │ ├── TranslationLintCommandTest.php │ │ ├── TranslationProviderTestCase.php │ │ ├── TranslationPullCommandTest.php │ │ ├── TranslationPushCommandTest.php │ │ └── XliffLintCommandTest.php │ ├── DataCollector/ │ │ └── TranslationDataCollectorTest.php │ ├── DataCollectorTranslatorTest.php │ ├── DependencyInjection/ │ │ ├── DataCollectorTranslatorPassTest.php │ │ ├── Fixtures/ │ │ │ ├── ControllerArguments.php │ │ │ ├── ServiceArguments.php │ │ │ ├── ServiceMethodCalls.php │ │ │ ├── ServiceProperties.php │ │ │ └── ServiceSubscriber.php │ │ ├── LoggingTranslatorPassTest.php │ │ ├── TranslationDumperPassTest.php │ │ ├── TranslationExtractorPassTest.php │ │ ├── TranslationPathsPassTest.php │ │ └── TranslatorPassTest.php │ ├── Dumper/ │ │ ├── CsvFileDumperTest.php │ │ ├── FileDumperTest.php │ │ ├── IcuResFileDumperTest.php │ │ ├── IniFileDumperTest.php │ │ ├── JsonFileDumperTest.php │ │ ├── MoFileDumperTest.php │ │ ├── PhpFileDumperTest.php │ │ ├── PoFileDumperTest.php │ │ ├── QtFileDumperTest.php │ │ ├── XliffFileDumperTest.php │ │ └── YamlFileDumperTest.php │ ├── Exception/ │ │ ├── ProviderExceptionTest.php │ │ └── UnsupportedSchemeExceptionTest.php │ ├── Extractor/ │ │ └── PhpAstExtractorTest.php │ ├── Fixtures/ │ │ ├── empty-translation.mo │ │ ├── empty-translation.po │ │ ├── empty.csv │ │ ├── empty.ini │ │ ├── empty.json │ │ ├── empty.mo │ │ ├── empty.po │ │ ├── empty.xlf │ │ ├── empty.yml │ │ ├── encoding.xlf │ │ ├── escaped-id-plurals.po │ │ ├── escaped-id.po │ │ ├── extractor/ │ │ │ ├── resource.format.engine │ │ │ ├── this.is.a.template.format.engine │ │ │ ├── translatable-fqn.html.php │ │ │ ├── translatable-short.html.php │ │ │ ├── translatable.html.php │ │ │ └── translation.html.php │ │ ├── extractor-7.3/ │ │ │ └── translation.html.php │ │ ├── extractor-ast/ │ │ │ ├── resource.format.engine │ │ │ ├── this.is.a.template.format.engine │ │ │ ├── translatable-fqn.html.php │ │ │ ├── translatable-short-fqn.html.php │ │ │ ├── translatable-short.html.php │ │ │ ├── translatable.html.php │ │ │ ├── translation.html.php │ │ │ └── validator-constraints.php │ │ ├── fuzzy-translations.po │ │ ├── invalid-xml-resources.xlf │ │ ├── malformed.json │ │ ├── messages.yml │ │ ├── messages_linear.yml │ │ ├── missing-plurals.po │ │ ├── non-string.yml │ │ ├── non-valid.xlf │ │ ├── non-valid.yml │ │ ├── plurals.mo │ │ ├── plurals.po │ │ ├── resname.xlf │ │ ├── resourcebundle/ │ │ │ ├── dat/ │ │ │ │ ├── en.res │ │ │ │ ├── en.txt │ │ │ │ ├── fr.res │ │ │ │ ├── fr.txt │ │ │ │ └── packagelist.txt │ │ │ └── res/ │ │ │ └── en.res │ │ ├── resources-2.0+intl-icu.xlf │ │ ├── resources-2.0-clean.xlf │ │ ├── resources-2.0-empty-notes.xlf │ │ ├── resources-2.0-multi-segment-unit.xlf │ │ ├── resources-2.0-name.xlf │ │ ├── resources-2.0-segment-attributes.xlf │ │ ├── resources-2.0.xlf │ │ ├── resources-2.1.xlf │ │ ├── resources-2.2-pgs-combined.xlf │ │ ├── resources-2.2-pgs-gender.xlf │ │ ├── resources-2.2-pgs-plural.xlf │ │ ├── resources-2.2.xlf │ │ ├── resources-clean.xlf │ │ ├── resources-clean.xliff │ │ ├── resources-multi-files.xlf │ │ ├── resources-notes-meta.xlf │ │ ├── resources-target-attributes.xlf │ │ ├── resources-tool-info.xlf │ │ ├── resources.csv │ │ ├── resources.dump.json │ │ ├── resources.ini │ │ ├── resources.json │ │ ├── resources.mo │ │ ├── resources.php │ │ ├── resources.po │ │ ├── resources.ts │ │ ├── resources.xlf │ │ ├── resources.yml │ │ ├── valid.csv │ │ ├── with-attributes.xlf │ │ ├── withdoctype.xlf │ │ └── withnote.xlf │ ├── Formatter/ │ │ ├── IntlFormatterTest.php │ │ └── MessageFormatterTest.php │ ├── IdentityTranslatorTest.php │ ├── Loader/ │ │ ├── CsvFileLoaderTest.php │ │ ├── IcuDatFileLoaderTest.php │ │ ├── IcuResFileLoaderTest.php │ │ ├── IniFileLoaderTest.php │ │ ├── JsonFileLoaderTest.php │ │ ├── LocalizedTestCase.php │ │ ├── MoFileLoaderTest.php │ │ ├── PhpFileLoaderTest.php │ │ ├── PoFileLoaderTest.php │ │ ├── QtFileLoaderTest.php │ │ ├── XliffFileLoaderTest.php │ │ └── YamlFileLoaderTest.php │ ├── LocaleFallbackProviderTest.php │ ├── LocaleSwitcherTest.php │ ├── LoggingTranslatorTest.php │ ├── MessageCatalogueTest.php │ ├── Provider/ │ │ ├── DsnTest.php │ │ ├── FilteringProviderTest.php │ │ ├── NullProviderFactoryTest.php │ │ └── TranslationProviderCollectionTest.php │ ├── PseudoLocalizationTranslatorTest.php │ ├── StaticMessageTest.php │ ├── TranslatableTest.php │ ├── TranslatorBagTest.php │ ├── TranslatorCacheTest.php │ ├── TranslatorTest.php │ ├── Util/ │ │ └── ArrayConverterTest.php │ └── Writer/ │ └── TranslationWriterTest.php ├── TranslatableMessage.php ├── Translator.php ├── TranslatorBag.php ├── TranslatorBagInterface.php ├── Util/ │ ├── ArrayConverter.php │ └── XliffUtils.php ├── Writer/ │ ├── TranslationWriter.php │ └── TranslationWriterInterface.php ├── composer.json └── phpunit.xml.dist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /Tests export-ignore /phpunit.xml.dist export-ignore /.git* export-ignore ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Please do not submit any Pull Requests here. They will be closed. --- Please submit your PR here instead: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .github/workflows/close-pull-request.yml ================================================ name: Close Pull Request on: pull_request_target: types: [opened] jobs: run: runs-on: ubuntu-latest steps: - uses: superbrothers/close-pull-request@v3 with: comment: | Thanks for your Pull Request! We love contributions. However, you should instead open your PR on the main repository: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .gitignore ================================================ vendor/ composer.lock phpunit.xml ================================================ FILE: CHANGELOG.md ================================================ CHANGELOG ========= 8.1 --- * Add support for XLIFF 2.1 and 2.2 * Add support for XLIFF 2.2 PGS (Plural, Gender, and Select Module) * Add `LocaleFallbackProvider` 8.0 --- * Remove the `$escape` parameter from `CsvFileLoader::setCsvControl()` * Make `DataCollectorTranslator` class `final` * Remove `ProviderFactoryTestCase`, extend `AbstractProviderFactoryTestCase` instead * Remove `TranslatableMessage::__toString()` method, use `trans()` or `getMessage()` instead 7.4 --- * Make the extractor alias optional * Deprecate `TranslatableMessage::__toString` * Add `Symfony\Component\Translation\StaticMessage` 7.3 --- * Add `Translator::addGlobalParameter()` to allow defining global translation parameters 7.2 --- * Deprecate `ProviderFactoryTestCase`, extend `AbstractProviderFactoryTestCase` instead The `testIncompleteDsnException()` test is no longer provided by default. If you make use of it by implementing the `incompleteDsnProvider()` data providers, you now need to use the `IncompleteDsnTestTrait`. * Make `ProviderFactoryTestCase` and `ProviderTestCase` compatible with PHPUnit 10+ * Add `lint:translations` command * Deprecate passing an escape character to `CsvFileLoader::setCsvControl()` * Make Xliff 2.0 attributes in segment element available as `segment-attributes` metadata returned by `XliffFileLoader` and make `XliffFileDumper` write them to the file 7.1 --- * Mark class `DataCollectorTranslator` as `final` 7.0 --- * Remove `PhpStringTokenParser` * Remove `PhpExtractor` in favor of `PhpAstExtractor` 6.4 --- * Give current locale to `LocaleSwitcher::runWithLocale()`'s callback * Add `--as-tree` option to `translation:pull` command to write YAML messages as a tree-like structure * [BC BREAK] Add argument `$buildDir` to `DataCollectorTranslator::warmUp()` * Add `DataCollectorTranslatorPass` and `LoggingTranslatorPass` (moved from `FrameworkBundle`) * Add `PhraseTranslationProvider` 6.2.7 ----- * [BC BREAK] The following data providers for `ProviderFactoryTestCase` are now static: `supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()` * [BC BREAK] `ProviderTestCase::toStringProvider()` is now static 6.2 --- * Deprecate `PhpStringTokenParser` * Deprecate `PhpExtractor` in favor of `PhpAstExtractor` * Add `PhpAstExtractor` (requires [nikic/php-parser](https://github.com/nikic/php-parser) to be installed) 6.1 --- * Parameters implementing `TranslatableInterface` are processed * Add the file extension to the `XliffFileDumper` constructor 5.4 --- * Add `github` format & autodetection to render errors as annotations when running the XLIFF linter command in a Github Actions environment. * Translation providers are not experimental anymore 5.3 --- * Add `translation:pull` and `translation:push` commands to manage translations with third-party providers * Add `TranslatorBagInterface::getCatalogues` method * Add support to load XLIFF string in `XliffFileLoader` 5.2.0 ----- * added support for calling `trans` with ICU formatted messages * added `PseudoLocalizationTranslator` * added `TranslatableMessage` objects that represent a message that can be translated * added the `t()` function to easily create `TranslatableMessage` objects * Added support for extracting messages from `TranslatableMessage` objects 5.1.0 ----- * added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element 5.0.0 ----- * removed support for using `null` as the locale in `Translator` * removed `TranslatorInterface` * removed `MessageSelector` * removed `ChoiceMessageFormatterInterface` * removed `PluralizationRule` * removed `Interval` * removed `transChoice()` methods, use the trans() method instead with a %count% parameter * removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()` * removed `MessageFormatter::choiceFormat()` * added argument `$filename` to `PhpExtractor::parseTokens()` * removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. 4.4.0 ----- * deprecated support for using `null` as the locale in `Translator` * deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. * Marked the `TranslationDataCollector` class as `@final`. 4.3.0 ----- * Improved Xliff 1.2 loader to load the original file's metadata * Added `TranslatorPathsPass` 4.2.0 ----- * Started using ICU parent locales as fallback locales. * allow using the ICU message format using domains with the "+intl-icu" suffix * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead * Added `IntlFormatter` and `IntlFormatterInterface` * added support for multiple files and directories in `XliffLintCommand` * Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal 4.1.0 ----- * The `FileDumper::setBackup()` method is deprecated. * The `TranslationWriter::disableBackup()` method is deprecated. * The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0. 4.0.0 ----- * removed the backup feature of the `FileDumper` class * removed `TranslationWriter::writeTranslations()` method * removed support for passing `MessageSelector` instances to the constructor of the `Translator` class 3.4.0 ----- * Added `TranslationDumperPass` * Added `TranslationExtractorPass` * Added `TranslatorPass` * Added `TranslationReader` and `TranslationReaderInterface` * Added `` section to the Xliff 2.0 dumper. * Improved Xliff 2.0 loader to load `` section. * Added `TranslationWriterInterface` * Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write` * added support for adding custom message formatter and decoupling the default one. * Added `PhpExtractor` * Added `PhpStringTokenParser` 3.2.0 ----- * Added support for escaping `|` in plural translations with double pipe. 3.1.0 ----- * Deprecated the backup feature of the file dumper classes. 3.0.0 ----- * removed `FileDumper::format()` method. * Changed the visibility of the locale property in `Translator` from protected to private. 2.8.0 ----- * deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead. * deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead. * added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file. * added option `json_encoding` to JsonFileDumper * added options `as_tree`, `inline` to YamlFileDumper * added support for XLIFF 2.0. * added support for XLIFF target and tool attributes. * added message parameters to DataCollectorTranslator. * [DEPRECATION] The `DiffOperation` class has been deprecated and will be removed in Symfony 3.0, since its operation has nothing to do with 'diff', so the class name is misleading. The `TargetOperation` class should be used for this use-case instead. 2.7.0 ----- * added DataCollectorTranslator for collecting the translated messages. 2.6.0 ----- * added possibility to cache catalogues * added TranslatorBagInterface * added LoggingTranslator * added Translator::getMessages() for retrieving the message catalogue as an array 2.5.0 ----- * added relative file path template to the file dumpers * added optional backup to the file dumpers * changed IcuResFileDumper to extend FileDumper 2.3.0 ----- * added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues) * added Translator::getFallbackLocales() * deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method 2.2.0 ----- * QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3. * [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid. * changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException (IcuDatFileLoader, IcuResFileLoader and QtFileLoader) 2.1.0 ----- * added support for more than one fallback locale * added support for extracting translation messages from templates (Twig and PHP) * added dumpers for translation catalogs * added support for QT, gettext, and ResourceBundles ================================================ FILE: Catalogue/AbstractOperation.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Base catalogues binary operation class. * * A catalogue binary operation performs operation on * source (the left argument) and target (the right argument) catalogues. * * @author Jean-François Simon */ abstract class AbstractOperation implements OperationInterface { public const OBSOLETE_BATCH = 'obsolete'; public const NEW_BATCH = 'new'; public const ALL_BATCH = 'all'; protected MessageCatalogue $result; /** * This array stores 'all', 'new' and 'obsolete' messages for all valid domains. * * The data structure of this array is as follows: * * [ * 'domain 1' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * 'domain 2' => [ * 'all' => [...], * 'new' => [...], * 'obsolete' => [...] * ], * ... * ] * * @var array The array that stores 'all', 'new' and 'obsolete' messages */ protected array $messages; private array $domains; /** * @throws LogicException */ public function __construct( protected MessageCatalogueInterface $source, protected MessageCatalogueInterface $target, ) { if ($source->getLocale() !== $target->getLocale()) { throw new LogicException('Operated catalogues must belong to the same locale.'); } $this->result = new MessageCatalogue($source->getLocale()); $this->messages = []; } public function getDomains(): array { if (!isset($this->domains)) { $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; } public function getMessages(string $domain): array { if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::ALL_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::ALL_BATCH]; } public function getNewMessages(string $domain): array { if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::NEW_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::NEW_BATCH]; } public function getObsoleteMessages(string $domain): array { if (!\in_array($domain, $this->getDomains(), true)) { throw new InvalidArgumentException(\sprintf('Invalid domain: "%s".', $domain)); } if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) { $this->processDomain($domain); } return $this->messages[$domain][self::OBSOLETE_BATCH]; } public function getResult(): MessageCatalogueInterface { foreach ($this->getDomains() as $domain) { if (!isset($this->messages[$domain])) { $this->processDomain($domain); } } 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; $messages = match ($batch) { self::OBSOLETE_BATCH => $this->getObsoleteMessages($domain), self::NEW_BATCH => $this->getNewMessages($domain), self::ALL_BATCH => $this->getMessages($domain), 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(string $domain): void; } ================================================ FILE: Catalogue/MergeOperation.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Merge operation between two catalogues as follows: * all = source ∪ target = {x: x ∈ source ∨ x ∈ target} * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅ * Basically, the result contains messages from both catalogues. * * @author Jean-François Simon */ class MergeOperation extends AbstractOperation { protected function processDomain(string $domain): void { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) { if (null === $this->result->getCatalogueMetadata($key, $domain)) { $this->result->setCatalogueMetadata($key, $value, $domain); } } foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) { if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) { $this->result->setCatalogueMetadata($key, $value, $intlDomain); } } foreach ($this->source->all($domain) as $id => $message) { $this->messages[$domain]['all'][$id] = $message; $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } ================================================ FILE: Catalogue/OperationInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Represents an operation on catalogue(s). * * An instance of this interface performs an operation on one or more catalogues and * stores intermediate and final results of the operation. * * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and * the following results are stored: * * Messages: also called 'all', are valid messages for the given domain after the operation is performed. * * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}). * * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}). * * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'. * * @author Jean-François Simon */ interface OperationInterface { /** * Returns domains affected by operation. */ public function getDomains(): array; /** * Returns all valid messages ('all') after operation. */ public function getMessages(string $domain): array; /** * Returns new messages ('new') after operation. */ public function getNewMessages(string $domain): array; /** * Returns obsolete messages ('obsolete') after operation. */ public function getObsoleteMessages(string $domain): array; /** * Returns resulting catalogue ('result'). */ public function getResult(): MessageCatalogueInterface; } ================================================ FILE: Catalogue/TargetOperation.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Catalogue; use Symfony\Component\Translation\MessageCatalogueInterface; /** * Target operation between two catalogues: * intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target} * all = intersection ∪ (target ∖ intersection) = target * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} * obsolete = source ∖ all = source ∖ target = {x: x ∈ source ∧ x ∉ target} * Basically, the result contains messages from the target catalogue. * * @author Michael Lee */ class TargetOperation extends AbstractOperation { protected function processDomain(string $domain): void { $this->messages[$domain] = [ 'all' => [], 'new' => [], 'obsolete' => [], ]; $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) { if (null === $this->result->getCatalogueMetadata($key, $domain)) { $this->result->setCatalogueMetadata($key, $value, $domain); } } foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) { if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) { $this->result->setCatalogueMetadata($key, $value, $intlDomain); } } // For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``, // because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} // // For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));`` // because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback} // // For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))`` // because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} foreach ($this->source->all($domain) as $id => $message) { if ($this->target->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } else { $this->messages[$domain]['obsolete'][$id] = $message; } } foreach ($this->target->all($domain) as $id => $message) { if (!$this->source->has($id, $domain)) { $this->messages[$domain]['all'][$id] = $message; $this->messages[$domain]['new'][$id] = $message; $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; $this->result->add([$id => $message], $d); if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { $this->result->setMetadata($id, $keyMetadata, $d); } } } } } ================================================ FILE: CatalogueMetadataAwareInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; /** * This interface is used to get, set, and delete metadata about the Catalogue. * * @author Hugo Alliaume */ interface CatalogueMetadataAwareInterface { /** * Gets catalogue metadata for the given domain and key. * * Passing an empty domain will return an array with all catalogue metadata indexed by * domain and then by key. Passing an empty key will return an array with all * catalogue metadata for the given domain. * * @return mixed The value that was set or an array with the domains/keys or null */ public function getCatalogueMetadata(string $key = '', string $domain = 'messages'): mixed; /** * Adds catalogue metadata to a message domain. */ public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'): void; /** * Deletes catalogue metadata for the given key and domain. * * Passing an empty domain will delete all catalogue metadata. Passing an empty key will * delete all metadata for the given domain. */ public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'): void; } ================================================ FILE: Command/TranslationLintCommand.php ================================================ * * 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\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; 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\Exception\ExceptionInterface; use Symfony\Component\Translation\TranslatorBagInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * Lint translations files syntax and outputs encountered errors. * * @author Hugo Alliaume */ #[AsCommand(name: 'lint:translations', description: 'Lint translations files syntax and outputs encountered errors')] class TranslationLintCommand extends Command { private SymfonyStyle $io; public function __construct( private TranslatorInterface&TranslatorBagInterface $translator, private array $enabledLocales = [], ) { $this->enabledLocales = array_filter($enabledLocales); parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('locale')) { $suggestions->suggestValues($this->enabledLocales); } } protected function configure(): void { $this ->setDefinition([ new InputOption('locale', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.', $this->enabledLocales), ]) ->setHelp(<<<'EOF' The %command.name% command lint translations. php %command.full_name% EOF ); } protected function initialize(InputInterface $input, OutputInterface $output): void { $this->io = new SymfonyStyle($input, $output); } protected function execute(InputInterface $input, OutputInterface $output): int { $locales = $input->getOption('locale'); /** @var array>> $errors */ $errors = []; $domainsByLocales = []; foreach ($locales as $locale) { $messageCatalogue = $this->translator->getCatalogue($locale); foreach ($domainsByLocales[$locale] = $messageCatalogue->getDomains() as $domain) { foreach ($messageCatalogue->all($domain) as $id => $translation) { try { $this->translator->trans($id, [], $domain, $messageCatalogue->getLocale()); } catch (ExceptionInterface $e) { $errors[$locale][$domain][$id] = $e; } } } } if (!$domainsByLocales) { $this->io->error('No translation files were found.'); return Command::SUCCESS; } $this->io->table( ['Locale', 'Domains', 'Valid?'], array_map( static fn (string $locale, array $domains) => [ $locale, implode(', ', $domains), !\array_key_exists($locale, $errors) ? 'Yes' : 'No', ], array_keys($domainsByLocales), $domainsByLocales ), ); if ($errors) { foreach ($errors as $locale => $domains) { foreach ($domains as $domain => $domainsErrors) { $this->io->section(\sprintf('Errors for locale "%s" and domain "%s"', $locale, $domain)); foreach ($domainsErrors as $id => $error) { $this->io->text(\sprintf('Translation key "%s" is invalid:', $id)); $this->io->error($error->getMessage()); } } } return Command::FAILURE; } $this->io->success('All translations are valid.'); return Command::SUCCESS; } } ================================================ FILE: Command/TranslationPullCommand.php ================================================ * * 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\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; 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 */ #[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')] final class TranslationPullCommand extends Command { use TranslationTrait; public function __construct( private TranslationProviderCollection $providerCollection, private TranslationWriterInterface $writer, private TranslationReaderInterface $reader, private string $defaultLocale, private array $transPaths = [], private array $enabledLocales = [], ) { $this->enabledLocales = array_filter($enabledLocales); parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providerCollection->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providerCollection->get($input->getArgument('provider')); if (method_exists($provider, 'getDomains')) { $suggestions->suggestValues($provider->getDomains()); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); return; } if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); } } protected function configure(): void { $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_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'Override the default output format.', 'xlf12'), new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'), ]) ->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 ) ; } 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'); $asTree = (int) $input->getOption('as-tree'); $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, 'as_tree' => (bool) $asTree, 'inline' => $asTree, ]; 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; } } ================================================ FILE: Command/TranslationPushCommand.php ================================================ * * 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\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; 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\FilteringProvider; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\TranslatorBag; /** * @author Mathieu Santostefano */ #[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')] final class TranslationPushCommand extends Command { use TranslationTrait; public function __construct( private TranslationProviderCollection $providers, private TranslationReaderInterface $reader, private array $transPaths = [], private array $enabledLocales = [], ) { $this->enabledLocales = array_filter($enabledLocales); parent::__construct(); } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestArgumentValuesFor('provider')) { $suggestions->suggestValues($this->providers->keys()); return; } if ($input->mustSuggestOptionValuesFor('domains')) { $provider = $this->providers->get($input->getArgument('provider')); if (method_exists($provider, 'getDomains')) { $domains = $provider->getDomains(); $suggestions->suggestValues($domains); } return; } if ($input->mustSuggestOptionValuesFor('locales')) { $suggestions->suggestValues($this->enabledLocales); } } protected function configure(): void { $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_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), new InputOption('locales', null, InputOption::VALUE_REQUIRED | 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 ) ; } 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.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'); if (!$domains && $provider instanceof FilteringProvider) { $domains = $provider->getDomains(); } // Reading local translations must be done after retrieving the domains from the provider // in order to manage only translations from configured domains $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); } } ================================================ FILE: Command/TranslationTrait.php ================================================ * * 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; } } ================================================ FILE: Command/XliffLintCommand.php ================================================ * * 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\Attribute\AsCommand; use Symfony\Component\Console\CI\GithubActionReporter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Exception\RuntimeException; 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\Exception\InvalidArgumentException; use Symfony\Component\Translation\Util\XliffUtils; /** * Validates XLIFF files syntax and outputs encountered errors. * * @author Grégoire Pineau * @author Robin Chalas * @author Javier Eguiluz */ #[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')] class XliffLintCommand extends Command { private string $format; private bool $displayCorrectFiles; private ?\Closure $directoryIteratorProvider; private ?\Closure $isReadableProvider; public function __construct( ?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null, private bool $requireStrictFileNames = true, ) { parent::__construct($name); $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...); $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...); } protected function configure(): void { $this ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->addOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions()))) ->setHelp(<<%command.name% command lints an XLIFF file and outputs to STDOUT the first encountered syntax error. You can validates XLIFF contents passed from STDIN: cat filename | php %command.full_name% - You can also validate the syntax of a file: php %command.full_name% filename Or of a whole directory: php %command.full_name% dirname The --format option specifies the format of the command output: php %command.full_name% dirname --format=json EOF ) ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $filenames = (array) $input->getArgument('filename'); $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); $this->displayCorrectFiles = $output->isVerbose(); if (['-'] === $filenames) { return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); } if (!$filenames) { throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); } $filesInfo = []; foreach ($filenames as $filename) { if (!$this->isReadable($filename)) { throw new RuntimeException(\sprintf('File or directory "%s" is not readable.', $filename)); } foreach ($this->getFiles($filename) as $file) { $filesInfo[] = $this->validate(file_get_contents($file), $file); } } return $this->display($io, $filesInfo); } private function validate(string $content, ?string $file = null): array { $errors = []; // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input if ('' === trim($content)) { return ['file' => $file, 'valid' => true]; } $internal = libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadXML($content); if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { $normalizedLocalePattern = \sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/')); // strict file names require translation files to be named '____.locale.xlf' // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed // also, the regexp matching must be case-insensitive, as defined for 'target-language' values // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language $expectedFilenamePattern = $this->requireStrictFileNames ? \sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : \sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern); if (0 === preg_match($expectedFilenamePattern, basename($file))) { $errors[] = [ 'line' => -1, 'column' => -1, 'message' => \sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage), ]; } } foreach (XliffUtils::validateSchema($document) as $xmlError) { $errors[] = [ 'line' => $xmlError['line'], 'column' => $xmlError['column'], 'message' => $xmlError['message'], ]; } libxml_clear_errors(); libxml_use_internal_errors($internal); return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors]; } private function display(SymfonyStyle $io, array $files): int { return match ($this->format) { 'txt' => $this->displayTxt($io, $files), 'json' => $this->displayJson($io, $files), 'github' => $this->displayTxt($io, $files, true), default => throw new InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))), }; } private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int { $countFiles = \count($filesInfo); $erroredFiles = 0; $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null; foreach ($filesInfo as $info) { if ($info['valid'] && $this->displayCorrectFiles) { $io->comment('OK'.($info['file'] ? \sprintf(' in %s', $info['file']) : '')); } elseif (!$info['valid']) { ++$erroredFiles; $io->text(' ERROR '.($info['file'] ? \sprintf(' in %s', $info['file']) : '')); $io->listing(array_map(static function ($error) use ($info, $githubReporter) { // general document errors have a '-1' line number $line = -1 === $error['line'] ? null : $error['line']; $githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null); return null === $line ? $error['message'] : \sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']); }, $info['messages'])); } } if (0 === $erroredFiles) { $io->success(\sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); } else { $io->warning(\sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); } return min($erroredFiles, 1); } private function displayJson(SymfonyStyle $io, array $filesInfo): int { $errors = 0; array_walk($filesInfo, static function (&$v) use (&$errors) { $v['file'] = (string) $v['file']; if (!$v['valid']) { ++$errors; } }); $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } /** * @return iterable<\SplFileInfo> */ private function getFiles(string $fileOrDirectory): iterable { if (is_file($fileOrDirectory)) { yield new \SplFileInfo($fileOrDirectory); return; } foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { if (!\in_array($file->getExtension(), ['xlf', 'xliff'], true)) { continue; } yield $file; } } /** * @return iterable<\SplFileInfo> */ private function getDirectoryIterator(string $directory): iterable { $default = static fn ($directory) => new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), \RecursiveIteratorIterator::LEAVES_ONLY ); if (null !== $this->directoryIteratorProvider) { return ($this->directoryIteratorProvider)($directory, $default); } return $default($directory); } private function isReadable(string $fileOrDirectory): bool { $default = static fn ($fileOrDirectory) => is_readable($fileOrDirectory); if (null !== $this->isReadableProvider) { return ($this->isReadableProvider)($fileOrDirectory, $default); } return $default($fileOrDirectory); } private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string { foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) { if ('target-language' === $attribute->nodeName) { return $attribute->nodeValue; } } return null; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { if ($input->mustSuggestOptionValuesFor('format')) { $suggestions->suggestValues($this->getAvailableFormatOptions()); } } /** @return string[] */ private function getAvailableFormatOptions(): array { return ['txt', 'json', 'github']; } } ================================================ FILE: DataCollector/TranslationDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Abdellatif Ait boudad * * @final */ class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface { public function __construct( private DataCollectorTranslator $translator, ) { } public function lateCollect(): void { $messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); $this->data += $this->computeCount($messages); $this->data['messages'] = $messages; $this->data = $this->cloneVar($this->data); } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data['locale'] = $this->translator->getLocale(); $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); $this->data['global_parameters'] = $this->translator->getGlobalParameters(); } public function reset(): void { $this->data = []; } public function getMessages(): array|Data { return $this->data['messages'] ?? []; } public function getCountMissings(): int { return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0; } public function getCountFallbacks(): int { return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0; } public function getCountDefines(): int { return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0; } public function getLocale(): ?string { return !empty($this->data['locale']) ? $this->data['locale'] : null; } /** * @internal */ public function getFallbackLocales(): Data|array { return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : []; } /** * @internal */ public function getGlobalParameters(): Data|array { return $this->data['global_parameters'] ?? []; } public function getName(): string { return 'translation'; } private function sanitizeCollectedMessages(array $messages): array { $result = []; foreach ($messages as $key => $message) { $messageId = $message['locale'].$message['domain'].$message['id']; if (!isset($result[$messageId])) { $message['count'] = 1; $message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : []; $messages[$key]['translation'] = $this->sanitizeString($message['translation']); $result[$messageId] = $message; } else { if (!empty($message['parameters'])) { $result[$messageId]['parameters'][] = $message['parameters']; } ++$result[$messageId]['count']; } unset($messages[$key]); } return $result; } private function computeCount(array $messages): array { $count = [ DataCollectorTranslator::MESSAGE_DEFINED => 0, DataCollectorTranslator::MESSAGE_MISSING => 0, DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, ]; foreach ($messages as $message) { ++$count[$message['state']]; } return $count; } private function sanitizeString(string $string, int $length = 80): string { $string = trim(preg_replace('/\s+/', ' ', $string)); if (false !== $encoding = mb_detect_encoding($string, null, true)) { if (mb_strlen($string, $encoding) > $length) { return mb_substr($string, 0, $length - 3, $encoding).'...'; } } elseif (\strlen($string) > $length) { return substr($string, 0, $length - 3).'...'; } return $string; } } ================================================ FILE: DataCollectorTranslator.php ================================================ * * 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\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Contracts\Service\ResetInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ final class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface, ResetInterface { public const MESSAGE_DEFINED = 0; public const MESSAGE_MISSING = 1; public const MESSAGE_EQUALS_FALLBACK = 2; private array $messages = []; public function __construct( private TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator, ) { } public function reset(): void { $this->messages = []; } public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->collectMessage($locale, $domain, $id, $trans, $parameters); return $trans; } public function setLocale(string $locale): void { $this->translator->setLocale($locale); } public function getLocale(): string { return $this->translator->getLocale(); } public function getCatalogue(?string $locale = null): MessageCatalogueInterface { return $this->translator->getCatalogue($locale); } public function getCatalogues(): array { return $this->translator->getCatalogues(); } public function warmUp(string $cacheDir, ?string $buildDir = null): array { if ($this->translator instanceof WarmableInterface) { return $this->translator->warmUp($cacheDir, $buildDir); } return []; } public function getFallbackLocales(): array { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } public function getGlobalParameters(): array { if ($this->translator instanceof Translator || method_exists($this->translator, 'getGlobalParameters')) { return $this->translator->getGlobalParameters(); } return []; } public function __call(string $method, array $args): mixed { return $this->translator->{$method}(...$args); } public function getCollectedMessages(): array { return $this->messages; } private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []): void { $domain ??= 'messages'; $catalogue = $this->translator->getCatalogue($locale); $locale = $catalogue->getLocale(); $fallbackLocale = null; if ($catalogue->defines($id, $domain)) { $state = self::MESSAGE_DEFINED; } elseif ($catalogue->has($id, $domain)) { $state = self::MESSAGE_EQUALS_FALLBACK; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { if ($fallbackCatalogue->defines($id, $domain)) { $fallbackLocale = $fallbackCatalogue->getLocale(); break; } $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } } else { $state = self::MESSAGE_MISSING; } $this->messages[] = [ 'locale' => $locale, 'fallbackLocale' => $fallbackLocale, 'domain' => $domain, 'id' => $id, 'translation' => $translation, 'parameters' => $parameters, 'state' => $state, 'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null, ]; } } ================================================ FILE: DependencyInjection/DataCollectorTranslatorPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Translation\TranslatorBagInterface; /** * @author Christian Flothmann */ class DataCollectorTranslatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->has('translator')) { return; } $translatorClass = $container->getParameterBag()->resolveValue($container->findDefinition('translator')->getClass()); if (!is_subclass_of($translatorClass, TranslatorBagInterface::class)) { $container->removeDefinition('translator.data_collector'); $container->removeDefinition('data_collector.translation'); } } } ================================================ FILE: DependencyInjection/LoggingTranslatorPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Translation\TranslatorBagInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ class LoggingTranslatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasAlias('logger') || !$container->hasAlias('translator')) { return; } if (!$container->hasParameter('translator.logging') || !$container->getParameter('translator.logging')) { return; } $translatorAlias = $container->getAlias('translator'); $definition = $container->getDefinition((string) $translatorAlias); $class = $container->getParameterBag()->resolveValue($definition->getClass()); if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias)); } if (!$r->isSubclassOf(TranslatorInterface::class) || !$r->isSubclassOf(TranslatorBagInterface::class)) { return; } $container->getDefinition('translator.logging')->setDecoratedService('translator'); $warmer = $container->getDefinition('translation.warmer'); $subscriberAttributes = $warmer->getTag('container.service_subscriber'); $warmer->clearTag('container.service_subscriber'); foreach ($subscriberAttributes as $k => $v) { if ((!isset($v['id']) || 'translator' !== $v['id']) && (!isset($v['key']) || 'translator' !== $v['key'])) { $warmer->addTag('container.service_subscriber', $v); } } $warmer->addTag('container.service_subscriber', ['key' => 'translator', 'id' => 'translator.logging.inner']); } } ================================================ FILE: DependencyInjection/TranslationDumperPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.formatter services to translation writer. */ class TranslationDumperPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('translation.writer')) { return; } $definition = $container->getDefinition('translation.writer'); foreach ($container->findTaggedServiceIds('translation.dumper', true) as $id => $attributes) { $definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]); } } } ================================================ FILE: DependencyInjection/TranslationExtractorPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Adds tagged translation.extractor services to translation extractor. */ class TranslationExtractorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('translation.extractor')) { return; } $definition = $container->getDefinition('translation.extractor'); foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) { $definition->addMethodCall('addExtractor', [$attributes[0]['alias'] ?? $id, new Reference($id)]); } } } ================================================ FILE: DependencyInjection/TranslatorPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; class TranslatorPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('translator.default')) { return; } $loaders = []; $loaderRefs = []; foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) { $loaderRefs[$id] = new Reference($id); $loaders[$id][] = $attributes[0]['alias']; if (isset($attributes[0]['legacy-alias'])) { $loaders[$id][] = $attributes[0]['legacy-alias']; } } if ($container->hasDefinition('translation.reader')) { $definition = $container->getDefinition('translation.reader'); foreach ($loaders as $id => $formats) { foreach ($formats as $format) { $definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]); } } } $container ->findDefinition('translator.default') ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) ->replaceArgument(3, $loaders) ; if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) { $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); $constraintClassNames = []; foreach ($container->getDefinitions() as $definition) { if (!$definition->hasTag('validator.constraint_validator')) { continue; } // Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter $className = $container->getParameterBag()->resolveValue($definition->getClass()); // Extraction of the constraint class name from the Constraint Validator FQCN $constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1)); } $constraintVisitorDefinition->setArgument(0, $constraintClassNames); } if (!$container->hasParameter('twig.default_path')) { return; } $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); if ($container->hasDefinition('console.command.translation_debug')) { $definition = $container->getDefinition('console.command.translation_debug'); $definition->replaceArgument(4, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 6) { $definition->replaceArgument(6, $paths); } } if ($container->hasDefinition('console.command.translation_extract')) { $definition = $container->getDefinition('console.command.translation_extract'); $definition->replaceArgument(5, $container->getParameter('twig.default_path')); if (\count($definition->getArguments()) > 7) { $definition->replaceArgument(7, $paths); } } } } ================================================ FILE: DependencyInjection/TranslatorPathsPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; /** * @author Yonel Ceruto */ class TranslatorPathsPass extends AbstractRecursivePass { protected bool $skipScalars = true; private int $level = 0; /** * @var array */ private array $paths = []; /** * @var array */ private array $definitions = []; /** * @var array> */ private array $controllers = []; public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('translator')) { return; } foreach ($this->findControllerArguments($container) as $controller => $argument) { $id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller)); if ($container->hasDefinition($id)) { [$locatorRef] = $argument->getValues(); $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; } } try { parent::process($container); $paths = []; foreach ($this->paths as $class => $_) { if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { $paths[] = $r->getFileName(); foreach ($r->getTraits() as $trait) { $paths[] = $trait->getFileName(); } } } if ($paths) { if ($container->hasDefinition('console.command.translation_debug')) { $definition = $container->getDefinition('console.command.translation_debug'); $definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths)); } if ($container->hasDefinition('console.command.translation_extract')) { $definition = $container->getDefinition('console.command.translation_extract'); $definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths)); } } } finally { $this->level = 0; $this->paths = []; $this->definitions = []; } } protected function processValue(mixed $value, bool $isRoot = false): mixed { if ($value instanceof Reference) { if ('translator' === (string) $value) { for ($i = $this->level - 1; $i >= 0; --$i) { $class = $this->definitions[$i]->getClass(); if (ServiceLocator::class === $class) { if (!isset($this->controllers[$this->currentId ?? ''])) { continue; } foreach ($this->controllers[$this->currentId ?? ''] as $class => $_) { $this->paths[$class] = true; } } else { $this->paths[$class] = true; } break; } } return $value; } if ($value instanceof Definition) { $this->definitions[$this->level++] = $value; $value = parent::processValue($value, $isRoot); unset($this->definitions[--$this->level]); return $value; } return parent::processValue($value, $isRoot); } private function findControllerArguments(ContainerBuilder $container): array { if (!$container->has('argument_resolver.service')) { return []; } $resolverDef = $container->findDefinition('argument_resolver.service'); if (TraceableValueResolver::class === $resolverDef->getClass()) { $resolverDef = $container->getDefinition($resolverDef->getArgument(0)); } $argument = $resolverDef->getArgument(0); if ($argument instanceof Reference) { $argument = $container->getDefinition($argument); } return $argument->getArgument(0); } } ================================================ FILE: Dumper/CsvFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * CsvFileDumper generates a csv formatted string representation of a message catalogue. * * @author Stealth35 */ class CsvFileDumper extends FileDumper { private string $delimiter = ';'; private string $enclosure = '"'; public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $handle = fopen('php://memory', 'r+'); foreach ($messages->all($domain) as $source => $target) { fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure, '\\'); } rewind($handle); $output = stream_get_contents($handle); fclose($handle); return $output; } /** * Sets the delimiter and escape character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"'): void { $this->delimiter = $delimiter; $this->enclosure = $enclosure; } protected function getExtension(): string { return 'csv'; } } ================================================ FILE: Dumper/DumperInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * DumperInterface is the interface implemented by all translation dumpers. * There is no common option. * * @author Michel Salib */ interface DumperInterface { /** * Dumps the message catalogue. * * @param array $options Options that are used by the dumper */ public function dump(MessageCatalogue $messages, array $options = []): void; } ================================================ FILE: Dumper/FileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s). * * Options: * - path (mandatory): the directory where the files should be saved * * @author Michel Salib */ abstract class FileDumper implements DumperInterface { /** * A template for the relative paths to files. */ protected string $relativePathTemplate = '%domain%.%locale%.%extension%'; /** * Sets the template for the relative paths to files. */ public function setRelativePathTemplate(string $relativePathTemplate): void { $this->relativePathTemplate = $relativePathTemplate; } public function dump(MessageCatalogue $messages, array $options = []): void { if (!\array_key_exists('path', $options)) { throw new InvalidArgumentException('The file dumper needs a path option.'); } // save a file for each domain foreach ($messages->getDomains() as $domain) { $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); if (!file_exists($fullpath)) { $directory = \dirname($fullpath); if (!file_exists($directory) && !@mkdir($directory, 0o777, true)) { throw new RuntimeException(\sprintf('Unable to create directory "%s".', $directory)); } } $intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; $intlMessages = $messages->all($intlDomain); if ($intlMessages) { $intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale()); file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options)); $messages->replace([], $intlDomain); try { if ($messages->all($domain)) { file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } continue; } finally { $messages->replace($intlMessages, $intlDomain); } } file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } } /** * Transforms a domain of a message catalogue to its string representation. */ abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string; /** * Gets the file extension of the dumper. */ abstract protected function getExtension(): string; /** * Gets the relative file path using the template. */ private function getRelativePath(string $domain, string $locale): string { return strtr($this->relativePathTemplate, [ '%domain%' => $domain, '%locale%' => $locale, '%extension%' => $this->getExtension(), ]); } } ================================================ FILE: Dumper/IcuResFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue. * * @author Stealth35 */ class IcuResFileDumper extends FileDumper { protected string $relativePathTemplate = '%domain%/%locale%.%extension%'; public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $data = $indexes = $resources = ''; foreach ($messages->all($domain) as $source => $target) { $indexes .= pack('v', \strlen($data) + 28); $data .= $source."\0"; } $data .= $this->writePadding($data); $keyTop = $this->getPosition($data); foreach ($messages->all($domain) as $source => $target) { $resources .= pack('V', $this->getPosition($data)); $data .= pack('V', \strlen($target)) .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8') .$this->writePadding($data) ; } $resOffset = $this->getPosition($data); $data .= pack('v', \count($messages->all($domain))) .$indexes .$this->writePadding($data) .$resources ; $bundleTop = $this->getPosition($data); $root = pack('V7', $resOffset + (2 << 28), // Resource Offset + Resource Type 6, // Index length $keyTop, // Index keys top $bundleTop, // Index resources top $bundleTop, // Index bundle top \count($messages->all($domain)), // Index max table length 0 // Index attributes ); $header = pack('vC2v4C12@32', 32, // Header size 0xDA, 0x27, // Magic number 1 and 2 20, 0, 0, 2, // Rest of the header, ..., Size of a char 0x52, 0x65, 0x73, 0x42, // Data format identifier 1, 2, 0, 0, // Data version 1, 4, 0, 0 // Unicode version ); return $header.$root.$data; } private function writePadding(string $data): ?string { $padding = \strlen($data) % 4; return $padding ? str_repeat("\xAA", 4 - $padding) : null; } private function getPosition(string $data): float|int { return (\strlen($data) + 28) / 4; } protected function getExtension(): string { return 'res'; } } ================================================ FILE: Dumper/IniFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * IniFileDumper generates an ini formatted string representation of a message catalogue. * * @author Stealth35 */ class IniFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $output = ''; foreach ($messages->all($domain) as $source => $target) { $escapeTarget = str_replace('"', '\"', $target); $output .= $source.'="'.$escapeTarget."\"\n"; } return $output; } protected function getExtension(): string { return 'ini'; } } ================================================ FILE: Dumper/JsonFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * JsonFileDumper generates an json formatted string representation of a message catalogue. * * @author singles */ class JsonFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; return json_encode($messages->all($domain), $flags); } protected function getExtension(): string { return 'json'; } } ================================================ FILE: Dumper/MoFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Loader\MoFileLoader; use Symfony\Component\Translation\MessageCatalogue; /** * MoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class MoFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $sources = $targets = $sourceOffsets = $targetOffsets = ''; $offsets = []; $size = 0; foreach ($messages->all($domain) as $source => $target) { $offsets[] = array_map('strlen', [$sources, $source, $targets, $target]); $sources .= "\0".$source; $targets .= "\0".$target; ++$size; } $header = [ 'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC, 'formatRevision' => 0, 'count' => $size, 'offsetId' => MoFileLoader::MO_HEADER_SIZE, 'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size), 'sizeHashes' => 0, 'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size), ]; $sourcesSize = \strlen($sources); $sourcesStart = $header['offsetHashes'] + 1; foreach ($offsets as $offset) { $sourceOffsets .= $this->writeLong($offset[1]) .$this->writeLong($offset[0] + $sourcesStart); $targetOffsets .= $this->writeLong($offset[3]) .$this->writeLong($offset[2] + $sourcesStart + $sourcesSize); } return implode('', array_map($this->writeLong(...), $header)) .$sourceOffsets .$targetOffsets .$sources .$targets; } protected function getExtension(): string { return 'mo'; } private function writeLong(mixed $str): string { return pack('V*', $str); } } ================================================ FILE: Dumper/PhpFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PhpFileDumper generates PHP files from a message catalogue. * * @author Michel Salib */ class PhpFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { return "all($domain), true).";\n"; } protected function getExtension(): string { return 'php'; } } ================================================ FILE: Dumper/PoFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * PoFileDumper generates a gettext formatted string representation of a message catalogue. * * @author Stealth35 */ class PoFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $output = 'msgid ""'."\n"; $output .= 'msgstr ""'."\n"; $output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n"; $output .= '"Content-Transfer-Encoding: 8bit\n"'."\n"; $output .= '"Language: '.$messages->getLocale().'\n"'."\n"; $output .= "\n"; $newLine = false; foreach ($messages->all($domain) as $source => $target) { if ($newLine) { $output .= "\n"; } else { $newLine = true; } $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['comments'])) { $output .= $this->formatComments($metadata['comments']); } if (isset($metadata['flags'])) { $output .= $this->formatComments(implode(',', (array) $metadata['flags']), ','); } if (isset($metadata['sources'])) { $output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':'); } $sourceRules = $this->getStandardRules($source); $targetRules = $this->getStandardRules($target); if (2 == \count($sourceRules) && [] !== $targetRules) { $output .= \sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0])); $output .= \sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1])); foreach ($targetRules as $i => $targetRule) { $output .= \sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule)); } } else { $output .= \sprintf('msgid "%s"'."\n", $this->escape($source)); $output .= \sprintf('msgstr "%s"'."\n", $this->escape($target)); } } return $output; } private function getStandardRules(string $id): array { // Partly copied from TranslatorTrait::trans. $parts = []; if (preg_match('/^\|++$/', $id)) { $parts = explode('|', $id); } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { $parts = $matches[0]; } $intervalRegexp = <<<'EOF' /^(?P ({\s* (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) \s*}) | (?P[\[\]]) \s* (?P-Inf|\-?\d+(\.\d+)?) \s*,\s* (?P\+?Inf|\-?\d+(\.\d+)?) \s* (?P[\[\]]) )\s*(?P.*?)$/xs EOF; $standardRules = []; foreach ($parts as $part) { $part = trim(str_replace('||', '|', $part)); if (preg_match($intervalRegexp, $part)) { // Explicit rule is not a standard rule. return []; } $standardRules[] = $part; } return $standardRules; } protected function getExtension(): string { return 'po'; } private function escape(string $str): string { return addcslashes($str, "\0..\37\42\134"); } private function formatComments(string|array $comments, string $prefix = ''): ?string { $output = null; foreach ((array) $comments as $comment) { $output .= \sprintf('#%s %s'."\n", $prefix, $comment); } return $output; } } ================================================ FILE: Dumper/QtFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\MessageCatalogue; /** * QtFileDumper generates ts files from a message catalogue. * * @author Benjamin Eberlei */ class QtFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $ts = $dom->appendChild($dom->createElement('TS')); $context = $ts->appendChild($dom->createElement('context')); $context->appendChild($dom->createElement('name', $domain)); foreach ($messages->all($domain) as $source => $target) { $message = $context->appendChild($dom->createElement('message')); $metadata = $messages->getMetadata($source, $domain); if (isset($metadata['sources'])) { foreach ((array) $metadata['sources'] as $location) { $loc = explode(':', $location, 2); $location = $message->appendChild($dom->createElement('location')); $location->setAttribute('filename', $loc[0]); if (isset($loc[1])) { $location->setAttribute('line', $loc[1]); } } } $message->appendChild($dom->createElement('source', $source)); $message->appendChild($dom->createElement('translation', $target)); } return $dom->saveXML(); } protected function getExtension(): string { return 'ts'; } } ================================================ FILE: Dumper/XliffFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * XliffFileDumper generates xliff files from a message catalogue. * * @author Michel Salib */ class XliffFileDumper extends FileDumper { public function __construct( private string $extension = 'xlf', ) { } public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { $xliffVersion = '1.2'; if (\array_key_exists('xliff_version', $options)) { $xliffVersion = $options['xliff_version']; } if (\array_key_exists('default_locale', $options)) { $defaultLocale = $options['default_locale']; } else { $defaultLocale = \Locale::getDefault(); } if ('1.2' === $xliffVersion) { return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); } if ('2.0' === $xliffVersion) { return $this->dumpXliff2($defaultLocale, $messages, $domain); } throw new InvalidArgumentException(\sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); } protected function getExtension(): string { return $this->extension; } private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []): string { $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; if (\array_key_exists('tool_info', $options)) { $toolInfo = array_merge($toolInfo, $options['tool_info']); } $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('version', '1.2'); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); $xliffFile = $xliff->appendChild($dom->createElement('file')); $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); $xliffFile->setAttribute('datatype', 'plaintext'); $xliffFile->setAttribute('original', 'file.ext'); $xliffHead = $xliffFile->appendChild($dom->createElement('header')); $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); foreach ($toolInfo as $id => $value) { $xliffTool->setAttribute($id, $value); } if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { $xliffPropGroup = $xliffHead->appendChild($dom->createElement('prop-group')); foreach ($catalogueMetadata as $key => $value) { $xliffProp = $xliffPropGroup->appendChild($dom->createElement('prop')); $xliffProp->setAttribute('prop-type', $key); $xliffProp->appendChild($dom->createTextNode($value)); } } $xliffBody = $xliffFile->appendChild($dom->createElement('body')); foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('trans-unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); $translation->setAttribute('resname', $source); $s = $translation->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); $metadata = $messages->getMetadata($source, $domain); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $translation->appendChild($targetElement); $t->appendChild($text); if ($this->hasMetadataArrayInfo('notes', $metadata)) { foreach ($metadata['notes'] as $note) { if (!isset($note['content'])) { continue; } $n = $translation->appendChild($dom->createElement('note')); $n->appendChild($dom->createTextNode($note['content'])); if (isset($note['priority'])) { $n->setAttribute('priority', $note['priority']); } if (isset($note['from'])) { $n->setAttribute('from', $note['from']); } } } $xliffBody->appendChild($translation); } return $dom->saveXML(); } private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain): string { $dom = new \DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); $xliff->setAttribute('version', '2.0'); $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); $xliffFile = $xliff->appendChild($dom->createElement('file')); if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) { $xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale()); } else { $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); } if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { $xliff->setAttribute('xmlns:m', 'urn:oasis:names:tc:xliff:metadata:2.0'); $xliffMetadata = $xliffFile->appendChild($dom->createElement('m:metadata')); foreach ($catalogueMetadata as $key => $value) { $xliffMeta = $xliffMetadata->appendChild($dom->createElement('prop')); $xliffMeta->setAttribute('type', $key); $xliffMeta->appendChild($dom->createTextNode($value)); } } foreach ($messages->all($domain) as $source => $target) { $translation = $dom->createElement('unit'); $translation->setAttribute('id', strtr(substr(base64_encode(hash('xxh128', $source, true)), 0, 7), '/+', '._')); if (\strlen($source) <= 80) { $translation->setAttribute('name', $source); } $metadata = $messages->getMetadata($source, $domain); // Add notes section if ($this->hasMetadataArrayInfo('notes', $metadata) && $metadata['notes']) { $notesElement = $dom->createElement('notes'); foreach ($metadata['notes'] as $note) { $n = $dom->createElement('note'); $n->appendChild($dom->createTextNode($note['content'] ?? '')); unset($note['content']); foreach ($note as $name => $value) { $n->setAttribute($name, $value); } $notesElement->appendChild($n); } $translation->appendChild($notesElement); } $segment = $translation->appendChild($dom->createElement('segment')); if ($this->hasMetadataArrayInfo('segment-attributes', $metadata)) { foreach ($metadata['segment-attributes'] as $name => $value) { $segment->setAttribute($name, $value); } } $s = $segment->appendChild($dom->createElement('source')); $s->appendChild($dom->createTextNode($source)); // Does the target contain characters requiring a CDATA section? $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); $targetElement = $dom->createElement('target'); if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { foreach ($metadata['target-attributes'] as $name => $value) { $targetElement->setAttribute($name, $value); } } $t = $segment->appendChild($targetElement); $t->appendChild($text); $xliffFile->appendChild($translation); } return $dom->saveXML(); } private function hasMetadataArrayInfo(string $key, ?array $metadata = null): bool { return is_iterable($metadata[$key] ?? null); } } ================================================ FILE: Dumper/YamlFileDumper.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Dumper; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Util\ArrayConverter; use Symfony\Component\Yaml\Yaml; /** * YamlFileDumper generates yaml files from a message catalogue. * * @author Michel Salib */ class YamlFileDumper extends FileDumper { public function __construct( private string $extension = 'yml', ) { } public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string { if (!class_exists(Yaml::class)) { throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); } $data = $messages->all($domain); if (isset($options['as_tree']) && $options['as_tree']) { $data = ArrayConverter::expandToTree($data); } if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) { return Yaml::dump($data, $inline); } return Yaml::dump($data); } protected function getExtension(): string { return $this->extension; } } ================================================ FILE: Exception/ExceptionInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier */ interface ExceptionInterface extends \Throwable { } ================================================ FILE: Exception/IncompleteDsnException.php ================================================ * * 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); } } ================================================ FILE: Exception/InvalidArgumentException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base InvalidArgumentException for the Translation component. * * @author Abdellatif Ait boudad */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } ================================================ FILE: Exception/InvalidResourceException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource cannot be loaded. * * @author Fabien Potencier */ class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface { } ================================================ FILE: Exception/LogicException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base LogicException for Translation component. * * @author Abdellatif Ait boudad */ class LogicException extends \LogicException implements ExceptionInterface { } ================================================ FILE: Exception/MissingRequiredOptionException.php ================================================ * * 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); } } ================================================ FILE: Exception/NotFoundResourceException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Thrown when a resource does not exist. * * @author Fabien Potencier */ class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface { } ================================================ FILE: Exception/ProviderException.php ================================================ * * 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 */ class ProviderException extends RuntimeException implements ProviderExceptionInterface { private string $debug; public function __construct( string $message, private ResponseInterface $response, int $code = 0, ?\Exception $previous = null, ) { $this->debug = $response->getInfo('debug') ?? ''; parent::__construct($message, $code, $previous); } public function getResponse(): ResponseInterface { return $this->response; } public function getDebug(): string { return $this->debug; } } ================================================ FILE: Exception/ProviderExceptionInterface.php ================================================ * * 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 */ interface ProviderExceptionInterface extends ExceptionInterface { /* * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface */ public function getDebug(): string; } ================================================ FILE: Exception/RuntimeException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Exception; /** * Base RuntimeException for the Translation component. * * @author Abdellatif Ait boudad */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } ================================================ FILE: Exception/UnsupportedSchemeException.php ================================================ * * 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', ], 'phrase' => [ 'class' => Bridge\Phrase\PhraseProviderFactory::class, 'package' => 'symfony/phrase-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.'.'); } } ================================================ FILE: Extractor/AbstractFileExtractor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * Base class used by classes that extract translation messages from files. * * @author Marcos D. Sánchez */ abstract class AbstractFileExtractor { protected function extractFiles(string|iterable $resource): iterable { if (is_iterable($resource)) { $files = []; foreach ($resource as $file) { if ($this->canBeExtracted($file)) { $files[] = $this->toSplFileInfo($file); } } } elseif (is_file($resource)) { $files = $this->canBeExtracted($resource) ? [$this->toSplFileInfo($resource)] : []; } else { $files = $this->extractFromDirectory($resource); } return $files; } private function toSplFileInfo(string $file): \SplFileInfo { return new \SplFileInfo($file); } /** * @throws InvalidArgumentException */ protected function isFile(string $file): bool { if (!is_file($file)) { throw new InvalidArgumentException(\sprintf('The "%s" file does not exist.', $file)); } return true; } abstract protected function canBeExtracted(string $file): bool; abstract protected function extractFromDirectory(string|array $resource): iterable; } ================================================ FILE: Extractor/ChainExtractor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * ChainExtractor extracts translation messages from template files. * * @author Michel Salib */ class ChainExtractor implements ExtractorInterface { /** * The extractors. * * @var ExtractorInterface[] */ private array $extractors = []; /** * Adds a loader to the translation extractor. */ public function addExtractor(string $format, ExtractorInterface $extractor): void { $this->extractors[$format] = $extractor; } public function setPrefix(string $prefix): void { foreach ($this->extractors as $extractor) { $extractor->setPrefix($prefix); } } public function extract(string|iterable $directory, MessageCatalogue $catalogue): void { foreach ($this->extractors as $extractor) { $extractor->extract($directory, $catalogue); } } } ================================================ FILE: Extractor/ExtractorInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use Symfony\Component\Translation\MessageCatalogue; /** * Extracts translation messages from a directory or files to the catalogue. * New found messages are injected to the catalogue using the prefix. * * @author Michel Salib */ interface ExtractorInterface { /** * Extracts translation messages from files, a file or a directory to the catalogue. * * @param string|iterable $resource Files, a file or a directory */ public function extract(string|iterable $resource, MessageCatalogue $catalogue): void; /** * Sets the prefix that should be used for new found messages. */ public function setPrefix(string $prefix): void; } ================================================ FILE: Extractor/PhpAstExtractor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor; use PhpParser\Parser; use PhpParser\ParserFactory; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor; use Symfony\Component\Translation\MessageCatalogue; /** * PhpAstExtractor extracts translation messages from a PHP AST. * * @author Mathieu Santostefano */ final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorInterface { private Parser $parser; public function __construct( /** * @param iterable $visitors */ private readonly iterable $visitors, private string $prefix = '', ) { if (!class_exists(ParserFactory::class)) { throw new \LogicException(\sprintf('You cannot use "%s" as the "nikic/php-parser" package is not installed. Try running "composer require nikic/php-parser".', static::class)); } $this->parser = (new ParserFactory())->createForHostVersion(); } public function extract(iterable|string $resource, MessageCatalogue $catalogue): void { foreach ($this->extractFiles($resource) as $file) { $traverser = new NodeTraverser(); // This is needed to resolve namespaces in class methods/constants. $nameResolver = new NodeVisitor\NameResolver(); $traverser->addVisitor($nameResolver); foreach ($this->visitors as $visitor) { $visitor->initialize($catalogue, $file, $this->prefix); $traverser->addVisitor($visitor); } $nodes = $this->parser->parse(file_get_contents($file)); $traverser->traverse($nodes); } } public function setPrefix(string $prefix): void { $this->prefix = $prefix; } protected function canBeExtracted(string $file): bool { return 'php' === pathinfo($file, \PATHINFO_EXTENSION) && $this->isFile($file) && preg_match('/\bt\(|->trans\(|TranslatableMessage|Symfony\\\\Component\\\\Validator\\\\Constraints/i', file_get_contents($file)); } protected function extractFromDirectory(array|string $resource): iterable|Finder { if (!class_exists(Finder::class)) { throw new \LogicException(\sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class)); } return (new Finder())->files()->name('*.php')->in($resource); } } ================================================ FILE: Extractor/Visitor/AbstractVisitor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor\Visitor; use PhpParser\Node; use Symfony\Component\Translation\MessageCatalogue; /** * @author Mathieu Santostefano */ abstract class AbstractVisitor { private MessageCatalogue $catalogue; private \SplFileInfo $file; private string $messagePrefix; public function initialize(MessageCatalogue $catalogue, \SplFileInfo $file, string $messagePrefix): void { $this->catalogue = $catalogue; $this->file = $file; $this->messagePrefix = $messagePrefix; } protected function addMessageToCatalogue(string $message, ?string $domain, int $line): void { $domain ??= 'messages'; $this->catalogue->set($message, $this->messagePrefix.$message, $domain); $metadata = $this->catalogue->getMetadata($message, $domain) ?? []; $normalizedFilename = preg_replace('{[\\\\/]+}', '/', $this->file); $metadata['sources'][] = $normalizedFilename.':'.$line; $this->catalogue->setMetadata($message, $metadata, $domain); } protected function getStringArguments(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node, int|string $index, bool $indexIsRegex = false): array { if (\is_string($index)) { return $this->getStringNamedArguments($node, $index, $indexIsRegex); } $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; if (!($arg = $args[$index] ?? null) instanceof Node\Arg) { return []; } return (array) $this->getStringValue($arg->value); } protected function hasNodeNamedArguments(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node): bool { $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; foreach ($args as $arg) { if ($arg instanceof Node\Arg && null !== $arg->name) { return true; } } return false; } protected function nodeFirstNamedArgumentIndex(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node): int { $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; foreach ($args as $i => $arg) { if ($arg instanceof Node\Arg && null !== $arg->name) { return $i; } } return \PHP_INT_MAX; } private function getStringNamedArguments(Node\Expr\CallLike|Node\Attribute $node, ?string $argumentName = null, bool $isArgumentNamePattern = false): array { $args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args; $argumentValues = []; foreach ($args as $arg) { if (!$isArgumentNamePattern && $arg->name?->toString() === $argumentName) { $argumentValues[] = $this->getStringValue($arg->value); } elseif ($isArgumentNamePattern && preg_match($argumentName, $arg->name?->toString() ?? '') > 0) { $argumentValues[] = $this->getStringValue($arg->value); } } return array_filter($argumentValues); } private function getStringValue(Node $node): ?string { if ($node instanceof Node\Scalar\String_) { return $node->value; } if ($node instanceof Node\Expr\BinaryOp\Concat) { if (null === $left = $this->getStringValue($node->left)) { return null; } if (null === $right = $this->getStringValue($node->right)) { return null; } return $left.$right; } if ($node instanceof Node\Expr\Assign && $node->expr instanceof Node\Scalar\String_) { return $node->expr->value; } if ($node instanceof Node\Expr\ClassConstFetch) { try { $reflection = new \ReflectionClass($node->class->toString()); $constant = $reflection->getReflectionConstant($node->name->toString()); if (false !== $constant && \is_string($constant->getValue())) { return $constant->getValue(); } } catch (\ReflectionException) { } } return null; } } ================================================ FILE: Extractor/Visitor/ConstraintVisitor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor\Visitor; use PhpParser\Node; use PhpParser\NodeVisitor; /** * @author Mathieu Santostefano * * Code mostly comes from https://github.com/php-translation/extractor/blob/master/src/Visitor/Php/Symfony/Constraint.php */ final class ConstraintVisitor extends AbstractVisitor implements NodeVisitor { public function __construct( private readonly array $constraintClassNames = [], ) { } public function beforeTraverse(array $nodes): ?Node { return null; } public function enterNode(Node $node): ?Node { return null; } public function leaveNode(Node $node): ?Node { if (!$node instanceof Node\Expr\New_ && !$node instanceof Node\Attribute) { return null; } $className = $node instanceof Node\Attribute ? $node->name : $node->class; if (!$className instanceof Node\Name) { return null; } $parts = $className->getParts(); $isConstraintClass = false; foreach ($parts as $part) { if (\in_array($part, $this->constraintClassNames, true)) { $isConstraintClass = true; break; } } if (!$isConstraintClass) { return null; } $arg = $node->args[0] ?? null; if (!$arg instanceof Node\Arg) { return null; } if ($this->hasNodeNamedArguments($node)) { $messages = $this->getStringArguments($node, '/message/i', true); } else { if (!$arg->value instanceof Node\Expr\Array_) { // There is no way to guess which argument is a message to be translated. return null; } $messages = []; $options = $arg->value; foreach ($options->items as $item) { if (!$item->key instanceof Node\Scalar\String_) { continue; } if (false === stripos($item->key->value ?? '', 'message')) { continue; } if (!$item->value instanceof Node\Scalar\String_) { continue; } $messages[] = $item->value->value; break; } } foreach ($messages as $message) { $this->addMessageToCatalogue($message, 'validators', $node->getStartLine()); } return null; } public function afterTraverse(array $nodes): ?Node { return null; } } ================================================ FILE: Extractor/Visitor/TransMethodVisitor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor\Visitor; use PhpParser\Node; use PhpParser\NodeVisitor; /** * @author Mathieu Santostefano */ final class TransMethodVisitor extends AbstractVisitor implements NodeVisitor { public function beforeTraverse(array $nodes): ?Node { return null; } public function enterNode(Node $node): ?Node { return null; } public function leaveNode(Node $node): ?Node { if (!$node instanceof Node\Expr\MethodCall && !$node instanceof Node\Expr\FuncCall) { return null; } if (!\is_string($node->name) && !$node->name instanceof Node\Identifier && !$node->name instanceof Node\Name) { return null; } $name = $node->name instanceof Node\Name ? $node->name->getLast() : (string) $node->name; if ('trans' === $name || 't' === $name) { $firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node); if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'id')) { return null; } $domain = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'domain')[0] ?? null; foreach ($messages as $message) { $this->addMessageToCatalogue($message, $domain, $node->getStartLine()); } } return null; } public function afterTraverse(array $nodes): ?Node { return null; } } ================================================ FILE: Extractor/Visitor/TranslatableMessageVisitor.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Extractor\Visitor; use PhpParser\Node; use PhpParser\NodeVisitor; /** * @author Mathieu Santostefano */ final class TranslatableMessageVisitor extends AbstractVisitor implements NodeVisitor { public function beforeTraverse(array $nodes): ?Node { return null; } public function enterNode(Node $node): ?Node { return null; } public function leaveNode(Node $node): ?Node { if (!$node instanceof Node\Expr\New_) { return null; } if (!($className = $node->class) instanceof Node\Name) { return null; } if (!\in_array('TranslatableMessage', $className->getParts(), true)) { return null; } $firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node); if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'message')) { return null; } $domain = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'domain')[0] ?? null; foreach ($messages as $message) { $this->addMessageToCatalogue($message, $domain, $node->getStartLine()); } return null; } public function afterTraverse(array $nodes): ?Node { return null; } } ================================================ FILE: Formatter/IntlFormatter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\LogicException; /** * @author Guilherme Blanco * @author Abdellatif Ait boudad */ class IntlFormatter implements IntlFormatterInterface { private bool $hasMessageFormatter; private array $cache = []; public function formatIntl(string $message, string $locale, array $parameters = []): string { // MessageFormatter constructor throws an exception if the message is empty if ('' === $message) { return ''; } if (!$formatter = $this->cache[$locale][$message] ?? null) { if (!$this->hasMessageFormatter ??= class_exists(\MessageFormatter::class)) { throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.'); } try { $this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message); } catch (\IntlException $e) { throw new InvalidArgumentException(\sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e); } } foreach ($parameters as $key => $value) { if (\in_array($key[0] ?? null, ['%', '{'], true)) { unset($parameters[$key]); $parameters[trim($key, '%{ }')] = $value; } } if (false === $message = $formatter->format($parameters)) { throw new InvalidArgumentException(\sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage()); } return $message; } } ================================================ FILE: Formatter/IntlFormatterInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; /** * Formats ICU message patterns. * * @author Nicolas Grekas */ interface IntlFormatterInterface { /** * Formats a localized message using rules defined by ICU MessageFormat. * * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details */ public function formatIntl(string $message, string $locale, array $parameters = []): string; } ================================================ FILE: Formatter/MessageFormatter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Formatter; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(IntlFormatter::class); /** * @author Abdellatif Ait boudad */ class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface { private TranslatorInterface $translator; private IntlFormatterInterface $intlFormatter; /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ public function __construct(?TranslatorInterface $translator = null, ?IntlFormatterInterface $intlFormatter = null) { $this->translator = $translator ?? new IdentityTranslator(); $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } public function format(string $message, string $locale, array $parameters = []): string { return $this->translator->trans($message, $parameters, null, $locale); } public function formatIntl(string $message, string $locale, array $parameters = []): string { return $this->intlFormatter->formatIntl($message, $locale, $parameters); } } ================================================ FILE: Formatter/MessageFormatterInterface.php ================================================ * * 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 Guilherme Blanco * @author Abdellatif Ait boudad */ interface MessageFormatterInterface { /** * 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 string $locale The message locale * @param array $parameters An array of parameters for the message */ public function format(string $message, string $locale, array $parameters = []): string; } ================================================ FILE: IdentityTranslator.php ================================================ * * 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\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; /** * IdentityTranslator does not translate anything. * * @author Fabien Potencier */ class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface { use TranslatorTrait; public function setLocale(string $locale): void { $this->locale = $locale; } } ================================================ FILE: LICENSE ================================================ Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Loader/ArrayLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\MessageCatalogue; /** * ArrayLoader loads translations from a PHP array. * * @author Fabien Potencier */ class ArrayLoader implements LoaderInterface { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { $resource = $this->flatten($resource); $catalogue = new MessageCatalogue($locale); $catalogue->add($resource, $domain); return $catalogue; } /** * Flattens an nested array of translations. * * The scheme used is: * 'key' => ['key2' => ['key3' => 'value']] * Becomes: * 'key.key2.key3' => 'value' */ private function flatten(array $messages): array { $result = []; foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { if (null !== $v) { $result[$key.'.'.$k] = $v; } } } elseif (null !== $value) { $result[$key] = $value; } } return $result; } } ================================================ FILE: Loader/CsvFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\NotFoundResourceException; /** * CsvFileLoader loads translations from CSV files. * * @author Saša Stamenković */ class CsvFileLoader extends FileLoader { private string $delimiter = ';'; private string $enclosure = '"'; protected function loadResource(string $resource): array { $messages = []; try { $file = new \SplFileObject($resource, 'rb'); } catch (\RuntimeException $e) { throw new NotFoundResourceException(\sprintf('Error opening file "%s".', $resource), 0, $e); } $file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE); $file->setCsvControl($this->delimiter, $this->enclosure, ''); foreach ($file as $data) { if (false === $data) { continue; } if (!str_starts_with($data[0], '#') && isset($data[1]) && 2 === \count($data)) { $messages[$data[0]] = $data[1]; } } return $messages; } /** * Sets the delimiter and enclosure character for CSV. */ public function setCsvControl(string $delimiter = ';', string $enclosure = '"'): void { $this->delimiter = $delimiter; $this->enclosure = $enclosure; } } ================================================ FILE: Loader/FileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * @author Abdellatif Ait boudad */ abstract class FileLoader extends ArrayLoader { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { 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)); } $messages = $this->loadResource($resource); // empty resource $messages ??= []; // not an array if (!\is_array($messages)) { throw new InvalidResourceException(\sprintf('Unable to load file "%s".', $resource)); } $catalogue = parent::load($messages, $locale, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } /** * @throws InvalidResourceException if stream content has an invalid format */ abstract protected function loadResource(string $resource): array; } ================================================ FILE: Loader/IcuDatFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuDatFileLoader extends IcuResFileLoader { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!stream_is_local($resource.'.dat')) { throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource)); } if (!file_exists($resource.'.dat')) { throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception) { $rb = null; } if (!$rb) { throw new InvalidResourceException(\sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource.'.dat')); } return $catalogue; } } ================================================ FILE: Loader/IcuResFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * IcuResFileLoader loads translations from a resource bundle. * * @author stealth35 */ class IcuResFileLoader implements LoaderInterface { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!stream_is_local($resource)) { throw new InvalidResourceException(\sprintf('This is not a local file "%s".', $resource)); } if (!is_dir($resource)) { throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource)); } try { $rb = new \ResourceBundle($locale, $resource); } catch (\Exception) { $rb = null; } if (!$rb) { throw new InvalidResourceException(\sprintf('Cannot load resource "%s".', $resource)); } elseif (intl_is_failure($rb->getErrorCode())) { throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); } $messages = $this->flatten($rb); $catalogue = new MessageCatalogue($locale); $catalogue->add($messages, $domain); if (class_exists(DirectoryResource::class)) { $catalogue->addResource(new DirectoryResource($resource)); } return $catalogue; } /** * Flattens an ResourceBundle. * * The scheme used is: * key { key2 { key3 { "value" } } } * Becomes: * 'key.key2.key3' => 'value' * * This function takes an array by reference and will modify it * * @param \ResourceBundle $rb The ResourceBundle that will be flattened * @param array $messages Used internally for recursive calls * @param string|null $path Current path being parsed, used internally for recursive calls */ protected function flatten(\ResourceBundle $rb, array &$messages = [], ?string $path = null): array { foreach ($rb as $key => $value) { $nodePath = $path ? $path.'.'.$key : $key; if ($value instanceof \ResourceBundle) { $this->flatten($value, $messages, $nodePath); } else { $messages[$nodePath] = $value; } } return $messages; } } ================================================ FILE: Loader/IniFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * IniFileLoader loads translations from an ini file. * * @author stealth35 */ class IniFileLoader extends FileLoader { protected function loadResource(string $resource): array { return parse_ini_file($resource, true); } } ================================================ FILE: Loader/JsonFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * JsonFileLoader loads translations from an json file. * * @author singles */ class JsonFileLoader extends FileLoader { protected function loadResource(string $resource): array { $messages = []; if ($data = file_get_contents($resource)) { $messages = json_decode($data, true); if (0 < $errorCode = json_last_error()) { throw new InvalidResourceException('Error parsing JSON: '.$this->getJSONErrorMessage($errorCode)); } } return $messages; } /** * Translates JSON_ERROR_* constant into meaningful message. */ private function getJSONErrorMessage(int $errorCode): string { return match ($errorCode) { \JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', \JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', \JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', \JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', \JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', default => 'Unknown error', }; } } ================================================ FILE: Loader/LoaderInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\MessageCatalogue; /** * LoaderInterface is the interface implemented by all translation loaders. * * @author Fabien Potencier */ interface LoaderInterface { /** * Loads a locale. * * @throws NotFoundResourceException when the resource cannot be found * @throws InvalidResourceException when the resource cannot be loaded */ public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue; } ================================================ FILE: Loader/MoFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/) */ class MoFileLoader extends FileLoader { /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was little endian. */ public const MO_LITTLE_ENDIAN_MAGIC = 0x950412DE; /** * Magic used for validating the format of an MO file as well as * detecting if the machine used to create that file was big endian. */ public const MO_BIG_ENDIAN_MAGIC = 0xDE120495; /** * The size of the header of an MO file in bytes. */ public const MO_HEADER_SIZE = 28; /** * Parses machine object (MO) format, independent of the machine's endian it * was created on. Both 32bit and 64bit systems are supported. */ protected function loadResource(string $resource): array { $stream = fopen($resource, 'r'); $stat = fstat($stream); if ($stat['size'] < self::MO_HEADER_SIZE) { throw new InvalidResourceException('MO stream content has an invalid format.'); } $magic = unpack('V1', fread($stream, 4)); $magic = hexdec(substr(dechex(current($magic)), -8)); if (self::MO_LITTLE_ENDIAN_MAGIC == $magic) { $isBigEndian = false; } elseif (self::MO_BIG_ENDIAN_MAGIC == $magic) { $isBigEndian = true; } else { throw new InvalidResourceException('MO stream content has an invalid format.'); } // formatRevision $this->readLong($stream, $isBigEndian); $count = $this->readLong($stream, $isBigEndian); $offsetId = $this->readLong($stream, $isBigEndian); $offsetTranslated = $this->readLong($stream, $isBigEndian); // sizeHashes $this->readLong($stream, $isBigEndian); // offsetHashes $this->readLong($stream, $isBigEndian); $messages = []; for ($i = 0; $i < $count; ++$i) { $pluralId = null; $translated = null; fseek($stream, $offsetId + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $singularId = fread($stream, $length); if (str_contains($singularId, "\000")) { [$singularId, $pluralId] = explode("\000", $singularId); } fseek($stream, $offsetTranslated + $i * 8); $length = $this->readLong($stream, $isBigEndian); $offset = $this->readLong($stream, $isBigEndian); if ($length < 1) { continue; } fseek($stream, $offset); $translated = fread($stream, $length); if (str_contains($translated, "\000")) { $translated = explode("\000", $translated); } $ids = ['singular' => $singularId, 'plural' => $pluralId]; $item = compact('ids', 'translated'); if (!empty($item['ids']['singular'])) { $id = $item['ids']['singular']; if (isset($item['ids']['plural'])) { $id .= '|'.$item['ids']['plural']; } $messages[$id] = stripcslashes(implode('|', (array) $item['translated'])); } } fclose($stream); return array_filter($messages); } /** * Reads an unsigned long from stream respecting endianness. * * @param resource $stream */ private function readLong($stream, bool $isBigEndian): int { $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4)); $result = current($result); return (int) substr($result, -8); } } ================================================ FILE: Loader/PhpFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * PhpFileLoader loads translations from PHP files returning an array of translations. * * @author Fabien Potencier */ class PhpFileLoader extends FileLoader { private static ?array $cache = []; protected function loadResource(string $resource): array { if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL))) { self::$cache = null; } if (null === self::$cache) { return require $resource; } return self::$cache[$resource] ??= require $resource; } } ================================================ FILE: Loader/PoFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; /** * @copyright Copyright (c) 2010, Union of RAD https://github.com/UnionOfRAD/lithium * @copyright Copyright (c) 2012, Clemens Tolboom */ class PoFileLoader extends FileLoader { /** * Parses portable object (PO) format. * * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files * we should be able to parse files having: * * white-space * # translator-comments * #. extracted-comments * #: reference... * #, flag... * #| msgid previous-untranslated-string * msgid untranslated-string * msgstr translated-string * * extra or different lines are: * * #| msgctxt previous-context * #| msgid previous-untranslated-string * msgctxt context * * #| msgid previous-untranslated-string-singular * #| msgid_plural previous-untranslated-string-plural * msgid untranslated-string-singular * msgid_plural untranslated-string-plural * msgstr[0] translated-string-case-0 * ... * msgstr[N] translated-string-case-n * * The definition states: * - white-space and comments are optional. * - msgid "" that an empty singleline defines a header. * * This parser sacrifices some features of the reference implementation the * differences to that implementation are as follows. * - No support for comments spanning multiple lines. * - Translator and extracted comments are treated as being the same type. * - Message IDs are allowed to have other encodings as just US-ASCII. * * Items with an empty id are ignored. */ protected function loadResource(string $resource): array { $stream = fopen($resource, 'r'); $defaults = [ 'ids' => [], 'translated' => null, ]; $messages = []; $item = $defaults; $flags = []; while ($line = fgets($stream)) { $line = trim($line); if ('' === $line) { // Whitespace indicated current item is done if (!\in_array('fuzzy', $flags, true)) { $this->addMessage($messages, $item); } $item = $defaults; $flags = []; } elseif (str_starts_with($line, '#,')) { $flags = array_map('trim', explode(',', substr($line, 2))); } elseif (str_starts_with($line, 'msgid "')) { // We start a new msg so save previous // TODO: this fails when comments or contexts are added $this->addMessage($messages, $item); $item = $defaults; $item['ids']['singular'] = substr($line, 7, -1); } elseif (str_starts_with($line, 'msgstr "')) { $item['translated'] = substr($line, 8, -1); } elseif ('"' === $line[0]) { $continues = isset($item['translated']) ? 'translated' : 'ids'; if (\is_array($item[$continues])) { end($item[$continues]); $item[$continues][key($item[$continues])] .= substr($line, 1, -1); } else { $item[$continues] .= substr($line, 1, -1); } } elseif (str_starts_with($line, 'msgid_plural "')) { $item['ids']['plural'] = substr($line, 14, -1); } elseif (str_starts_with($line, 'msgstr[')) { $size = strpos($line, ']'); $item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1); } } // save last item if (!\in_array('fuzzy', $flags, true)) { $this->addMessage($messages, $item); } fclose($stream); return $messages; } /** * Save a translation item to the messages. * * A .po file could contain by error missing plural indexes. We need to * fix these before saving them. */ private function addMessage(array &$messages, array $item): void { if (!empty($item['ids']['singular'])) { $id = stripcslashes($item['ids']['singular']); if (isset($item['ids']['plural'])) { $id .= '|'.stripcslashes($item['ids']['plural']); } $translated = (array) $item['translated']; // PO are by definition indexed so sort by index. ksort($translated); // Make sure every index is filled. end($translated); $count = key($translated); // Fill missing spots with '-'. $empties = array_fill(0, $count + 1, '-'); $translated += $empties; ksort($translated); $messages[$id] = stripcslashes(implode('|', $translated)); } } } ================================================ FILE: Loader/QtFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Config\Resource\FileResource; 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; /** * QtFileLoader loads translations from QT Translations XML files. * * @author Benjamin Eberlei */ class QtFileLoader implements LoaderInterface { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { 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)); } if (!file_exists($resource)) { throw new NotFoundResourceException(\sprintf('File "%s" not found.', $resource)); } try { $dom = XmlUtils::loadFile($resource); } catch (\InvalidArgumentException $e) { throw new InvalidResourceException(\sprintf('Unable to load "%s".', $resource), $e->getCode(), $e); } $internalErrors = libxml_use_internal_errors(true); libxml_clear_errors(); $xpath = new \DOMXPath($dom); $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]'); $catalogue = new MessageCatalogue($locale); if (1 == $nodes->length) { $translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message'); foreach ($translations as $translation) { $translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue; if ($translationValue) { $catalogue->set( (string) $translation->getElementsByTagName('source')->item(0)->nodeValue, $translationValue, $domain ); } } if (class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } } libxml_use_internal_errors($internalErrors); return $catalogue; } } ================================================ FILE: Loader/XliffFileLoader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ 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\MessageCatalogueInterface; use Symfony\Component\Translation\Util\XliffUtils; /** * XliffFileLoader loads translations from XLIFF files. * * @author Fabien Potencier */ class XliffFileLoader implements LoaderInterface { public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue { if (!class_exists(XmlUtils::class)) { throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); } 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($dom, $catalogue, $domain); if (is_file($resource) && class_exists(FileResource::class)) { $catalogue->addResource(new FileResource($resource)); } return $catalogue; } private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void { $xliffVersion = XliffUtils::getVersionNumber($dom); if ('1.2' === $xliffVersion) { $this->extractXliff1($dom, $catalogue, $domain); } if (\in_array($xliffVersion, ['2.0', '2.1', '2.2'], true)) { $this->extractXliff2($dom, $catalogue, $domain); } } /** * Extract messages and metadata from DOMDocument into a MessageCatalogue. */ private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void { $xml = simplexml_import_dom($dom); $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; $xml->registerXPathNamespace('xliff', $namespace); foreach ($xml->xpath('//xliff:file') as $file) { $fileAttributes = $file->attributes(); $file->registerXPathNamespace('xliff', $namespace); foreach ($file->xpath('.//xliff:prop') as $prop) { $catalogue->setCatalogueMetadata($prop->attributes()['prop-type'], (string) $prop, $domain); } foreach ($file->xpath('.//xliff:trans-unit') as $translation) { $attributes = $translation->attributes(); if (!(isset($attributes['resname']) || isset($translation->source))) { continue; } $source = (string) (isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source); if (isset($translation->target) && 'needs-translation' === (string) $translation->target->attributes()['state'] && \in_array((string) $translation->target, [$source, (string) $translation->source], true) ) { continue; } // 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) ($translation->target ?? $translation->source), $encoding); $catalogue->set($source, $target, $domain); $metadata = [ 'source' => (string) $translation->source, 'file' => [ 'original' => (string) $fileAttributes['original'], ], ]; if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { $metadata['notes'] = $notes; } if (isset($translation->target) && $translation->target->attributes()) { $metadata['target-attributes'] = []; foreach ($translation->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($attributes['id'])) { $metadata['id'] = (string) $attributes['id']; } $catalogue->setMetadata($source, $metadata, $domain); } } } private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void { $xml = simplexml_import_dom($dom); $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); foreach ($xml->xpath('//xliff:unit') as $unit) { if (null !== $pgsSwitch = $unit->attributes('urn:oasis:names:tc:xliff:pgs:1.0')['switch'] ?? null) { $this->extractXliff2PgsUnit($unit, $catalogue, $domain, (string) $pgsSwitch, $encoding); continue; } foreach ($unit->segment as $segment) { $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 ?? $segment->source), $encoding); $catalogue->set((string) $source, $target, $domain); $metadata = []; if ($segment->attributes()) { $metadata['segment-attributes'] = []; foreach ($segment->attributes() as $key => $value) { $metadata['segment-attributes'][$key] = (string) $value; } } if (isset($segment->target) && $segment->target->attributes()) { $metadata['target-attributes'] = []; foreach ($segment->target->attributes() as $key => $value) { $metadata['target-attributes'][$key] = (string) $value; } } if (isset($unit->notes)) { $metadata['notes'] = []; foreach ($unit->notes->note as $noteNode) { $note = []; foreach ($noteNode->attributes() as $key => $value) { $note[$key] = (string) $value; } $note['content'] = (string) $noteNode; $metadata['notes'][] = $note; } } $catalogue->setMetadata((string) $source, $metadata, $domain); } } } private function extractXliff2PgsUnit(\SimpleXMLElement $unit, MessageCatalogue $catalogue, string $domain, string $pgsSwitch, ?string $encoding): void { $switches = $this->parsePgsSwitch($pgsSwitch); $attributes = $unit->attributes(); $source = (string) ($attributes['name'] ?? $attributes['id']); $cases = []; foreach ($unit->segment as $segment) { if (null === $pgsCase = $segment->attributes('urn:oasis:names:tc:xliff:pgs:1.0')['case'] ?? null) { continue; } $cases[(string) $pgsCase] = $this->extractPgsSegmentText($segment->target ?? $segment->source, $switches); } $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; $catalogue->set($source, $this->utf8ToCharset($this->buildIcuMessage($switches, $cases), $encoding), $intlDomain); $metadata = ['pgs-switch' => $pgsSwitch]; if (isset($unit->notes)) { $metadata['notes'] = []; foreach ($unit->notes->note as $noteNode) { $note = array_map('strval', $noteNode->attributes() ?? []); $note['content'] = (string) $noteNode; $metadata['notes'][] = $note; } } $catalogue->setMetadata($source, $metadata, $intlDomain); } private function parsePgsSwitch(string $pgsSwitch): array { $switches = []; foreach (preg_split('/\s+/', trim($pgsSwitch)) as $item) { $switches[] = array_combine(['type', 'variable'], explode(':', $item, 2)); } return $switches; } private function extractPgsSegmentText(\SimpleXMLElement $element, array $switches): string { $pluralVariables = []; foreach ($switches as $switch) { if ('plural' === $switch['type'] || 'ordinal' === $switch['type']) { $pluralVariables[$switch['variable']] = true; } } $text = ''; foreach (dom_import_simplexml($element)->childNodes as $child) { if (\XML_TEXT_NODE === $child->nodeType) { $text .= $child->textContent; } elseif (\XML_ELEMENT_NODE === $child->nodeType && 'ph' === $child->localName) { if (($disp = $child->getAttribute('disp')) && isset($pluralVariables[$disp])) { $text .= '#'; } elseif ($disp) { $text .= '{'.$disp.'}'; } } } return $text; } private function buildIcuMessage(array $switches, array $cases): string { if (1 === \count($switches)) { $switch = $switches[0]; $icuType = $this->getIcuType($switch['type']); $icuCases = []; foreach ($cases as $caseValue => $text) { $icuCases[] = $this->formatIcuCase($caseValue, $switch['type']).' {'.$text.'}'; } return '{'.$switch['variable'].', '.$icuType.', '.implode(' ', $icuCases).'}'; } $outerSwitch = $switches[0]; $innerSwitches = \array_slice($switches, 1); $icuType = $this->getIcuType($outerSwitch['type']); $grouped = []; foreach ($cases as $caseKey => $text) { $caseParts = explode(' ', $caseKey, 2); $grouped[$caseParts[0]][$caseParts[1] ?? 'other'] = $text; } $icuCases = []; foreach ($grouped as $caseValue => $innerCases) { $icuCases[] = $this->formatIcuCase($caseValue, $outerSwitch['type']).' {'.$this->buildIcuMessage($innerSwitches, $innerCases).'}'; } return '{'.$outerSwitch['variable'].', '.$icuType.', '.implode(' ', $icuCases).'}'; } private function getIcuType(string $pgsType): string { return match ($pgsType) { 'plural' => 'plural', 'ordinal' => 'selectordinal', default => 'select', }; } private function formatIcuCase(int|string $caseValue, string $switchType): string { return (\in_array($switchType, ['plural', 'ordinal'], true) && is_numeric($caseValue) ? '=' : '').$caseValue; } /** * Convert a UTF8 string to the specified encoding. */ private function utf8ToCharset(string $content, ?string $encoding = null): string { if ('UTF-8' !== $encoding && $encoding) { return mb_convert_encoding($content, $encoding, 'UTF-8'); } return $content; } private function parseNotesMetadata(?\SimpleXMLElement $noteElement = null, ?string $encoding = null): array { $notes = []; if (null === $noteElement) { return $notes; } /** @var \SimpleXMLElement $xmlNote */ foreach ($noteElement as $xmlNote) { $noteAttributes = $xmlNote->attributes(); $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; if (isset($noteAttributes['priority'])) { $note['priority'] = (int) $noteAttributes['priority']; } if (isset($noteAttributes['from'])) { $note['from'] = (string) $noteAttributes['from']; } $notes[] = $note; } return $notes; } private function isXmlString(string $resource): bool { return str_starts_with($resource, ' * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Loader; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser as YamlParser; use Symfony\Component\Yaml\Yaml; /** * YamlFileLoader loads translations from Yaml files. * * @author Fabien Potencier */ class YamlFileLoader extends FileLoader { private YamlParser $yamlParser; protected function loadResource(string $resource): array { if (!isset($this->yamlParser)) { if (!class_exists(YamlParser::class)) { throw new LogicException('Loading translations from the YAML format requires the Symfony Yaml component.'); } $this->yamlParser = new YamlParser(); } try { $messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT); } catch (ParseException $e) { throw new InvalidResourceException(\sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e); } if (null !== $messages && !\is_array($messages)) { throw new InvalidResourceException(\sprintf('Unable to load file "%s".', $resource)); } return $messages ?: []; } } ================================================ FILE: LocaleFallbackProvider.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * Derives fallback locales based on ICU parent locale information, by shortening locale * sub tags and ultimately by going through a list of configured fallback locales. * * Also provides locale string validation. * * @author Matthias Pigulla */ final class LocaleFallbackProvider { /** * @param string[] $localeFallbacks List of fallback locales to add _after_ the ones derived from ICU information * * @throws InvalidArgumentException If a locale contains invalid characters */ public function __construct( private array $localeFallbacks = [], ) { foreach ($localeFallbacks as $locale) { self::validateLocale($locale); } } /** * @return string[] */ public function computeFallbackLocales(string $locale): array { self::validateLocale($locale); static $parentLocales; $parentLocales ??= require __DIR__.'/Resources/data/parents.php'; $originLocale = $locale; $locales = []; while ($locale) { if ($parent = $parentLocales[$locale] ?? null) { $locale = 'root' !== $parent ? $parent : null; } elseif (\function_exists('locale_parse')) { $localeSubTags = locale_parse($locale); $locale = null; if (1 < \count($localeSubTags)) { array_pop($localeSubTags); $locale = locale_compose($localeSubTags) ?: null; } } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) { $locale = substr($locale, 0, $i); } else { $locale = null; } if (null !== $locale) { $locales[$locale] = $locale; } } foreach ($this->localeFallbacks as $fallback) { if ($fallback === $originLocale) { continue; } $locales[$fallback] = $fallback; } return array_keys($locales); } /** * Asserts that the locale is valid, throws an Exception if not. * * @throws InvalidArgumentException If the locale contains invalid characters */ public static function validateLocale(string $locale): void { if (!preg_match('/^[a-z0-9@_\.\-]*$/i', $locale)) { throw new InvalidArgumentException(\sprintf('Invalid "%s" locale.', $locale)); } } } ================================================ FILE: LocaleSwitcher.php ================================================ * * 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\Routing\RequestContext; use Symfony\Contracts\Translation\LocaleAwareInterface; /** * @author Kevin Bond */ class LocaleSwitcher implements LocaleAwareInterface { private string $defaultLocale; /** * @param LocaleAwareInterface[] $localeAwareServices */ public function __construct( private string $locale, private iterable $localeAwareServices, private ?RequestContext $requestContext = null, ) { $this->defaultLocale = $locale; } public function setLocale(string $locale): void { // Silently ignore if the intl extension is not loaded try { if (class_exists(\Locale::class, false)) { \Locale::setDefault($locale); } } catch (\Exception) { } $this->locale = $locale; $this->requestContext?->setParameter('_locale', $locale); foreach ($this->localeAwareServices as $service) { $service->setLocale($locale); } } public function getLocale(): string { return $this->locale; } /** * Switch to a new locale, execute a callback, then switch back to the original. * * @template T * * @param callable(string $locale):T $callback * * @return T */ public function runWithLocale(string $locale, callable $callback): mixed { $original = $this->getLocale(); $this->setLocale($locale); try { return $callback($locale); } finally { $this->setLocale($original); } } public function reset(): void { $this->setLocale($this->defaultLocale); } } ================================================ FILE: LoggingTranslator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; use Psr\Log\LoggerInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { public function __construct( private TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator, private LoggerInterface $logger, ) { } public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); $this->log($id, $domain, $locale); return $trans; } public function setLocale(string $locale): void { $prev = $this->translator->getLocale(); $this->translator->setLocale($locale); if ($prev === $locale) { return; } $this->logger->debug(\sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale)); } public function getLocale(): string { return $this->translator->getLocale(); } public function getCatalogue(?string $locale = null): MessageCatalogueInterface { return $this->translator->getCatalogue($locale); } public function getCatalogues(): array { return $this->translator->getCatalogues(); } public function getFallbackLocales(): array { if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { return $this->translator->getFallbackLocales(); } return []; } public function __call(string $method, array $args): mixed { return $this->translator->{$method}(...$args); } /** * Logs for missing translations. */ private function log(string $id, ?string $domain, ?string $locale): void { $domain ??= 'messages'; $catalogue = $this->translator->getCatalogue($locale); if ($catalogue->defines($id, $domain)) { return; } if ($catalogue->has($id, $domain)) { $this->logger->debug('Translation use fallback catalogue.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } else { $this->logger->warning('Translation not found.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); } } } ================================================ FILE: MessageCatalogue.php ================================================ * * 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\Config\Resource\ResourceInterface; use Symfony\Component\Translation\Exception\LogicException; /** * @author Fabien Potencier */ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface, CatalogueMetadataAwareInterface { private array $metadata = []; private array $catalogueMetadata = []; private array $resources = []; private ?MessageCatalogueInterface $fallbackCatalogue = null; private ?self $parent = null; /** * @param array $messages An array of messages classified by domain */ public function __construct( private string $locale, private array $messages = [], ) { } public function getLocale(): string { return $this->locale; } public function getDomains(): array { $domains = []; foreach ($this->messages as $domain => $messages) { if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); } $domains[$domain] = $domain; } return array_values($domains); } public function all(?string $domain = null): array { if (null !== $domain) { // skip messages merge if intl-icu requested explicitly if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { return $this->messages[$domain] ?? []; } return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []); } $allMessages = []; foreach ($this->messages as $domain => $messages) { if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); $allMessages[$domain] = $messages + ($allMessages[$domain] ?? []); } else { $allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages; } } return $allMessages; } public function set(string $id, string $translation, string $domain = 'messages'): void { $this->add([$id => $translation], $domain); } public function has(string $id, string $domain = 'messages'): bool { if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return true; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->has($id, $domain); } return false; } public function defines(string $id, string $domain = 'messages'): bool { return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); } public function get(string $id, string $domain = 'messages'): string { if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; } if (isset($this->messages[$domain][$id])) { return $this->messages[$domain][$id]; } if (null !== $this->fallbackCatalogue) { return $this->fallbackCatalogue->get($id, $domain); } return $id; } public function replace(array $messages, string $domain = 'messages'): void { unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); $this->add($messages, $domain); } public function add(array $messages, string $domain = 'messages'): void { $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX; foreach ($messages as $id => $message) { unset($this->messages[$altDomain][$id]); $this->messages[$domain][$id] = $message; } if ([] === ($this->messages[$altDomain] ?? null)) { unset($this->messages[$altDomain]); } } public function addCatalogue(MessageCatalogueInterface $catalogue): void { if ($catalogue->getLocale() !== $this->locale) { throw new LogicException(\sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale)); } foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) { $this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX); $messages = array_diff_key($messages, $intlMessages); } $this->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } if ($catalogue instanceof MetadataAwareInterface) { $metadata = $catalogue->getMetadata('', ''); $this->addMetadata($metadata); } if ($catalogue instanceof CatalogueMetadataAwareInterface) { $catalogueMetadata = $catalogue->getCatalogueMetadata('', ''); $this->addCatalogueMetadata($catalogueMetadata); } } public function addFallbackCatalogue(MessageCatalogueInterface $catalogue): void { // detect circular references $c = $catalogue; while ($c = $c->getFallbackCatalogue()) { if ($c->getLocale() === $this->getLocale()) { throw new LogicException(\sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } } $c = $this; do { if ($c->getLocale() === $catalogue->getLocale()) { throw new LogicException(\sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); } foreach ($catalogue->getResources() as $resource) { $c->addResource($resource); } } while ($c = $c->parent); $catalogue->parent = $this; $this->fallbackCatalogue = $catalogue; foreach ($catalogue->getResources() as $resource) { $this->addResource($resource); } } public function getFallbackCatalogue(): ?MessageCatalogueInterface { return $this->fallbackCatalogue; } public function getResources(): array { return array_values($this->resources); } public function addResource(ResourceInterface $resource): void { $this->resources[$resource->__toString()] = $resource; } public function getMetadata(string $key = '', string $domain = 'messages'): mixed { if ('' == $domain) { return $this->metadata; } if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX])) { if ('' === $key) { return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX]; } if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key])) { return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key]; } } if (isset($this->metadata[$domain])) { if ('' == $key) { return $this->metadata[$domain]; } if (isset($this->metadata[$domain][$key])) { return $this->metadata[$domain][$key]; } } return null; } public function setMetadata(string $key, mixed $value, string $domain = 'messages'): void { $this->metadata[$domain][$key] = $value; } public function deleteMetadata(string $key = '', string $domain = 'messages'): void { if ('' == $domain) { $this->metadata = []; } elseif ('' == $key) { unset($this->metadata[$domain]); } else { unset($this->metadata[$domain][$key]); } } public function getCatalogueMetadata(string $key = '', string $domain = 'messages'): mixed { if (!$domain) { return $this->catalogueMetadata; } if (isset($this->catalogueMetadata[$domain])) { if (!$key) { return $this->catalogueMetadata[$domain]; } if (isset($this->catalogueMetadata[$domain][$key])) { return $this->catalogueMetadata[$domain][$key]; } } return null; } public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'): void { $this->catalogueMetadata[$domain][$key] = $value; } public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'): void { if (!$domain) { $this->catalogueMetadata = []; } elseif (!$key) { unset($this->catalogueMetadata[$domain]); } else { unset($this->catalogueMetadata[$domain][$key]); } } /** * Adds current values with the new values. * * @param array $values Values to add */ private function addMetadata(array $values): void { foreach ($values as $domain => $keys) { foreach ($keys as $key => $value) { $this->setMetadata($key, $value, $domain); } } } private function addCatalogueMetadata(array $values): void { foreach ($values as $domain => $keys) { foreach ($keys as $key => $value) { $this->setCatalogueMetadata($key, $value, $domain); } } } } ================================================ FILE: MessageCatalogueInterface.php ================================================ * * 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\Config\Resource\ResourceInterface; /** * MessageCatalogueInterface. * * @author Fabien Potencier */ interface MessageCatalogueInterface { public const INTL_DOMAIN_SUFFIX = '+intl-icu'; /** * Gets the catalogue locale. */ public function getLocale(): string; /** * Gets the domains. */ public function getDomains(): array; /** * Gets the messages within a given domain. * * If $domain is null, it returns all messages. */ public function all(?string $domain = null): array; /** * Sets a message translation. * * @param string $id The message id * @param string $translation The messages translation * @param string $domain The domain name */ public function set(string $id, string $translation, string $domain = 'messages'): void; /** * Checks if a message has a translation. * * @param string $id The message id * @param string $domain The domain name */ public function has(string $id, string $domain = 'messages'): bool; /** * Checks if a message has a translation (it does not take into account the fallback mechanism). * * @param string $id The message id * @param string $domain The domain name */ public function defines(string $id, string $domain = 'messages'): bool; /** * Gets a message translation. * * @param string $id The message id * @param string $domain The domain name */ public function get(string $id, string $domain = 'messages'): string; /** * Sets translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function replace(array $messages, string $domain = 'messages'): void; /** * Adds translations for a given domain. * * @param array $messages An array of translations * @param string $domain The domain name */ public function add(array $messages, string $domain = 'messages'): void; /** * Merges translations from the given Catalogue into the current one. * * The two catalogues must have the same locale. */ public function addCatalogue(self $catalogue): void; /** * Merges translations from the given Catalogue into the current one * only when the translation does not exist. * * This is used to provide default translations when they do not exist for the current locale. */ public function addFallbackCatalogue(self $catalogue): void; /** * Gets the fallback catalogue. */ public function getFallbackCatalogue(): ?self; /** * Returns an array of resources loaded to build this collection. * * @return ResourceInterface[] */ public function getResources(): array; /** * Adds a resource for this collection. */ public function addResource(ResourceInterface $resource): void; } ================================================ FILE: MetadataAwareInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation; /** * This interface is used to get, set, and delete metadata about the translation messages. * * @author Fabien Potencier */ interface MetadataAwareInterface { /** * Gets metadata for the given domain and key. * * Passing an empty domain will return an array with all metadata indexed by * domain and then by key. Passing an empty key will return an array with all * metadata for the given domain. * * @return mixed The value that was set or an array with the domains/keys or null */ public function getMetadata(string $key = '', string $domain = 'messages'): mixed; /** * Adds metadata to a message domain. */ public function setMetadata(string $key, mixed $value, string $domain = 'messages'): void; /** * 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. */ public function deleteMetadata(string $key = '', string $domain = 'messages'): void; } ================================================ FILE: Provider/AbstractProviderFactory.php ================================================ * * 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 { return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.', $dsn->getScheme().'://'.$dsn->getHost()); } protected function getPassword(Dsn $dsn): string { return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); } } ================================================ FILE: Provider/Dsn.php ================================================ * * 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 ?string $scheme; private ?string $host; private ?string $user; private ?string $password; private ?int $port; private ?string $path; private array $options = []; private string $originalDsn; public function __construct(#[\SensitiveParameter] string $dsn) { $this->originalDsn = $dsn; if (false === $params = parse_url($dsn)) { throw new InvalidArgumentException('The translation provider DSN is invalid.'); } if (!isset($params['scheme'])) { throw new InvalidArgumentException('The translation provider DSN must contain a scheme.'); } $this->scheme = $params['scheme']; if (!isset($params['host'])) { throw new InvalidArgumentException('The translation provider DSN must contain a host (use "default" by default).'); } $this->host = $params['host']; $this->user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; $this->password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null; $this->port = $params['port'] ?? null; $this->path = $params['path'] ?? null; parse_str($params['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, mixed $default = null): mixed { return $this->options[$key] ?? $default; } public function getRequiredOption(string $key): mixed { 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; } } ================================================ FILE: Provider/FilteringProvider.php ================================================ * * 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 */ class FilteringProvider implements ProviderInterface { public function __construct( private ProviderInterface $provider, private array $locales, private array $domains = [], ) { $this->locales = array_filter($locales); } public function __toString(): string { return (string) $this->provider; } 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; } } ================================================ FILE: Provider/NullProvider.php ================================================ * * 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 */ 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 { } } ================================================ FILE: Provider/NullProviderFactory.php ================================================ * * 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 */ 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']; } } ================================================ FILE: Provider/ProviderFactoryInterface.php ================================================ * * 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; } ================================================ FILE: Provider/ProviderInterface.php ================================================ * * 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 extends \Stringable { /** * 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; } ================================================ FILE: Provider/TranslationProviderCollection.php ================================================ * * 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 */ final class TranslationProviderCollection { /** * @var array */ private array $providers; /** * @param array $providers */ public function __construct(iterable $providers) { $this->providers = \is_array($providers) ? $providers : iterator_to_array($providers); } 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); } } ================================================ FILE: Provider/TranslationProviderCollectionFactory.php ================================================ * * 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 */ class TranslationProviderCollectionFactory { /** * @param iterable $factories */ public function __construct( private iterable $factories, private array $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); } } ================================================ FILE: PseudoLocalizationTranslator.php ================================================ * * 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\LogicException; use Symfony\Contracts\Translation\TranslatorInterface; /** * This translator should only be used in a development environment. */ final class PseudoLocalizationTranslator implements TranslatorInterface, TranslatorBagInterface { private const EXPANSION_CHARACTER = '~'; private bool $accents; private float $expansionFactor; private bool $brackets; private bool $parseHTML; /** * @var string[] */ private array $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 or 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( private 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'] ?? []; } public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { $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(); } public function getCatalogue(?string $locale = null): MessageCatalogueInterface { if (!$this->translator instanceof TranslatorBagInterface) { throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class)); } return $this->translator->getCatalogue($locale); } public function getCatalogues(): array { if (!$this->translator instanceof TranslatorBagInterface) { throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class)); } return $this->translator->getCatalogues(); } private function getParts(string $originalTrans): array { if (!$this->parseHTML) { return [[true, true, $originalTrans]]; } $html = mb_encode_numericentity($originalTrans, [0x80, 0x10FFFF, 0, 0x1FFFFF], 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]; 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[] = [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); } } // @php-cs-fixer-ignore random_api_migration As logic is coupled with mt_srand() in tests ================================================ FILE: README.md ================================================ Translation Component ===================== The Translation component provides tools to internationalize your application. Getting Started --------------- ```bash composer require symfony/translation ``` ```php use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\Loader\ArrayLoader; $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'Hello World!' => 'Bonjour !', ], 'fr_FR'); echo $translator->trans('Hello World!'); // outputs « Bonjour ! » ``` Sponsor ------- Help Symfony by [sponsoring][3] its development! Resources --------- * [Documentation](https://symfony.com/doc/current/translation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) [3]: https://symfony.com/sponsor ================================================ FILE: Reader/TranslationReader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Finder\Finder; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Michel Salib */ class TranslationReader implements TranslationReaderInterface { /** * Loaders used for import. * * @var array */ private array $loaders = []; /** * Adds a loader to the translation extractor. * * @param string $format The format of the loader */ public function addLoader(string $format, LoaderInterface $loader): void { $this->loaders[$format] = $loader; } public function read(string $directory, MessageCatalogue $catalogue): void { if (!is_dir($directory)) { return; } foreach ($this->loaders as $format => $loader) { // load any existing translation files $finder = new Finder(); $extension = $catalogue->getLocale().'.'.$format; $files = $finder->files()->name('*.'.$extension)->in($directory); foreach ($files as $file) { $domain = substr($file->getFilename(), 0, -1 * \strlen($extension) - 1); $catalogue->addCatalogue($loader->load($file->getPathname(), $catalogue->getLocale(), $domain)); } } } } ================================================ FILE: Reader/TranslationReaderInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Reader; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationReader reads translation messages from translation files. * * @author Tobias Nyholm */ interface TranslationReaderInterface { /** * Reads translation messages from a directory to the catalogue. */ public function read(string $directory, MessageCatalogue $catalogue): void; } ================================================ FILE: Resources/bin/translation-status.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if ('cli' !== \PHP_SAPI) { throw new Exception('This script must be run from the command line.'); } $usageInstructions = << false, // NULL = analyze all locales 'locale_to_analyze' => null, // append --incomplete to only show incomplete languages 'include_completed_languages' => true, // the reference files all the other translations are compared to 'original_files' => [ 'src/Symfony/Component/Form/Resources/translations/validators.en.xlf', 'src/Symfony/Component/Security/Core/Resources/translations/security.en.xlf', 'src/Symfony/Component/Validator/Resources/translations/validators.en.xlf', ], ]; $argc = $_SERVER['argc']; $argv = $_SERVER['argv']; if ($argc > 4) { echo str_replace('translation-status.php', $argv[0], $usageInstructions); exit(1); } foreach (array_slice($argv, 1) as $argumentOrOption) { if ('--incomplete' === $argumentOrOption) { $config['include_completed_languages'] = false; continue; } if (str_starts_with($argumentOrOption, '-')) { $config['verbose_output'] = true; } else { $config['locale_to_analyze'] = $argumentOrOption; } } foreach ($config['original_files'] as $originalFilePath) { if (!file_exists($originalFilePath)) { echo sprintf('The following file does not exist. Make sure that you execute this command at the root dir of the Symfony code repository.%s %s', \PHP_EOL, $originalFilePath); exit(1); } } $totalMissingTranslations = 0; $totalTranslationMismatches = 0; foreach ($config['original_files'] as $originalFilePath) { $translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']); $translationStatus = calculateTranslationStatus($originalFilePath, $translationFilePaths); $totalMissingTranslations += array_sum(array_map(static fn ($translation) => count($translation['missingKeys']), array_values($translationStatus))); $totalTranslationMismatches += array_sum(array_map(static fn ($translation) => count($translation['mismatches']), array_values($translationStatus))); printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output'], $config['include_completed_languages']); } exit($totalTranslationMismatches > 0 ? 1 : 0); function findTranslationFiles($originalFilePath, $localeToAnalyze): array { $translations = []; $translationsDir = dirname($originalFilePath); $originalFileName = basename($originalFilePath); $translationFileNamePattern = str_replace('.en.', '.*.', $originalFileName); $translationFiles = glob($translationsDir.'/'.$translationFileNamePattern, \GLOB_NOSORT); sort($translationFiles); foreach ($translationFiles as $filePath) { $locale = extractLocaleFromFilePath($filePath); if (null !== $localeToAnalyze && $locale !== $localeToAnalyze) { continue; } $translations[$locale] = $filePath; } return $translations; } function calculateTranslationStatus($originalFilePath, $translationFilePaths): array { $translationStatus = []; $allTranslationKeys = extractTranslationKeys($originalFilePath); foreach ($translationFilePaths as $locale => $translationPath) { $translatedKeys = extractTranslationKeys($translationPath); $missingKeys = array_diff_key($allTranslationKeys, $translatedKeys); $mismatches = findTransUnitMismatches($allTranslationKeys, $translatedKeys); $translationStatus[$locale] = [ 'total' => count($allTranslationKeys), 'translated' => count($translatedKeys), 'missingKeys' => $missingKeys, 'mismatches' => $mismatches, ]; $translationStatus[$locale]['is_completed'] = isTranslationCompleted($translationStatus[$locale]); } return $translationStatus; } function isTranslationCompleted(array $translationStatus): bool { return $translationStatus['total'] === $translationStatus['translated'] && 0 === count($translationStatus['mismatches']); } function printTranslationStatus($originalFilePath, $translationStatus, $verboseOutput, $includeCompletedLanguages): void { printTitle($originalFilePath); printTable($translationStatus, $verboseOutput, $includeCompletedLanguages); echo \PHP_EOL.\PHP_EOL; } function extractLocaleFromFilePath($filePath) { $parts = explode('.', $filePath); return $parts[count($parts) - 2]; } function extractTranslationKeys($filePath): array { $translationKeys = []; $contents = new SimpleXMLElement(file_get_contents($filePath)); foreach ($contents->file->body->{'trans-unit'} as $translationKey) { $translationId = (string) $translationKey['id']; $translationKey = (string) ($translationKey['resname'] ?? $translationKey->source); $translationKeys[$translationId] = $translationKey; } return $translationKeys; } /** * Check whether the trans-unit id and source match with the base translation. */ function findTransUnitMismatches(array $baseTranslationKeys, array $translatedKeys): array { $mismatches = []; foreach ($baseTranslationKeys as $translationId => $translationKey) { if (!isset($translatedKeys[$translationId])) { continue; } if ($translatedKeys[$translationId] !== $translationKey) { $mismatches[$translationId] = [ 'found' => $translatedKeys[$translationId], 'expected' => $translationKey, ]; } } return $mismatches; } function printTitle($title): void { echo $title.\PHP_EOL; echo str_repeat('=', strlen($title)).\PHP_EOL.\PHP_EOL; } function printTable($translations, $verboseOutput, bool $includeCompletedLanguages): void { if (0 === count($translations)) { echo 'No translations found'; return; } $longestLocaleNameLength = max(array_map('strlen', array_keys($translations))); foreach ($translations as $locale => $translation) { if (!$includeCompletedLanguages && $translation['is_completed']) { continue; } if ($translation['translated'] > $translation['total']) { textColorRed(); } elseif (count($translation['mismatches']) > 0) { textColorRed(); } elseif ($translation['is_completed']) { textColorGreen(); } echo sprintf( '| Locale: %-'.$longestLocaleNameLength.'s | Translated: %2d/%2d | Mismatches: %d |', $locale, $translation['translated'], $translation['total'], count($translation['mismatches']) ).\PHP_EOL; textColorNormal(); $shouldBeClosed = false; if (true === $verboseOutput && count($translation['missingKeys']) > 0) { echo '| Missing Translations:'.\PHP_EOL; foreach ($translation['missingKeys'] as $id => $content) { echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL; } $shouldBeClosed = true; } if (true === $verboseOutput && count($translation['mismatches']) > 0) { echo '| Mismatches between trans-unit id and source:'.\PHP_EOL; foreach ($translation['mismatches'] as $id => $content) { echo sprintf('| (id=%s) Expected: %s', $id, $content['expected']).\PHP_EOL; echo sprintf('| Found: %s', $content['found']).\PHP_EOL; } $shouldBeClosed = true; } if ($shouldBeClosed) { echo str_repeat('-', 80).\PHP_EOL; } } } function textColorGreen(): void { echo "\033[32m"; } function textColorRed(): void { echo "\033[31m"; } function textColorNormal(): void { echo "\033[0m"; } ================================================ FILE: Resources/data/parents.json ================================================ { "az_Cyrl": "root", "bs_Cyrl": "root", "en_150": "en_001", "en_AG": "en_001", "en_AI": "en_001", "en_AT": "en_150", "en_AU": "en_001", "en_BB": "en_001", "en_BE": "en_150", "en_BM": "en_001", "en_BS": "en_001", "en_BW": "en_001", "en_BZ": "en_001", "en_CC": "en_001", "en_CH": "en_150", "en_CK": "en_001", "en_CM": "en_001", "en_CX": "en_001", "en_CY": "en_001", "en_CZ": "en_150", "en_DE": "en_150", "en_DG": "en_001", "en_DK": "en_150", "en_DM": "en_001", "en_EE": "en_150", "en_ER": "en_001", "en_ES": "en_150", "en_FI": "en_150", "en_FJ": "en_001", "en_FK": "en_001", "en_FM": "en_001", "en_FR": "en_150", "en_GB": "en_001", "en_GD": "en_001", "en_GE": "en_150", "en_GG": "en_001", "en_GH": "en_001", "en_GI": "en_001", "en_GM": "en_001", "en_GS": "en_001", "en_GY": "en_001", "en_HK": "en_001", "en_HU": "en_150", "en_ID": "en_001", "en_IE": "en_001", "en_IL": "en_001", "en_IM": "en_001", "en_IN": "en_001", "en_IO": "en_001", "en_IT": "en_150", "en_JE": "en_001", "en_JM": "en_001", "en_KE": "en_001", "en_KI": "en_001", "en_KN": "en_001", "en_KY": "en_001", "en_LC": "en_001", "en_LR": "en_001", "en_LS": "en_001", "en_LT": "en_150", "en_LV": "en_150", "en_MG": "en_001", "en_MO": "en_001", "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", "en_NF": "en_001", "en_NG": "en_001", "en_NL": "en_150", "en_NO": "en_150", "en_NR": "en_001", "en_NU": "en_001", "en_NZ": "en_001", "en_PG": "en_001", "en_PK": "en_001", "en_PL": "en_150", "en_PN": "en_001", "en_PT": "en_150", "en_PW": "en_001", "en_RO": "en_150", "en_RW": "en_001", "en_SB": "en_001", "en_SC": "en_001", "en_SD": "en_001", "en_SE": "en_150", "en_SG": "en_001", "en_SH": "en_001", "en_SI": "en_150", "en_SK": "en_150", "en_SL": "en_001", "en_SS": "en_001", "en_SX": "en_001", "en_SZ": "en_001", "en_TC": "en_001", "en_TK": "en_001", "en_TO": "en_001", "en_TT": "en_001", "en_TV": "en_001", "en_TZ": "en_001", "en_UA": "en_150", "en_UG": "en_001", "en_VC": "en_001", "en_VG": "en_001", "en_VU": "en_001", "en_WS": "en_001", "en_ZA": "en_001", "en_ZM": "en_001", "en_ZW": "en_001", "es_AR": "es_419", "es_BO": "es_419", "es_BR": "es_419", "es_BZ": "es_419", "es_CL": "es_419", "es_CO": "es_419", "es_CR": "es_419", "es_CU": "es_419", "es_DO": "es_419", "es_EC": "es_419", "es_GT": "es_419", "es_HN": "es_419", "es_MX": "es_419", "es_NI": "es_419", "es_PA": "es_419", "es_PE": "es_419", "es_PR": "es_419", "es_PY": "es_419", "es_SV": "es_419", "es_US": "es_419", "es_UY": "es_419", "es_VE": "es_419", "ff_Adlm": "root", "hi_Latn": "en_IN", "kk_Arab": "root", "ks_Deva": "root", "nb": "no", "nn": "no", "pa_Arab": "root", "pt_AO": "pt_PT", "pt_CH": "pt_PT", "pt_CV": "pt_PT", "pt_GQ": "pt_PT", "pt_GW": "pt_PT", "pt_LU": "pt_PT", "pt_MO": "pt_PT", "pt_MZ": "pt_PT", "pt_ST": "pt_PT", "pt_TL": "pt_PT", "sd_Deva": "root", "sr_Latn": "root", "uz_Arab": "root", "uz_Cyrl": "root", "zh_Hant": "root", "zh_Hant_MO": "zh_Hant_HK" } ================================================ FILE: Resources/data/parents.php ================================================ 'root', 'bs_Cyrl' => 'root', 'en_150' => 'en_001', 'en_AG' => 'en_001', 'en_AI' => 'en_001', 'en_AT' => 'en_150', 'en_AU' => 'en_001', 'en_BB' => 'en_001', 'en_BE' => 'en_150', 'en_BM' => 'en_001', 'en_BS' => 'en_001', 'en_BW' => 'en_001', 'en_BZ' => 'en_001', 'en_CC' => 'en_001', 'en_CH' => 'en_150', 'en_CK' => 'en_001', 'en_CM' => 'en_001', 'en_CX' => 'en_001', 'en_CY' => 'en_001', 'en_CZ' => 'en_150', 'en_DE' => 'en_150', 'en_DG' => 'en_001', 'en_DK' => 'en_150', 'en_DM' => 'en_001', 'en_EE' => 'en_150', 'en_ER' => 'en_001', 'en_ES' => 'en_150', 'en_FI' => 'en_150', 'en_FJ' => 'en_001', 'en_FK' => 'en_001', 'en_FM' => 'en_001', 'en_FR' => 'en_150', 'en_GB' => 'en_001', 'en_GD' => 'en_001', 'en_GE' => 'en_150', 'en_GG' => 'en_001', 'en_GH' => 'en_001', 'en_GI' => 'en_001', 'en_GM' => 'en_001', 'en_GS' => 'en_001', 'en_GY' => 'en_001', 'en_HK' => 'en_001', 'en_HU' => 'en_150', 'en_ID' => 'en_001', 'en_IE' => 'en_001', 'en_IL' => 'en_001', 'en_IM' => 'en_001', 'en_IN' => 'en_001', 'en_IO' => 'en_001', 'en_IT' => 'en_150', 'en_JE' => 'en_001', 'en_JM' => 'en_001', 'en_KE' => 'en_001', 'en_KI' => 'en_001', 'en_KN' => 'en_001', 'en_KY' => 'en_001', 'en_LC' => 'en_001', 'en_LR' => 'en_001', 'en_LS' => 'en_001', 'en_LT' => 'en_150', 'en_LV' => 'en_150', 'en_MG' => 'en_001', 'en_MO' => 'en_001', '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', 'en_NF' => 'en_001', 'en_NG' => 'en_001', 'en_NL' => 'en_150', 'en_NO' => 'en_150', 'en_NR' => 'en_001', 'en_NU' => 'en_001', 'en_NZ' => 'en_001', 'en_PG' => 'en_001', 'en_PK' => 'en_001', 'en_PL' => 'en_150', 'en_PN' => 'en_001', 'en_PT' => 'en_150', 'en_PW' => 'en_001', 'en_RO' => 'en_150', 'en_RW' => 'en_001', 'en_SB' => 'en_001', 'en_SC' => 'en_001', 'en_SD' => 'en_001', 'en_SE' => 'en_150', 'en_SG' => 'en_001', 'en_SH' => 'en_001', 'en_SI' => 'en_150', 'en_SK' => 'en_150', 'en_SL' => 'en_001', 'en_SS' => 'en_001', 'en_SX' => 'en_001', 'en_SZ' => 'en_001', 'en_TC' => 'en_001', 'en_TK' => 'en_001', 'en_TO' => 'en_001', 'en_TT' => 'en_001', 'en_TV' => 'en_001', 'en_TZ' => 'en_001', 'en_UA' => 'en_150', 'en_UG' => 'en_001', 'en_VC' => 'en_001', 'en_VG' => 'en_001', 'en_VU' => 'en_001', 'en_WS' => 'en_001', 'en_ZA' => 'en_001', 'en_ZM' => 'en_001', 'en_ZW' => 'en_001', 'es_AR' => 'es_419', 'es_BO' => 'es_419', 'es_BR' => 'es_419', 'es_BZ' => 'es_419', 'es_CL' => 'es_419', 'es_CO' => 'es_419', 'es_CR' => 'es_419', 'es_CU' => 'es_419', 'es_DO' => 'es_419', 'es_EC' => 'es_419', 'es_GT' => 'es_419', 'es_HN' => 'es_419', 'es_MX' => 'es_419', 'es_NI' => 'es_419', 'es_PA' => 'es_419', 'es_PE' => 'es_419', 'es_PR' => 'es_419', 'es_PY' => 'es_419', 'es_SV' => 'es_419', 'es_US' => 'es_419', 'es_UY' => 'es_419', 'es_VE' => 'es_419', 'ff_Adlm' => 'root', 'hi_Latn' => 'en_IN', 'kk_Arab' => 'root', 'ks_Deva' => 'root', 'nb' => 'no', 'nn' => 'no', 'pa_Arab' => 'root', 'pt_AO' => 'pt_PT', 'pt_CH' => 'pt_PT', 'pt_CV' => 'pt_PT', 'pt_GQ' => 'pt_PT', 'pt_GW' => 'pt_PT', 'pt_LU' => 'pt_PT', 'pt_MO' => 'pt_PT', 'pt_MZ' => 'pt_PT', 'pt_ST' => 'pt_PT', 'pt_TL' => 'pt_PT', 'sd_Deva' => 'root', 'sr_Latn' => 'root', 'uz_Arab' => 'root', 'uz_Cyrl' => 'root', 'zh_Hant' => 'root', 'zh_Hant_MO' => 'zh_Hant_HK', ]; ================================================ FILE: Resources/functions.php ================================================ * * 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); } } ================================================ FILE: Resources/schemas/xliff-core-1.2-transitional.xsd ================================================ Values for the attribute 'context-type'. Indicates a database content. Indicates the content of an element within an XML document. Indicates the name of an element within an XML document. Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found. Indicates a the number of parameters contained within the <source>. Indicates notes pertaining to the parameters in the <source>. Indicates the content of a record within a database. Indicates the name of a record within a database. Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file. Values for the attribute 'count-type'. Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts. Indicates the count units are translation units existing already in the same document. Indicates a total count. Values for the attribute 'ctype' when used other elements than <ph> or <x>. Indicates a run of bolded text. Indicates a run of text in italics. Indicates a run of underlined text. Indicates a run of hyper-text. Values for the attribute 'ctype' when used with <ph> or <x>. Indicates a inline image. Indicates a page break. Indicates a line break. Values for the attribute 'datatype'. Indicates Active Server Page data. Indicates C source file data. Indicates Channel Definition Format (CDF) data. Indicates ColdFusion data. Indicates C++ source file data. Indicates C-Sharp data. Indicates strings from C, ASM, and driver files data. Indicates comma-separated values data. Indicates database data. Indicates portions of document that follows data and contains metadata. Indicates portions of document that precedes data and contains metadata. Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import). Indicates standard user input screen data. Indicates HyperText Markup Language (HTML) data - document instance. Indicates content within an HTML document’s <body> element. Indicates Windows INI file data. Indicates Interleaf data. Indicates Java source file data (extension '.java'). Indicates Java property resource bundle data. Indicates Java list resource bundle data. Indicates JavaScript source file data. Indicates JScript source file data. Indicates information relating to formatting. Indicates LISP source file data. Indicates information relating to margin formats. Indicates a file containing menu. Indicates numerically identified string table. Indicates Maker Interchange Format (MIF) data. Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute. Indicates GNU Machine Object data. Indicates Message Librarian strings created by Novell's Message Librarian Tool. Indicates information to be displayed at the bottom of each page of a document. Indicates information to be displayed at the top of each page of a document. Indicates a list of property values (e.g., settings within INI files or preferences dialog). Indicates Pascal source file data. Indicates Hypertext Preprocessor data. Indicates plain text file (no formatting other than, possibly, wrapping). Indicates GNU Portable Object file. Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc. Indicates Windows .NET binary resources. Indicates Windows .NET Resources. Indicates Rich Text Format (RTF) data. Indicates Standard Generalized Markup Language (SGML) data - document instance. Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD). Indicates Scalable Vector Graphic (SVG) data. Indicates VisualBasic Script source file. Indicates warning message. Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file). Indicates Extensible HyperText Markup Language (XHTML) data - document instance. Indicates Extensible Markup Language (XML) data - document instance. Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD). Indicates Extensible Stylesheet Language (XSL) data. Indicates XUL elements. Values for the attribute 'mtype'. Indicates the marked text is an abbreviation. ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept. ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective'). ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging'). ISO-12620: A proper-name term, such as the name of an agency or other proper entity. ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another. ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language. Indicates the marked text is a date and/or time. ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign. ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form. ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula. ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record. ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy'). ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body. ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages. ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like. ISO-12620 2.1.17: A unit to track object. Indicates the marked text is a name. ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others. ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system. Indicates the marked text is a phrase. ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase. Indicates the marked text should not be translated. ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet. Indicates that the marked text represents a segment. ISO-12620 2.1.18.2: A fixed, lexicalized phrase. ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs'). ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system. ISO-12620 2.1.19: A fixed chunk of recurring text. ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof. ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry. ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language. Indicates the marked text is a term. ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted. ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system. ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza'). ISO-12620 2.1.9: One of the alternate forms of a term. Values for the attribute 'restype'. Indicates a Windows RC AUTO3STATE control. Indicates a Windows RC AUTOCHECKBOX control. Indicates a Windows RC AUTORADIOBUTTON control. Indicates a Windows RC BEDIT control. Indicates a bitmap, for example a BITMAP resource in Windows. Indicates a button object, for example a BUTTON control Windows. Indicates a caption, such as the caption of a dialog box. Indicates the cell in a table, for example the content of the <td> element in HTML. Indicates check box object, for example a CHECKBOX control in Windows. Indicates a menu item with an associated checkbox. Indicates a list box, but with a check-box for each item. Indicates a color selection dialog. Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows. Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234). Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403). Indicates a UI base class element that cannot be represented by any other element. Indicates a context menu. Indicates a Windows RC CTEXT control. Indicates a cursor, for example a CURSOR resource in Windows. Indicates a date/time picker. Indicates a Windows RC DEFPUSHBUTTON control. Indicates a dialog box. Indicates a Windows RC DLGINIT resource block. Indicates an edit box object, for example an EDIT control in Windows. Indicates a filename. Indicates a file dialog. Indicates a footnote. Indicates a font name. Indicates a footer. Indicates a frame object. Indicates a XUL grid element. Indicates a groupbox object, for example a GROUPBOX control in Windows. Indicates a header item. Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML. Indicates a Windows RC HEDIT control. Indicates a horizontal scrollbar. Indicates an icon, for example an ICON resource in Windows. Indicates a Windows RC IEDIT control. Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF. Indicates a label object. Indicates a label that is also a HTML link (not necessarily a URL). Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML). Indicates a listbox object, for example an LISTBOX control in Windows. Indicates an list item (an entry in a list). Indicates a Windows RC LTEXT control. Indicates a menu (a group of menu-items). Indicates a toolbar containing one or more tope level menus. Indicates a menu item (an entry in a menu). Indicates a XUL menuseparator element. Indicates a message, for example an entry in a MESSAGETABLE resource in Windows. Indicates a calendar control. Indicates an edit box beside a spin control. Indicates a catch all for rectangular areas. Indicates a standalone menu not necessarily associated with a menubar. Indicates a pushbox object, for example a PUSHBOX control in Windows. Indicates a Windows RC PUSHBUTTON control. Indicates a radio button object. Indicates a menuitem with associated radio button. Indicates raw data resources for an application. Indicates a row in a table. Indicates a Windows RC RTEXT control. Indicates a user navigable container used to show a portion of a document. Indicates a generic divider object (e.g. menu group separator). Windows accelerators, shortcuts in resource or property files. Indicates a UI control to indicate process activity but not progress. Indicates a splitter bar. Indicates a Windows RC STATE3 control. Indicates a window for providing feedback to the users, like 'read-only', etc. Indicates a string, for example an entry in a STRINGTABLE resource in Windows. Indicates a layers of controls with a tab to select layers. Indicates a display and edits regular two-dimensional tables of cells. Indicates a XUL textbox element. Indicates a UI button that can be toggled to on or off state. Indicates an array of controls, usually buttons. Indicates a pop up tool tip text. Indicates a bar with a pointer indicating a position within a certain range. Indicates a control that displays a set of hierarchical data. Indicates a URI (URN or URL). Indicates a Windows RC USERBUTTON control. Indicates a user-defined control like CONTROL control in Windows. Indicates the text of a variable. Indicates version information about a resource like VERSIONINFO in Windows. Indicates a vertical scrollbar. Indicates a graphical window. Values for the attribute 'size-unit'. Indicates a size in 8-bit bytes. Indicates a size in Unicode characters. Indicates a size in columns. Used for HTML text area. Indicates a size in centimeters. Indicates a size in dialog units, as defined in Windows resources. Indicates a size in 'font-size' units (as defined in CSS). Indicates a size in 'x-height' units (as defined in CSS). Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster' Indicates a size in inches. Indicates a size in millimeters. Indicates a size in percentage. Indicates a size in pixels. Indicates a size in point. Indicates a size in rows. Used for HTML text area. Values for the attribute 'state'. Indicates the terminating state. Indicates only non-textual information needs adaptation. Indicates both text and non-textual information needs adaptation. Indicates only non-textual information needs review. Indicates both text and non-textual information needs review. Indicates that only the text of the item needs to be reviewed. Indicates that the item needs to be translated. Indicates that the item is new. For example, translation units that were not in a previous version of the document. Indicates that changes are reviewed and approved. Indicates that the item has been translated. Values for the attribute 'state-qualifier'. Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously. Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.). Indicates a match based on matching IDs (in addition to matching text). Indicates a translation derived from a glossary. Indicates a translation derived from existing translation. Indicates a translation derived from machine translation. Indicates a translation derived from a translation repository. Indicates a translation derived from a translation memory. Indicates the translation is suggested by machine translation. Indicates that the item has been rejected because of incorrect grammar. Indicates that the item has been rejected because it is incorrect. Indicates that the item has been rejected because it is too long or too short. Indicates that the item has been rejected because of incorrect spelling. Indicates the translation is suggested by translation memory. Values for the attribute 'unit'. Refers to words. Refers to pages. Refers to <trans-unit> elements. Refers to <bin-unit> elements. Refers to glyphs. Refers to <trans-unit> and/or <bin-unit> elements. Refers to the occurrences of instances defined by the count-type value. Refers to characters. Refers to lines. Refers to sentences. Refers to paragraphs. Refers to segments. Refers to placeables (inline elements). Values for the attribute 'priority'. Highest priority. High priority. High priority, but not as important as 2. High priority, but not as important as 3. Medium priority, but more important than 6. Medium priority, but less important than 5. Low priority, but more important than 8. Low priority, but more important than 9. Low priority. Lowest priority. This value indicates that all properties can be reformatted. This value must be used alone. This value indicates that no properties should be reformatted. This value must be used alone. This value indicates that all information in the coord attribute can be modified. This value indicates that the x information in the coord attribute can be modified. This value indicates that the y information in the coord attribute can be modified. This value indicates that the cx information in the coord attribute can be modified. This value indicates that the cy information in the coord attribute can be modified. This value indicates that all the information in the font attribute can be modified. This value indicates that the name information in the font attribute can be modified. This value indicates that the size information in the font attribute can be modified. This value indicates that the weight information in the font attribute can be modified. This value indicates that the information in the css-style attribute can be modified. This value indicates that the information in the style attribute can be modified. This value indicates that the information in the exstyle attribute can be modified. Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document. Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed. Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed. Represents a translation proposal from a translation memory or other resource. Represents a previous version of the target element. Represents a rejected version of the target element. Represents a translation to be used for reference purposes only, for example from a related product or a different language. Represents a proposed translation that was used for the translation of the trans-unit, possibly modified. Values for the attribute 'coord'. Version values: 1.0 and 1.1 are allowed for backward compatibility. ================================================ FILE: Resources/schemas/xliff-core-2.0.xsd ================================================ ================================================ FILE: Resources/schemas/xliff-core-2.2.xsd ================================================ ================================================ FILE: Resources/schemas/xml.xsd ================================================

About the XML namespace

This schema document describes the XML namespace, in a form suitable for import by other schema documents.

See http://www.w3.org/XML/1998/namespace.html and http://www.w3.org/TR/REC-xml for information about this namespace.

Note that local names in this namespace are intended to be defined only by the World Wide Web Consortium or its subgroups. The names currently defined in this namespace are listed below. They should not be used with conflicting semantics by any Working Group, specification, or document instance.

See further below in this document for more information about how to refer to this schema document from your own XSD schema documents and about the namespace-versioning policy governing this schema document.

lang (as an attribute name)

denotes an attribute whose value is a language code for the natural language of the content of any element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

Notes

Attempting to install the relevant ISO 2- and 3-letter codes as the enumerated possible values is probably never going to be a realistic possibility.

See BCP 47 at http://www.rfc-editor.org/rfc/bcp/bcp47.txt and the IANA language subtag registry at http://www.iana.org/assignments/language-subtag-registry for further information.

The union allows for the 'un-declaration' of xml:lang with the empty string.

space (as an attribute name)

denotes an attribute whose value is a keyword indicating what whitespace processing discipline is intended for the content of the element; its value is inherited. This name is reserved by virtue of its definition in the XML specification.

base (as an attribute name)

denotes an attribute whose value provides a URI to be used as the base for interpreting any relative URIs in the scope of the element on which it appears; its value is inherited. This name is reserved by virtue of its definition in the XML Base specification.

See http://www.w3.org/TR/xmlbase/ for information about this attribute.

id (as an attribute name)

denotes an attribute whose value should be interpreted as if declared to be of type ID. This name is reserved by virtue of its definition in the xml:id specification.

See http://www.w3.org/TR/xml-id/ for information about this attribute.

Father (in any context at all)

denotes Jon Bosak, the chair of the original XML Working Group. This name is reserved by the following decision of the W3C XML Plenary and XML Coordination groups:

In appreciation for his vision, leadership and dedication the W3C XML Plenary on this 10th day of February, 2000, reserves for Jon Bosak in perpetuity the XML name "xml:Father".

About this schema document

This schema defines attributes and an attribute group suitable for use by schemas wishing to allow xml:base, xml:lang, xml:space or xml:id attributes on elements they define.

To enable this, such a schema must import this schema for the XML namespace, e.g. as follows:

          <schema.. .>
          .. .
           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
     

or


           <import namespace="http://www.w3.org/XML/1998/namespace"
                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
     

Subsequently, qualified reference to any of the attributes or the group defined below will have the desired effect, e.g.

          <type.. .>
          .. .
           <attributeGroup ref="xml:specialAttrs"/>
     

will define a type which will schema-validate an instance element with any of those attributes.

Versioning policy for this schema document

In keeping with the XML Schema WG's standard versioning policy, this schema document will persist at http://www.w3.org/2009/01/xml.xsd.

At the date of issue it can also be found at http://www.w3.org/2001/xml.xsd.

The schema document at that URI may however change in the future, in order to remain compatible with the latest version of XML Schema itself, or with the XML namespace itself. In other words, if the XML Schema or XML namespaces change, the version of this document at http://www.w3.org/2001/xml.xsd will change accordingly; the version at http://www.w3.org/2009/01/xml.xsd will not change.

Previous dated (and unchanging) versions of this schema document are at:

================================================ FILE: StaticMessage.php ================================================ * * 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; final class StaticMessage implements TranslatableInterface { public function __construct( private string $message, ) { } public function getMessage(): string { return $this->message; } public function trans(TranslatorInterface $translator, ?string $locale = null): string { return $this->getMessage(); } } ================================================ FILE: Test/AbstractProviderFactoryTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Test; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; use Symfony\Component\Translation\Provider\Dsn; use Symfony\Component\Translation\Provider\ProviderFactoryInterface; abstract class AbstractProviderFactoryTestCase extends TestCase { abstract public function createFactory(): ProviderFactoryInterface; /** * @return iterable */ abstract public static function supportsProvider(): iterable; /** * @return iterable */ abstract public static function createProvider(): iterable; /** * @return iterable */ abstract public static function unsupportedSchemeProvider(): iterable; /** * @dataProvider supportsProvider */ #[DataProvider('supportsProvider')] public function testSupports(bool $expected, string $dsn) { $factory = $this->createFactory(); $this->assertSame($expected, $factory->supports(new Dsn($dsn))); } /** * @dataProvider createProvider */ #[DataProvider('createProvider')] public function testCreate(string $expected, string $dsn) { $factory = $this->createFactory(); $provider = $factory->create(new Dsn($dsn)); $this->assertSame($expected, (string) $provider); } /** * @dataProvider unsupportedSchemeProvider */ #[DataProvider('unsupportedSchemeProvider')] public function testUnsupportedSchemeException(string $dsn, ?string $message = null) { $factory = $this->createFactory(); $dsn = new Dsn($dsn); $this->expectException(UnsupportedSchemeException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } } ================================================ FILE: Test/IncompleteDsnTestTrait.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Test; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Translation\Exception\IncompleteDsnException; use Symfony\Component\Translation\Provider\Dsn; trait IncompleteDsnTestTrait { /** * @return iterable */ abstract public static function incompleteDsnProvider(): iterable; /** * @dataProvider incompleteDsnProvider */ #[DataProvider('incompleteDsnProvider')] public function testIncompleteDsnException(string $dsn, ?string $message = null) { $factory = $this->createFactory(); $dsn = new Dsn($dsn); $this->expectException(IncompleteDsnException::class); if (null !== $message) { $this->expectExceptionMessage($message); } $factory->create($dsn); } } ================================================ FILE: Test/ProviderTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Test; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\TranslatorBag; use Symfony\Component\Translation\TranslatorBagInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** * A test case to ease testing a translation provider. * * @author Mathieu Santostefano */ abstract class ProviderTestCase extends TestCase { protected HttpClientInterface $client; protected LoggerInterface|MockObject $logger; protected string $defaultLocale; protected LoaderInterface|MockObject $loader; protected XliffFileDumper|MockObject $xliffFileDumper; protected TranslatorBagInterface|MockObject $translatorBag; abstract public static function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface; /** * @return iterable */ abstract public static function toStringProvider(): iterable; /** * @dataProvider toStringProvider */ #[DataProvider('toStringProvider')] public function testToString(ProviderInterface $provider, string $expected) { $this->assertSame($expected, (string) $provider); } protected function getClient(): MockHttpClient { return $this->client ??= new MockHttpClient(); } protected function getLoader(): LoaderInterface { return $this->loader ??= new ArrayLoader(); } protected function getLogger(): LoggerInterface { return $this->logger ??= new NullLogger(); } protected function getDefaultLocale(): string { return $this->defaultLocale ??= 'en'; } protected function getXliffFileDumper(): XliffFileDumper { return $this->xliffFileDumper ??= new XliffFileDumper(); } protected function getTranslatorBag(): TranslatorBagInterface { return $this->translatorBag ??= new TranslatorBag(); } } ================================================ FILE: Tests/Catalogue/AbstractOperationTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Catalogue; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; abstract class AbstractOperationTestCase extends TestCase { public function testGetEmptyDomains() { $this->assertEquals( [], $this->createOperation( new MessageCatalogue('en'), new MessageCatalogue('en') )->getDomains() ); } public function testGetMergedDomains() { $this->assertEquals( ['a', 'b', 'c'], $this->createOperation( new MessageCatalogue('en', ['a' => [], 'b' => []]), new MessageCatalogue('en', ['b' => [], 'c' => []]) )->getDomains() ); } public function testGetMessagesFromUnknownDomain() { $this->expectException(\InvalidArgumentException::class); $this->createOperation( new MessageCatalogue('en'), new MessageCatalogue('en') )->getMessages('domain'); } public function testGetEmptyMessages() { $this->assertEquals( [], $this->createOperation( new MessageCatalogue('en', ['a' => []]), new MessageCatalogue('en') )->getMessages('a') ); } public function testGetEmptyResult() { $this->assertEquals( new MessageCatalogue('en'), $this->createOperation( new MessageCatalogue('en'), new MessageCatalogue('en') )->getResult() ); } public function testSourceAndTargetWithDifferentLocales() { $this->expectException(LogicException::class); $this->expectExceptionMessage('Operated catalogues must belong to the same locale.'); $this->createOperation( new MessageCatalogue('en'), new MessageCatalogue('fr') ); } abstract protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target); } ================================================ FILE: Tests/Catalogue/MergeOperationTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Catalogue; use Symfony\Component\Translation\Catalogue\MergeOperation; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; class MergeOperationTest extends AbstractOperationTestCase { public function testGetMessagesFromSingleDomain() { $operation = $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a', 'c' => 'new_c']]) ); $this->assertEquals( ['a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c'], $operation->getMessages('messages') ); $this->assertEquals( ['c' => 'new_c'], $operation->getNewMessages('messages') ); $this->assertEquals( [], $operation->getObsoleteMessages('messages') ); } public function testGetResultFromSingleDomain() { $this->assertEquals( new MessageCatalogue('en', [ 'messages' => ['a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a', 'c' => 'new_c']]) )->getResult() ); } public function testGetResultFromIntlDomain() { $this->assertEquals( new MessageCatalogue('en', [ 'messages' => ['b' => 'old_b'], 'messages+intl-icu' => ['d' => 'old_d', 'c' => 'new_c', 'a' => 'new_a'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b'], 'messages+intl-icu' => ['d' => 'old_d']]), new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'new_a', 'c' => 'new_c']]) )->getResult() ); } public function testGetResultWithMetadata() { $leftCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]); $leftCatalogue->setMetadata('a', 'foo', 'messages'); $leftCatalogue->setMetadata('b', 'bar', 'messages'); $rightCatalogue = new MessageCatalogue('en', ['messages' => ['b' => 'new_b', 'c' => 'new_c']]); $rightCatalogue->setMetadata('b', 'baz', 'messages'); $rightCatalogue->setMetadata('c', 'qux', 'messages'); $mergedCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c']]); $mergedCatalogue->setMetadata('a', 'foo', 'messages'); $mergedCatalogue->setMetadata('b', 'bar', 'messages'); $mergedCatalogue->setMetadata('c', 'qux', 'messages'); $this->assertEquals( $mergedCatalogue, $this->createOperation( $leftCatalogue, $rightCatalogue )->getResult() ); } public function testGetResultWithMetadataFromIntlDomain() { $leftCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'old_a', 'b' => 'old_b']]); $leftCatalogue->setMetadata('a', 'foo', 'messages+intl-icu'); $leftCatalogue->setMetadata('b', 'bar', 'messages+intl-icu'); $rightCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['b' => 'new_b', 'c' => 'new_c']]); $rightCatalogue->setMetadata('b', 'baz', 'messages+intl-icu'); $rightCatalogue->setMetadata('c', 'qux', 'messages+intl-icu'); $mergedCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'old_a', 'b' => 'old_b', 'c' => 'new_c']]); $mergedCatalogue->setMetadata('a', 'foo', 'messages+intl-icu'); $mergedCatalogue->setMetadata('b', 'bar', 'messages+intl-icu'); $mergedCatalogue->setMetadata('c', 'qux', 'messages+intl-icu'); $this->assertEquals( $mergedCatalogue, $this->createOperation( $leftCatalogue, $rightCatalogue )->getResult() ); } protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target) { return new MergeOperation($source, $target); } } ================================================ FILE: Tests/Catalogue/MessageCatalogueTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Catalogue; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\MessageCatalogue; class MessageCatalogueTest extends TestCase { public function testIcuMetadataKept() { $mc = new MessageCatalogue('en', ['messages' => ['a' => 'new_a']]); $metadata = ['metadata' => 'value']; $mc->setMetadata('a', $metadata, 'messages+intl-icu'); $this->assertEquals($metadata, $mc->getMetadata('a', 'messages')); $this->assertEquals($metadata, $mc->getMetadata('a', 'messages+intl-icu')); } } ================================================ FILE: Tests/Catalogue/TargetOperationTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Catalogue; use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogueInterface; class TargetOperationTest extends AbstractOperationTestCase { public function testGetMessagesFromSingleDomain() { $operation = $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a', 'c' => 'new_c']]) ); $this->assertEquals( ['a' => 'old_a', 'c' => 'new_c'], $operation->getMessages('messages') ); $this->assertEquals( ['c' => 'new_c'], $operation->getNewMessages('messages') ); $this->assertEquals( ['b' => 'old_b'], $operation->getObsoleteMessages('messages') ); } public function testGetResultFromSingleDomain() { $this->assertEquals( new MessageCatalogue('en', [ 'messages' => ['a' => 'old_a', 'c' => 'new_c'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a', 'c' => 'new_c']]) )->getResult() ); } public function testGetResultFromIntlDomain() { $this->assertEquals( new MessageCatalogue('en', [ 'messages' => ['a' => 'old_a'], 'messages+intl-icu' => ['c' => 'new_c'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a'], 'messages+intl-icu' => ['b' => 'old_b']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a'], 'messages+intl-icu' => ['c' => 'new_c']]) )->getResult() ); } public function testGetResultWithMixedDomains() { $this->assertEquals( new MessageCatalogue('en', [ 'messages+intl-icu' => ['a' => 'new_a'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a']]), new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'new_a']]) )->getResult() ); $this->assertEquals( new MessageCatalogue('en', [ 'messages+intl-icu' => ['a' => 'old_a'], ]), $this->createOperation( new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'old_a']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a']]) )->getResult() ); $this->assertEquals( new MessageCatalogue('en', [ 'messages+intl-icu' => ['a' => 'old_a'], 'messages' => ['b' => 'new_b'], ]), $this->createOperation( new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'old_a']]), new MessageCatalogue('en', ['messages' => ['a' => 'new_a', 'b' => 'new_b']]) )->getResult() ); $this->assertEquals( new MessageCatalogue('en', [ 'messages+intl-icu' => ['b' => 'new_b', 'a' => 'new_a'], ]), $this->createOperation( new MessageCatalogue('en', ['messages' => ['a' => 'old_a']]), new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'new_a', 'b' => 'new_b']]) )->getResult() ); } public function testGetResultWithMetadata() { $leftCatalogue = new MessageCatalogue('en', ['messages' => ['a' => 'old_a', 'b' => 'old_b']]); $leftCatalogue->setMetadata('a', 'foo', 'messages'); $leftCatalogue->setMetadata('b', 'bar', 'messages'); $rightCatalogue = new MessageCatalogue('en', ['messages' => ['b' => 'new_b', 'c' => 'new_c']]); $rightCatalogue->setMetadata('b', 'baz', 'messages'); $rightCatalogue->setMetadata('c', 'qux', 'messages'); $diffCatalogue = new MessageCatalogue('en', ['messages' => ['b' => 'old_b', 'c' => 'new_c']]); $diffCatalogue->setMetadata('b', 'bar', 'messages'); $diffCatalogue->setMetadata('c', 'qux', 'messages'); $this->assertEquals( $diffCatalogue, $this->createOperation( $leftCatalogue, $rightCatalogue )->getResult() ); } public function testGetResultWithMetadataFromIntlDomain() { $leftCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['a' => 'old_a', 'b' => 'old_b']]); $leftCatalogue->setMetadata('a', 'foo', 'messages+intl-icu'); $leftCatalogue->setMetadata('b', 'bar', 'messages+intl-icu'); $rightCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['b' => 'new_b', 'c' => 'new_c']]); $rightCatalogue->setMetadata('b', 'baz', 'messages+intl-icu'); $rightCatalogue->setMetadata('c', 'qux', 'messages+intl-icu'); $diffCatalogue = new MessageCatalogue('en', ['messages+intl-icu' => ['b' => 'old_b', 'c' => 'new_c']]); $diffCatalogue->setMetadata('b', 'bar', 'messages+intl-icu'); $diffCatalogue->setMetadata('c', 'qux', 'messages+intl-icu'); $this->assertEquals( $diffCatalogue, $this->createOperation( $leftCatalogue, $rightCatalogue )->getResult() ); } protected function createOperation(MessageCatalogueInterface $source, MessageCatalogueInterface $target) { return new TargetOperation($source, $target); } } ================================================ FILE: Tests/Command/TranslationLintCommandTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Command; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Translation\Command\TranslationLintCommand; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Translator; final class TranslationLintCommandTest extends TestCase { #[RequiresPhpExtension('intl')] public function testLintCorrectTranslations() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['hello' => 'Hello!'], 'en', 'messages'); $translator->addResource('array', [ 'hello_name' => 'Hello {name}!', 'num_of_apples' => <<addResource('array', ['hello' => 'Bonjour !'], 'fr', 'messages'); $translator->addResource('array', [ 'hello_name' => 'Bonjour {name} !', 'num_of_apples' => <<createCommand($translator, ['en', 'fr']); $commandTester = new CommandTester($command); $commandTester->execute([], ['decorated' => false]); $commandTester->assertCommandIsSuccessful(); $display = $this->getNormalizedDisplay($commandTester); $this->assertStringContainsString('[OK] All translations are valid.', $display); } #[RequiresPhpExtension('intl')] public function testLintMalformedIcuTranslations() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['hello' => 'Hello!'], 'en', 'messages'); $translator->addResource('array', [ 'hello_name' => 'Hello {name}!', // Missing "other" case 'num_of_apples' => <<addResource('array', ['hello' => 'Bonjour !'], 'fr', 'messages'); $translator->addResource('array', [ // Missing "}" 'hello_name' => 'Bonjour {name !', // "other" is translated 'num_of_apples' => <<createCommand($translator, ['en', 'fr']); $commandTester = new CommandTester($command); $this->assertSame(1, $commandTester->execute([], ['decorated' => false])); $display = $this->getNormalizedDisplay($commandTester); $this->assertStringContainsString(<<assertStringContainsString(\sprintf(<<= 80500 ? 'MessageFormatter::__construct()' : 'msgfmt_create' ), $display); if (\PHP_VERSION_ID >= 80500) { $this->assertStringContainsString(<<assertStringContainsString(<<addCommand($command); return $command; } /** * Normalize the CommandTester display, by removing trailing spaces for each line. */ private function getNormalizedDisplay(CommandTester $commandTester): string { return implode("\n", array_map(rtrim(...), explode("\n", $commandTester->getDisplay(true)))); } } ================================================ FILE: Tests/Command/TranslationProviderTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Command; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Translation\Provider\FilteringProvider; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\Provider\TranslationProviderCollection; /** * @author Mathieu Santostefano */ abstract class TranslationProviderTestCase extends TestCase { protected Filesystem $fs; protected string $translationAppDir; protected array $files; protected string $defaultLocale; protected function setUp(): void { $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); $this->fs = new Filesystem(); $this->translationAppDir = tempnam(sys_get_temp_dir(), 'sf_translation_'); $this->fs->remove($this->translationAppDir); $this->fs->mkdir($this->translationAppDir.'/translations'); } protected function tearDown(): void { \Locale::setDefault($this->defaultLocale); $this->fs->remove($this->translationAppDir); } protected function getProviderCollection(ProviderInterface $provider, array $providerNames = ['loco'], array $locales = ['en'], array $domains = ['messages']): TranslationProviderCollection { $collection = []; foreach ($providerNames as $providerName) { $collection[$providerName] = new FilteringProvider($provider, $locales, $domains); } return new TranslationProviderCollection($collection); } protected function createYamlFile(array $messages = ['node' => 'NOTE'], $targetLanguage = 'en', $fileNamePattern = 'messages.%locale%.yml'): string { $yamlContent = ''; foreach ($messages as $key => $value) { $yamlContent .= "$key: $value\n"; } $yamlContent .= "\n"; $filename = \sprintf('%s/%s', $this->translationAppDir.'/translations', str_replace('%locale%', $targetLanguage, $fileNamePattern)); file_put_contents($filename, $yamlContent); $this->files[] = $filename; return $filename; } protected function createFile(array $messages = ['note' => 'NOTE'], $targetLanguage = 'en', $fileNamePattern = 'messages.%locale%.xlf', string $xlfVersion = 'xlf12'): string { if ('xlf12' === $xlfVersion) { $transUnits = ''; foreach ($messages as $key => $value) { $transUnits .= << $key $value XLIFF; } $xliffContent = << $transUnits XLIFF; } else { $units = ''; foreach ($messages as $key => $value) { $units .= << $key $value XLIFF; } $xliffContent = << $units XLIFF; } $filename = \sprintf('%s/%s', $this->translationAppDir.'/translations', str_replace('%locale%', $targetLanguage, $fileNamePattern)); file_put_contents($filename, $xliffContent); $this->files[] = $filename; return $filename; } } ================================================ FILE: Tests/Command/TranslationPullCommandTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Command; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Translation\Command\TranslationPullCommand; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Dumper\YamlFileDumper; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Translation\Loader\YamlFileLoader; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\Reader\TranslationReader; use Symfony\Component\Translation\TranslatorBag; use Symfony\Component\Translation\Writer\TranslationWriter; /** * @author Mathieu Santostefano */ class TranslationPullCommandTest extends TranslationProviderTestCase { private string|false $colSize; protected function setUp(): void { $this->colSize = getenv('COLUMNS'); putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); parent::setUp(); } protected function tearDown(): void { parent::tearDown(); putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } public function testPullNewXlf12Messages() { $arrayLoader = new ArrayLoader(); $filenameEn = $this->createFile(); $filenameEnIcu = $this->createFile(['say_hello' => 'Welcome, {firstname}!'], 'en', 'messages+intl-icu.%locale%.xlf'); $filenameFr = $this->createFile(['note' => 'NOTE'], 'fr'); $filenameFrIcu = $this->createFile(['say_hello' => 'Bonjour, {firstname}!'], 'fr', 'messages+intl-icu.%locale%.xlf'); $locales = ['en', 'fr']; $domains = ['messages', 'messages+intl-icu']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'say_hello' => 'Welcome, {firstname}!', ], 'en', 'messages+intl-icu')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'say_hello' => 'Bonjour, {firstname}!', ], 'fr', 'messages+intl-icu')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages', 'messages+intl-icu']]); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages, messages+intl-icu"', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<<
new.foo newFoo note NOTE
XLIFF, file_get_contents($filenameEn) ); $this->assertXmlStringEqualsXmlString(<<
say_hello Welcome, {firstname}!
XLIFF, file_get_contents($filenameEnIcu) ); $this->assertXmlStringEqualsXmlString(<<
new.foo nouveauFoo note NOTE
XLIFF, file_get_contents($filenameFr) ); $this->assertXmlStringEqualsXmlString(<<
say_hello Bonjour, {firstname}!
XLIFF, file_get_contents($filenameFrIcu) ); } public function testPullNewXlf20Messages() { $arrayLoader = new ArrayLoader(); $filenameEn = $this->createFile(['note' => 'NOTE'], 'en', 'messages.%locale%.xlf', 'xlf20'); $filenameFr = $this->createFile(['note' => 'NOTE'], 'fr', 'messages.%locale%.xlf', 'xlf20'); $locales = ['en', 'fr']; $domains = ['messages']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--format' => 'xlf20']); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<< new.foo newFoo note NOTE XLIFF, file_get_contents($filenameEn) ); $this->assertXmlStringEqualsXmlString(<< new.foo nouveauFoo note NOTE XLIFF, file_get_contents($filenameFr) ); } public function testPullNewYamlMessagesAsInlined() { $arrayLoader = new ArrayLoader(); $filenameEn = $this->createYamlFile(['note' => 'NOTE'], 'en', 'messages.%locale%.yml'); $filenameFr = $this->createYamlFile(['note' => 'NOTE'], 'fr', 'messages.%locale%.yml'); $locales = ['en', 'fr']; $domains = ['messages']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--format' => 'yml']); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertEquals(<<assertEquals(<<createYamlFile(['note' => 'NOTE'], 'en', 'messages.%locale%.yml'); $filenameFr = $this->createYamlFile(['note' => 'NOTE'], 'fr', 'messages.%locale%.yml'); $locales = ['en', 'fr']; $domains = ['messages']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--format' => 'yml', '--as-tree' => 10]); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertEquals(<<assertEquals(<<createFile(['note' => 'NOTE'], 'en'); $filenameMessagesFr = $this->createFile(['note' => 'NOTE'], 'fr'); $filenameValidatorsEn = $this->createFile(['foo.error' => 'Wrong value'], 'en', 'validators.%locale%.xlf'); $filenameValidatorsFr = $this->createFile(['foo.error' => 'Valeur erronée'], 'fr', 'validators.%locale%.xlf'); $locales = ['en', 'fr']; $domains = ['messages', 'validators']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'UPDATED NOTE', 'new.foo' => 'newFoo', ], 'en', 'messages')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE MISE À JOUR', 'new.foo' => 'nouveauFoo', ], 'fr', 'messages')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'foo.error' => 'Bad value', 'bar.error' => 'Bar error', ], 'en', 'validators')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'foo.error' => 'Valeur invalide', 'bar.error' => 'Bar erreur', ], 'fr', 'validators')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => $locales, '--domains' => $domains, '--force' => true]); $this->assertStringContainsString('[OK] Local translations has been updated from "null" (for "en, fr" locale(s), and "messages, validators" domain(s)).', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<<
note UPDATED NOTE new.foo newFoo
XLIFF, file_get_contents($filenameMessagesEn) ); $this->assertXmlStringEqualsXmlString(<<
note NOTE MISE À JOUR new.foo nouveauFoo
XLIFF, file_get_contents($filenameMessagesFr) ); $this->assertXmlStringEqualsXmlString(<<
foo.error Bad value bar.error Bar error
XLIFF, file_get_contents($filenameValidatorsEn) ); $this->assertXmlStringEqualsXmlString(<<
foo.error Valeur invalide bar.error Bar erreur
XLIFF, file_get_contents($filenameValidatorsFr) ); } #[RequiresPhpExtension('intl')] public function testPullForceIntlIcuMessages() { $arrayLoader = new ArrayLoader(); $filenameEn = $this->createFile(['note' => 'NOTE'], 'en', 'messages+intl-icu.%locale%.xlf'); $filenameFr = $this->createFile(['note' => 'NOTE'], 'fr', 'messages+intl-icu.%locale%.xlf'); $locales = ['en', 'fr']; $domains = ['messages']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'UPDATED NOTE', 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE MISE À JOUR', 'new.foo' => 'nouveauFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true, '--intl-icu' => true]); $this->assertStringContainsString('[OK] Local translations has been updated from "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<<
note UPDATED NOTE new.foo newFoo
XLIFF, file_get_contents($filenameEn) ); $this->assertXmlStringEqualsXmlString(<<
note NOTE MISE À JOUR new.foo nouveauFoo
XLIFF, file_get_contents($filenameFr) ); } public function testPullMessagesWithDefaultLocale() { $arrayLoader = new ArrayLoader(); $filenameFr = $this->createFile(['note' => 'NOTE'], 'fr'); $filenameEn = $this->createFile(['note' => 'NOTE']); $locales = ['en', 'fr']; $domains = ['messages']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ], 'en')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains, 'fr'); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages']]); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<<
new.foo newFoo note NOTE
XLIFF, file_get_contents($filenameEn) ); $this->assertXmlStringEqualsXmlString(<<
new.foo nouveauFoo note NOTE
XLIFF, file_get_contents($filenameFr) ); } public function testPullMessagesMultipleDomains() { $arrayLoader = new ArrayLoader(); $filenameMessages = $this->createFile(['note' => 'NOTE']); $filenameDomain = $this->createFile(['note' => 'NOTE'], 'en', 'domain.%locale%.xlf'); $locales = ['en']; $domains = ['messages', 'domain']; $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'new.foo' => 'newFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'new.foo' => 'newFoo', ], 'en', 'domain' )); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains, 'en'); $tester->execute(['--locales' => ['en'], '--domains' => ['messages', 'domain']]); $this->assertStringContainsString('[OK] New translations from "null" has been written locally (for "en" locale(s), and "messages, domain" domain(s)).', trim($tester->getDisplay())); $this->assertXmlStringEqualsXmlString(<<
new.foo newFoo note NOTE
XLIFF, file_get_contents($filenameMessages) ); $this->assertXmlStringEqualsXmlString(<<
new.foo newFoo note NOTE
XLIFF, file_get_contents($filenameDomain) ); } #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $application = new Application(); $application->addCommand($this->createCommand($this->createStub(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], 'en', ['loco', 'crowdin', 'lokalise'])); $tester = new CommandCompletionTester($application->get('translation:pull')); $suggestions = $tester->complete($input); $this->assertSame($expectedSuggestions, $suggestions); } public static function provideCompletionSuggestions(): \Generator { yield 'provider' => [ [''], ['loco', 'crowdin', 'lokalise'], ]; yield '--domains' => [ ['loco', '--domains'], ['messages', 'validators'], ]; yield '--locales' => [ ['loco', '--locales'], ['en', 'fr', 'it'], ]; } private function createCommandTester(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages'], $defaultLocale = 'en'): CommandTester { $command = $this->createCommand($provider, $locales, $domains, $defaultLocale); $application = new Application(); $application->addCommand($command); return new CommandTester($application->find('translation:pull')); } private function createCommand(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages'], $defaultLocale = 'en', array $providerNames = ['loco']): TranslationPullCommand { $writer = new TranslationWriter(); $writer->addDumper('xlf', new XliffFileDumper()); $writer->addDumper('yml', new YamlFileDumper()); $reader = new TranslationReader(); $reader->addLoader('xlf', new XliffFileLoader()); $reader->addLoader('yml', new YamlFileLoader()); return new TranslationPullCommand( $this->getProviderCollection($provider, $providerNames, $locales, $domains), $writer, $reader, $defaultLocale, [$this->translationAppDir.'/translations'], $locales ); } } ================================================ FILE: Tests/Command/TranslationPushCommandTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Command; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Console\Application; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Translation\Command\TranslationPushCommand; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Translation\Provider\FilteringProvider; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\Provider\TranslationProviderCollection; use Symfony\Component\Translation\Reader\TranslationReader; use Symfony\Component\Translation\TranslatorBag; /** * @author Mathieu Santostefano */ class TranslationPushCommandTest extends TranslationProviderTestCase { private string|false $colSize; protected function setUp(): void { $this->colSize = getenv('COLUMNS'); putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); parent::setUp(); } protected function tearDown(): void { parent::tearDown(); putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } public function testPushNewMessages() { $arrayLoader = new ArrayLoader(); $xliffLoader = new XliffFileLoader(); $locales = ['en', 'fr']; $domains = ['messages']; // Simulate existing messages on Provider $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); // Create local files, with a new message $filenameEn = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ]); $filenameFr = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr'); $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameEn, 'en')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameFr, 'fr')); $provider->expects($this->once()) ->method('write') ->with($localTranslatorBag->diff($providerReadTranslatorBag)); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages']]); $this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); } public function testPushNewIntlIcuMessages() { $arrayLoader = new ArrayLoader(); $xliffLoader = new XliffFileLoader(); $locales = ['en', 'fr']; $domains = ['messages']; // Simulate existing messages on Provider $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); // Create local files, with a new message $filenameEn = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'newFooIntlIcu', ], 'en', 'messages+intl-icu.%locale%.xlf'); $filenameFr = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'nouveauFooIntlIcu', ], 'fr', 'messages+intl-icu.%locale%.xlf'); $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameEn, 'en')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameFr, 'fr')); $provider->expects($this->once()) ->method('write') ->with($localTranslatorBag->diff($providerReadTranslatorBag)); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages']]); $this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); } public function testPushForceMessages() { $xliffLoader = new XliffFileLoader(); $filenameMessagesEn = $this->createFile([ 'note' => 'NOTE UPDATED', 'new.foo' => 'newFoo', ], 'en'); $filenameMessagesFr = $this->createFile([ 'note' => 'NOTE MISE À JOUR', 'new.foo' => 'nouveauFoo', ], 'fr'); $filenameValidatorsEn = $this->createFile([ 'foo.error' => 'Wrong value', 'bar.success' => 'Form valid!', ], 'en', 'validators.%locale%.xlf'); $filenameValidatorsFr = $this->createFile([ 'foo.error' => 'Valeur erronée', 'bar.success' => 'Formulaire valide !', ], 'fr', 'validators.%locale%.xlf'); $locales = ['en', 'fr']; $domains = ['messages', 'validators']; $provider = $this->createMock(ProviderInterface::class); $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameMessagesEn, 'en', 'messages')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameMessagesFr, 'fr', 'messages')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameValidatorsEn, 'en', 'validators')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameValidatorsFr, 'fr', 'validators')); $provider->expects($this->once()) ->method('write') ->with($localTranslatorBag); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => $locales, '--domains' => $domains, '--force' => true]); $this->assertStringContainsString('[OK] All local translations has been sent to "null" (for "en, fr" locale(s), and "messages, validators" domain(s)).', trim($tester->getDisplay())); } public function testDeleteMissingMessages() { $xliffLoader = new XliffFileLoader(); $arrayLoader = new ArrayLoader(); $locales = ['en', 'fr']; $domains = ['messages']; // Simulate existing messages on Provider. $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'obsolete.foo' => 'obsoleteFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'obsolete.foo' => 'obsolèteFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider ->method('read') ->willReturnMap([ [$domains, $locales, $providerReadTranslatorBag], ]); // Create local bag, with a missing message. $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(), 'en')); $localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE'], 'fr'), 'fr')); $missingTranslatorBag = $providerReadTranslatorBag->diff($localTranslatorBag); $provider->expects($this->once()) ->method('delete') ->with($missingTranslatorBag); // Read provider translations again, after missing translations deletion, // to avoid push freshly deleted translations. $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr')); $provider ->method('read') ->willReturnMap([ [$domains, $locales, $providerReadTranslatorBag], ]); $provider->expects($this->once()) ->method('write') ->with($localTranslatorBag->diff($providerReadTranslatorBag)); $provider->expects($this->exactly(2)) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--delete-missing' => true]); $this->assertStringContainsString('[OK] Missing translations on "null" has been deleted (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); } public function testPushForceAndDeleteMissingMessages() { $xliffLoader = new XliffFileLoader(); $arrayLoader = new ArrayLoader(); $locales = ['en', 'fr']; $domains = ['messages']; // Simulate existing messages on Provider. $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'obsolete.foo' => 'obsoleteFoo', ], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load([ 'note' => 'NOTE', 'obsolete.foo' => 'obsolèteFoo', ], 'fr')); $provider = $this->createMock(ProviderInterface::class); $provider ->method('read') ->willReturnMap([ [$domains, $locales, $providerReadTranslatorBag], ]); // Create local bag, with a missing message, an updated one and a new one. $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE UPDATED', 'note2' => 'NOTE 2']), 'en')); $localTranslatorBag->addCatalogue($xliffLoader->load($this->createFile(['note' => 'NOTE MISE À JOUR', 'note2' => 'NOTE 2'], 'fr'), 'fr')); $missingTranslatorBag = $providerReadTranslatorBag->diff($localTranslatorBag); $provider->expects($this->once()) ->method('delete') ->with($missingTranslatorBag); // Read provider translations again, after missing translations deletion, // to avoid push freshly deleted translations. $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr')); $provider ->method('read') ->willReturnMap([ [$domains, $locales, $providerReadTranslatorBag], ]); $translationBagToWrite = $localTranslatorBag->diff($providerReadTranslatorBag); $translationBagToWrite->addBag($localTranslatorBag->intersect($providerReadTranslatorBag)); $provider->expects($this->once()) ->method('write') ->with($translationBagToWrite); $provider->expects($this->exactly(2)) ->method('__toString') ->willReturn('null://default'); $tester = $this->createCommandTester($provider, $locales, $domains); $tester->execute(['--locales' => ['en', 'fr'], '--domains' => ['messages'], '--force' => true, '--delete-missing' => true]); $this->assertStringContainsString('[OK] Missing translations on "null" has been deleted (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); $this->assertStringContainsString('[OK] All local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); } public function testPushWithProviderDomains() { $arrayLoader = new ArrayLoader(); $xliffLoader = new XliffFileLoader(); $locales = ['en', 'fr']; $domains = ['messages']; // Simulate existing messages on Provider $providerReadTranslatorBag = new TranslatorBag(); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'en')); $providerReadTranslatorBag->addCatalogue($arrayLoader->load(['note' => 'NOTE'], 'fr')); $provider = $this->createMock(FilteringProvider::class); $provider->expects($this->once()) ->method('read') ->with($domains, $locales) ->willReturn($providerReadTranslatorBag); $provider->expects($this->once()) ->method('getDomains') ->willReturn(['messages']); $filenameEn = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'newFoo', ]); $filenameFr = $this->createFile([ 'note' => 'NOTE', 'new.foo' => 'nouveauFoo', ], 'fr'); $localTranslatorBag = new TranslatorBag(); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameEn, 'en')); $localTranslatorBag->addCatalogue($xliffLoader->load($filenameFr, 'fr')); $provider->expects($this->once()) ->method('write') ->with($localTranslatorBag->diff($providerReadTranslatorBag)); $provider->expects($this->once()) ->method('__toString') ->willReturn('null://default'); $reader = new TranslationReader(); $reader->addLoader('xlf', new XliffFileLoader()); $command = new TranslationPushCommand( new TranslationProviderCollection([ 'loco' => $provider, ]), $reader, [$this->translationAppDir.'/translations'], $locales ); $application = new Application(); $application->addCommand($command); $tester = new CommandTester($application->find('translation:push')); $tester->execute(['--locales' => ['en', 'fr']]); $this->assertStringContainsString('[OK] New local translations has been sent to "null" (for "en, fr" locale(s), and "messages" domain(s)).', trim($tester->getDisplay())); } #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $application = new Application(); $application->addCommand($this->createCommand($this->createStub(ProviderInterface::class), ['en', 'fr', 'it'], ['messages', 'validators'], ['loco', 'crowdin', 'lokalise'])); $tester = new CommandCompletionTester($application->get('translation:push')); $suggestions = $tester->complete($input); $this->assertSame($expectedSuggestions, $suggestions); } public static function provideCompletionSuggestions(): \Generator { yield 'provider' => [ [''], ['loco', 'crowdin', 'lokalise'], ]; yield '--domains' => [ ['loco', '--domains'], ['messages', 'validators'], ]; yield '--locales' => [ ['loco', '--locales'], ['en', 'fr', 'it'], ]; } private function createCommandTester(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages']): CommandTester { $command = $this->createCommand($provider, $locales, $domains); $application = new Application(); $application->addCommand($command); return new CommandTester($application->find('translation:push')); } private function createCommand(ProviderInterface $provider, array $locales = ['en'], array $domains = ['messages'], array $providerNames = ['loco']): TranslationPushCommand { $reader = new TranslationReader(); $reader->addLoader('xlf', new XliffFileLoader()); return new TranslationPushCommand( $this->getProviderCollection($provider, $providerNames, $locales, $domains), $reader, [$this->translationAppDir.'/translations'], $locales ); } } ================================================ FILE: Tests/Command/XliffLintCommandTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Command; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandCompletionTester; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Translation\Command\XliffLintCommand; /** * Tests the XliffLintCommand. * * @author Javier Eguiluz */ class XliffLintCommandTest extends TestCase { private array $files; public function testLintCorrectFile() { $tester = $this->createCommandTester(); $filename = $this->createFile(); $tester->execute( ['filename' => $filename], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('OK', trim($tester->getDisplay())); } public function testLintCorrectFiles() { $tester = $this->createCommandTester(); $filename1 = $this->createFile(); $filename2 = $this->createFile(); $tester->execute( ['filename' => [$filename1, $filename2]], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); $tester->assertCommandIsSuccessful('Returns 0 in case of success'); $this->assertStringContainsString('OK', trim($tester->getDisplay())); } #[DataProvider('provideStrictFilenames')] public function testStrictFilenames($requireStrictFileNames, $fileNamePattern, $targetLanguage, $mustFail) { $tester = $this->createCommandTester($requireStrictFileNames); $filename = $this->createFile('note', $targetLanguage, $fileNamePattern); $tester->execute( ['filename' => $filename], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false] ); $this->assertEquals($mustFail ? 1 : 0, $tester->getStatusCode()); $this->assertStringContainsString($mustFail ? '[WARNING] 0 XLIFF files have valid syntax and 1 contain errors.' : '[OK] All 1 XLIFF files contain valid syntax.', $tester->getDisplay()); } public function testLintIncorrectXmlSyntax() { $tester = $this->createCommandTester(); $filename = $this->createFile('note '); $tester->execute(['filename' => $filename], ['decorated' => false]); $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); $this->assertStringContainsString('Opening and ending tag mismatch: target line 6 and source', trim($tester->getDisplay())); } public function testLintIncorrectTargetLanguage() { $tester = $this->createCommandTester(); $filename = $this->createFile('note', 'es'); $tester->execute(['filename' => $filename], ['decorated' => false]); $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); $this->assertStringContainsString('There is a mismatch between the language included in the file name ("messages.en.xlf") and the "es" value used in the "target-language" attribute of the file.', trim($tester->getDisplay())); } public function testLintTargetLanguageIsCaseInsensitive() { $tester = $this->createCommandTester(); $filename = $this->createFile('note', 'zh-cn', 'messages.zh_CN.xlf'); $tester->execute(['filename' => $filename], ['decorated' => false]); $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('[OK] All 1 XLIFF files contain valid syntax.', trim($tester->getDisplay())); } public function testLintSucceedsWhenLocaleInFileAndInTargetLanguageNameUsesDashesInsteadOfUnderscores() { $tester = $this->createCommandTester(); $filename = $this->createFile('note', 'en-GB', 'messages.en-GB.xlf'); $tester->execute(['filename' => $filename], ['decorated' => false]); $tester->assertCommandIsSuccessful(); $this->assertStringContainsString('[OK] All 1 XLIFF files contain valid syntax.', trim($tester->getDisplay())); } public function testLintFileNotReadable() { $tester = $this->createCommandTester(); $filename = $this->createFile(); unlink($filename); $this->expectException(\RuntimeException::class); $tester->execute(['filename' => $filename], ['decorated' => false]); } public function testGetHelp() { $command = new XliffLintCommand(); $expected = <<php %command.full_name% dirname The --format option specifies the format of the command output: php %command.full_name% dirname --format=json EOF; $this->assertStringContainsString($expected, $command->getHelp()); } public function testLintIncorrectFileWithGithubFormat() { $filename = $this->createFile('note '); $tester = $this->createCommandTester(); $tester->execute(['filename' => [$filename], '--format' => 'github'], ['decorated' => false]); self::assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); self::assertStringMatchesFormat('%A::error file=%s,line=6,col=47::Opening and ending tag mismatch: target line 6 and source%A', trim($tester->getDisplay())); } public function testLintAutodetectsGithubActionEnvironment() { $prev = getenv('GITHUB_ACTIONS'); putenv('GITHUB_ACTIONS'); try { putenv('GITHUB_ACTIONS=1'); $filename = $this->createFile('note '); $tester = $this->createCommandTester(); $tester->execute(['filename' => [$filename]], ['decorated' => false]); self::assertStringMatchesFormat('%A::error file=%s,line=6,col=47::Opening and ending tag mismatch: target line 6 and source%A', trim($tester->getDisplay())); } finally { putenv('GITHUB_ACTIONS'.($prev ? "=$prev" : '')); } } public function testPassingClosureAndCallableToConstructor() { $command = new XliffLintCommand('translation:xliff:lint', $this->testPassingClosureAndCallableToConstructor(...), [$this, 'testPassingClosureAndCallableToConstructor'] ); self::assertInstanceOf(XliffLintCommand::class, $command); } private function createFile($sourceContent = 'note', $targetLanguage = 'en', $fileNamePattern = 'messages.%locale%.xlf'): string { $xliffContent = << $sourceContent NOTE XLIFF; $filename = \sprintf('%s/translation-xliff-lint-test/%s', sys_get_temp_dir(), str_replace('%locale%', 'en', $fileNamePattern)); file_put_contents($filename, $xliffContent); $this->files[] = $filename; return $filename; } private function createCommand($requireStrictFileNames = true, $application = null): Command { if (!$application) { $application = new Application(); $application->addCommand(new XliffLintCommand(null, null, null, $requireStrictFileNames)); } $command = $application->find('lint:xliff'); $command->setApplication($application); return $command; } private function createCommandTester($requireStrictFileNames = true, $application = null): CommandTester { return new CommandTester($this->createCommand($requireStrictFileNames, $application)); } protected function setUp(): void { $this->files = []; @mkdir(sys_get_temp_dir().'/translation-xliff-lint-test'); } protected function tearDown(): void { foreach ($this->files as $file) { if (file_exists($file)) { @unlink($file); } } @rmdir(sys_get_temp_dir().'/translation-xliff-lint-test'); } public static function provideStrictFilenames() { yield [false, 'messages.%locale%.xlf', 'en', false]; yield [false, 'messages.%locale%.xlf', 'es', true]; yield [false, '%locale%.messages.xlf', 'en', false]; yield [false, '%locale%.messages.xlf', 'es', true]; yield [true, 'messages.%locale%.xlf', 'en', false]; yield [true, 'messages.%locale%.xlf', 'es', true]; yield [true, '%locale%.messages.xlf', 'en', true]; yield [true, '%locale%.messages.xlf', 'es', true]; } #[DataProvider('provideCompletionSuggestions')] public function testComplete(array $input, array $expectedSuggestions) { $tester = new CommandCompletionTester($this->createCommand()); $this->assertSame($expectedSuggestions, $tester->complete($input)); } public static function provideCompletionSuggestions() { yield 'option' => [['--format', ''], ['txt', 'json', 'github']]; } } ================================================ FILE: Tests/DataCollector/TranslationDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Translator; class TranslationDataCollectorTest extends TestCase { public function testCollectEmptyMessages() { $dataCollector = new TranslationDataCollector(new DataCollectorTranslator(new Translator('en'))); $dataCollector->lateCollect(); $this->assertEquals(0, $dataCollector->getCountMissings()); $this->assertEquals(0, $dataCollector->getCountFallbacks()); $this->assertEquals(0, $dataCollector->getCountDefines()); $this->assertEquals([], $dataCollector->getMessages()->getValue()); } public function testCollect() { $expectedMessages = [ [ 'id' => 'foo', 'translation' => 'foo (en)', 'locale' => 'en', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_DEFINED, 'count' => 1, 'parameters' => [], 'transChoiceNumber' => null, 'fallbackLocale' => null, ], [ 'id' => 'bar', 'translation' => 'bar (fr)', 'locale' => 'en', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, 'count' => 1, 'parameters' => [], 'transChoiceNumber' => null, 'fallbackLocale' => 'fr', ], [ 'id' => 'choice', 'translation' => 'choice', 'locale' => 'en', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_MISSING, 'count' => 3, 'parameters' => [ ['%count%' => 3], ['%count%' => 3], ['%count%' => 4, '%foo%' => 'bar'], ], 'transChoiceNumber' => 3, 'fallbackLocale' => null, ], ]; $translator = new Translator('en'); $translator->setFallbackLocales(['fr']); $translator->addLoader('memory', new ArrayLoader()); $translator->addResource('memory', ['foo' => 'foo (en)'], 'en'); $translator->addResource('memory', ['bar' => 'bar (fr)'], 'fr'); $dataCollectorTranslator = new DataCollectorTranslator($translator); $dataCollectorTranslator->trans('foo'); $dataCollectorTranslator->trans('bar'); $dataCollectorTranslator->trans('choice', ['%count%' => 3]); $dataCollectorTranslator->trans('choice', ['%count%' => 3]); $dataCollectorTranslator->trans('choice', ['%count%' => 4, '%foo%' => 'bar']); $dataCollector = new TranslationDataCollector($dataCollectorTranslator); $dataCollector->lateCollect(); $this->assertEquals(1, $dataCollector->getCountMissings()); $this->assertEquals(1, $dataCollector->getCountFallbacks()); $this->assertEquals(1, $dataCollector->getCountDefines()); $this->assertEquals($expectedMessages, array_values($dataCollector->getMessages()->getValue(true))); } public function testCollectAndReset() { $translator = new Translator('fr'); $translator->setFallbackLocales(['en']); $translator->addGlobalParameter('welcome', 'Welcome {name}!'); $dataCollector = new TranslationDataCollector(new DataCollectorTranslator($translator)); $dataCollector->collect(new Request(), new Response()); $this->assertSame('fr', $dataCollector->getLocale()); $this->assertSame(['en'], $dataCollector->getFallbackLocales()); $this->assertSame(['welcome' => 'Welcome {name}!'], $dataCollector->getGlobalParameters()); $dataCollector->reset(); $this->assertNull($dataCollector->getLocale()); $this->assertSame([], $dataCollector->getFallbackLocales()); $this->assertSame([], $dataCollector->getGlobalParameters()); } } ================================================ FILE: Tests/DataCollectorTranslatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Translation\Translator; class DataCollectorTranslatorTest extends TestCase { public function testCollectMessages() { $collector = $this->createCollector(); $collector->setFallbackLocales(['fr', 'ru']); $collector->trans('foo'); $collector->trans('bar'); $collector->trans('choice', ['%count%' => 0]); $collector->trans('bar_ru'); $collector->trans('bar_ru', ['foo' => 'bar']); $expectedMessages = []; $expectedMessages[] = [ 'id' => 'foo', 'translation' => 'foo (en)', 'locale' => 'en', 'fallbackLocale' => null, 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_DEFINED, 'parameters' => [], 'transChoiceNumber' => null, ]; $expectedMessages[] = [ 'id' => 'bar', 'translation' => 'bar (fr)', 'locale' => 'en', 'fallbackLocale' => 'fr', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, 'parameters' => [], 'transChoiceNumber' => null, ]; $expectedMessages[] = [ 'id' => 'choice', 'translation' => 'choice', 'locale' => 'en', 'fallbackLocale' => null, 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_MISSING, 'parameters' => ['%count%' => 0], 'transChoiceNumber' => 0, ]; $expectedMessages[] = [ 'id' => 'bar_ru', 'translation' => 'bar (ru)', 'locale' => 'en', 'fallbackLocale' => 'ru', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, 'parameters' => [], 'transChoiceNumber' => null, ]; $expectedMessages[] = [ 'id' => 'bar_ru', 'translation' => 'bar (ru)', 'locale' => 'en', 'fallbackLocale' => 'ru', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK, 'parameters' => ['foo' => 'bar'], 'transChoiceNumber' => null, ]; $this->assertEquals($expectedMessages, $collector->getCollectedMessages()); } public function testGetGlobalParameters() { $translatable = new TranslatableMessage('url.front'); $translator = new Translator('en'); $translator->addGlobalParameter('app', 'My app'); $translator->addGlobalParameter('url', $translatable); $collector = new DataCollectorTranslator($translator); $this->assertEquals(['app' => 'My app', 'url' => $translatable], $collector->getGlobalParameters()); } private function createCollector() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (en)'], 'en'); $translator->addResource('array', ['bar' => 'bar (fr)'], 'fr'); $translator->addResource('array', ['bar_ru' => 'bar (ru)'], 'ru'); return new DataCollectorTranslator($translator); } } ================================================ FILE: Tests/DependencyInjection/DataCollectorTranslatorPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DataCollector\TranslationDataCollector; use Symfony\Component\Translation\DataCollectorTranslator; use Symfony\Component\Translation\DependencyInjection\DataCollectorTranslatorPass; use Symfony\Component\Translation\Translator; use Symfony\Contracts\Translation\TranslatorInterface; class DataCollectorTranslatorPassTest extends TestCase { private ContainerBuilder $container; private DataCollectorTranslatorPass $dataCollectorTranslatorPass; protected function setUp(): void { $this->container = new ContainerBuilder(); $this->dataCollectorTranslatorPass = new DataCollectorTranslatorPass(); $this->container->setParameter('translator_implementing_bag', Translator::class); $this->container->setParameter('translator_not_implementing_bag', 'Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TranslatorWithoutTranslatorBag'); $this->container->register('translator.data_collector', DataCollectorTranslator::class) ->setDecoratedService('translator') ->setArguments([new Reference('translator.data_collector.inner')]) ; $this->container->register('data_collector.translation', TranslationDataCollector::class) ->setArguments([new Reference('translator.data_collector')]) ; } #[DataProvider('getImplementingTranslatorBagInterfaceTranslatorClassNames')] public function testProcessKeepsDataCollectorTranslatorIfItImplementsTranslatorBagInterface($class) { $this->container->register('translator', $class); $this->dataCollectorTranslatorPass->process($this->container); $this->assertTrue($this->container->hasDefinition('translator.data_collector')); } #[DataProvider('getImplementingTranslatorBagInterfaceTranslatorClassNames')] public function testProcessKeepsDataCollectorIfTranslatorImplementsTranslatorBagInterface($class) { $this->container->register('translator', $class); $this->dataCollectorTranslatorPass->process($this->container); $this->assertTrue($this->container->hasDefinition('data_collector.translation')); } public static function getImplementingTranslatorBagInterfaceTranslatorClassNames() { return [ [Translator::class], ['%translator_implementing_bag%'], ]; } #[DataProvider('getNotImplementingTranslatorBagInterfaceTranslatorClassNames')] public function testProcessRemovesDataCollectorTranslatorIfItDoesNotImplementTranslatorBagInterface($class) { $this->container->register('translator', $class); $this->dataCollectorTranslatorPass->process($this->container); $this->assertFalse($this->container->hasDefinition('translator.data_collector')); } #[DataProvider('getNotImplementingTranslatorBagInterfaceTranslatorClassNames')] public function testProcessRemovesDataCollectorIfTranslatorDoesNotImplementTranslatorBagInterface($class) { $this->container->register('translator', $class); $this->dataCollectorTranslatorPass->process($this->container); $this->assertFalse($this->container->hasDefinition('data_collector.translation')); } public static function getNotImplementingTranslatorBagInterfaceTranslatorClassNames() { return [ ['Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\TranslatorWithoutTranslatorBag'], ['%translator_not_implementing_bag%'], ]; } } class TranslatorWithoutTranslatorBag implements TranslatorInterface { public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { } public function getLocale(): string { return 'en'; } } ================================================ FILE: Tests/DependencyInjection/Fixtures/ControllerArguments.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection\Fixtures; use Symfony\Contracts\Translation\TranslatorInterface; class ControllerArguments { public function __invoke(TranslatorInterface $translator) { } public function index(TranslatorInterface $translator) { } } ================================================ FILE: Tests/DependencyInjection/Fixtures/ServiceArguments.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection\Fixtures; use Symfony\Contracts\Translation\TranslatorInterface; class ServiceArguments { public function __construct(TranslatorInterface $translator) { } } ================================================ FILE: Tests/DependencyInjection/Fixtures/ServiceMethodCalls.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection\Fixtures; use Symfony\Contracts\Translation\TranslatorInterface; class ServiceMethodCalls { public function setTranslator(TranslatorInterface $translator) { } } ================================================ FILE: Tests/DependencyInjection/Fixtures/ServiceProperties.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection\Fixtures; class ServiceProperties { public $translator; } ================================================ FILE: Tests/DependencyInjection/Fixtures/ServiceSubscriber.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection\Fixtures; use Psr\Container\ContainerInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Translation\TranslatorInterface; class ServiceSubscriber implements ServiceSubscriberInterface { public function __construct(ContainerInterface $container) { } public static function getSubscribedServices(): array { return ['translator' => TranslatorInterface::class]; } } ================================================ FILE: Tests/DependencyInjection/LoggingTranslatorPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DependencyInjection\LoggingTranslatorPass; use Symfony\Component\Translation\Translator; class LoggingTranslatorPassTest extends TestCase { public function testProcess() { $container = new ContainerBuilder(); $container->setParameter('translator.logging', true); $container->setParameter('translator.class', Translator::class); $container->register('monolog.logger'); $container->setAlias('logger', 'monolog.logger'); $container->register('translator.default', '%translator.class%'); $container->register('translator.logging', '%translator.class%'); $container->setAlias('translator', 'translator.default'); $translationWarmerDefinition = $container->register('translation.warmer') ->addArgument(new Reference('translator')) ->addTag('container.service_subscriber', ['id' => 'translator']) ->addTag('container.service_subscriber', ['id' => 'foo']); $pass = new LoggingTranslatorPass(); $pass->process($container); $this->assertEquals( ['container.service_subscriber' => [ ['id' => 'foo'], ['key' => 'translator', 'id' => 'translator.logging.inner'], ]], $translationWarmerDefinition->getTags() ); } public function testThatCompilerPassIsIgnoredIfThereIsNotLoggerDefinition() { $container = new ContainerBuilder(); $container->register('identity_translator'); $container->setAlias('translator', 'identity_translator'); $definitionsBefore = \count($container->getDefinitions()); $aliasesBefore = \count($container->getAliases()); $pass = new LoggingTranslatorPass(); $pass->process($container); // the container is untouched (i.e. no new definitions or aliases) $this->assertCount($definitionsBefore, $container->getDefinitions()); $this->assertCount($aliasesBefore, $container->getAliases()); } public function testThatCompilerPassIsIgnoredIfThereIsNotTranslatorDefinition() { $container = new ContainerBuilder(); $container->register('monolog.logger'); $container->setAlias('logger', 'monolog.logger'); $definitionsBefore = \count($container->getDefinitions()); $aliasesBefore = \count($container->getAliases()); $pass = new LoggingTranslatorPass(); $pass->process($container); // the container is untouched (i.e. no new definitions or aliases) $this->assertCount($definitionsBefore, $container->getDefinitions()); $this->assertCount($aliasesBefore, $container->getAliases()); } } ================================================ FILE: Tests/DependencyInjection/TranslationDumperPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass; class TranslationDumperPassTest extends TestCase { public function testProcess() { $container = new ContainerBuilder(); $writerDefinition = $container->register('translation.writer'); $container->register('foo.id') ->addTag('translation.dumper', ['alias' => 'bar.alias']); $translationDumperPass = new TranslationDumperPass(); $translationDumperPass->process($container); $this->assertEquals([['addDumper', ['bar.alias', new Reference('foo.id')]]], $writerDefinition->getMethodCalls()); } public function testProcessNoDefinitionFound() { $container = new ContainerBuilder(); $definitionsBefore = \count($container->getDefinitions()); $aliasesBefore = \count($container->getAliases()); $translationDumperPass = new TranslationDumperPass(); $translationDumperPass->process($container); // the container is untouched (i.e. no new definitions or aliases) $this->assertCount($definitionsBefore, $container->getDefinitions()); $this->assertCount($aliasesBefore, $container->getAliases()); } } ================================================ FILE: Tests/DependencyInjection/TranslationExtractorPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass; class TranslationExtractorPassTest extends TestCase { public function testProcess() { $container = new ContainerBuilder(); $extractorDefinition = $container->register('translation.extractor'); $container->register('foo.id') ->addTag('translation.extractor', ['alias' => 'bar.alias']); $translationDumperPass = new TranslationExtractorPass(); $translationDumperPass->process($container); $this->assertEquals([['addExtractor', ['bar.alias', new Reference('foo.id')]]], $extractorDefinition->getMethodCalls()); } public function testProcessNoDefinitionFound() { $container = new ContainerBuilder(); $definitionsBefore = \count($container->getDefinitions()); $aliasesBefore = \count($container->getAliases()); $translationDumperPass = new TranslationExtractorPass(); $translationDumperPass->process($container); // the container is untouched (i.e. no new definitions or aliases) $this->assertCount($definitionsBefore, $container->getDefinitions()); $this->assertCount($aliasesBefore, $container->getAliases()); } public function testProcessMissingAlias() { $container = new ContainerBuilder(); $extractorDefinition = $container->register('translation.extractor'); $container->register('foo.id') ->addTag('translation.extractor', []); $translationDumperPass = new TranslationExtractorPass(); $translationDumperPass->process($container); $this->assertEquals([['addExtractor', ['foo.id', new Reference('foo.id')]]], $extractorDefinition->getMethodCalls()); } } ================================================ FILE: Tests/DependencyInjection/TranslationPathsPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass; use Symfony\Component\Translation\Tests\DependencyInjection\Fixtures\ControllerArguments; use Symfony\Component\Translation\Tests\DependencyInjection\Fixtures\ServiceArguments; use Symfony\Component\Translation\Tests\DependencyInjection\Fixtures\ServiceMethodCalls; use Symfony\Component\Translation\Tests\DependencyInjection\Fixtures\ServiceProperties; use Symfony\Component\Translation\Tests\DependencyInjection\Fixtures\ServiceSubscriber; class TranslationPathsPassTest extends TestCase { public function testProcess() { $container = new ContainerBuilder(); $container->register('translator'); $debugCommand = $container->register('console.command.translation_debug') ->setArguments([null, null, null, null, null, [], []]) ; $updateCommand = $container->register('console.command.translation_extract') ->setArguments([null, null, null, null, null, null, [], []]) ; $container->register(ControllerArguments::class, ControllerArguments::class) ->setTags(['controller.service_arguments']) ; $container->register(ServiceArguments::class, ServiceArguments::class) ->setArguments([new Reference('translator')]) ; $container->register(ServiceProperties::class, ServiceProperties::class) ->setProperties([new Reference('translator')]) ; $container->register(ServiceMethodCalls::class, ServiceMethodCalls::class) ->setMethodCalls([['setTranslator', [new Reference('translator')]]]) ; $container->register('service_rc') ->setArguments([new Definition(), new Reference(ServiceMethodCalls::class)]) ; $serviceLocator1 = $container->register('.service_locator.foo', ServiceLocator::class) ->setArguments([new ServiceClosureArgument(new Reference('translator'))]) ; $serviceLocator2 = (new Definition(ServiceLocator::class)) ->setArguments([ServiceSubscriber::class, new Reference('service_container')]) ->setFactory([$serviceLocator1, 'withContext']) ; $container->register('service_subscriber', ServiceSubscriber::class) ->setArguments([$serviceLocator2]) ; $container->register('.service_locator.bar', ServiceLocator::class) ->setArguments([[ ControllerArguments::class.'::index' => new ServiceClosureArgument(new Reference('.service_locator.foo')), ControllerArguments::class.'::__invoke' => new ServiceClosureArgument(new Reference('.service_locator.foo')), ControllerArguments::class => new ServiceClosureArgument(new Reference('.service_locator.foo')), ]]) ; $container->register('argument_resolver.service') ->setArguments([new Reference('.service_locator.bar')]) ; $pass = new TranslatorPathsPass(); $pass->process($container); $expectedPaths = [ $container->getReflectionClass(ServiceArguments::class)->getFileName(), $container->getReflectionClass(ServiceProperties::class)->getFileName(), $container->getReflectionClass(ServiceMethodCalls::class)->getFileName(), $container->getReflectionClass(ControllerArguments::class)->getFileName(), $container->getReflectionClass(ServiceSubscriber::class)->getFileName(), ]; $this->assertSame($expectedPaths, $debugCommand->getArgument(6)); $this->assertSame($expectedPaths, $updateCommand->getArgument(7)); } } ================================================ FILE: Tests/DependencyInjection/TranslatorPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor; use Symfony\Component\Validator\Constraints\IsbnValidator; use Symfony\Component\Validator\Constraints\LengthValidator; use Symfony\Component\Validator\Constraints\NotBlankValidator; use Symfony\Component\Validator\Constraints\TimeValidator; class TranslatorPassTest extends TestCase { public function testValidCollector() { $loader = (new Definition()) ->addTag('translation.loader', ['alias' => 'xliff', 'legacy-alias' => 'xlf']); $reader = new Definition(); $translator = (new Definition()) ->setArguments([null, null, null, null]); $container = new ContainerBuilder(); $container->setDefinition('translator.default', $translator); $container->setDefinition('translation.reader', $reader); $container->setDefinition('translation.xliff_loader', $loader); $pass = new TranslatorPass(); $pass->process($container); $expectedReader = (new Definition()) ->addMethodCall('addLoader', ['xliff', new Reference('translation.xliff_loader')]) ->addMethodCall('addLoader', ['xlf', new Reference('translation.xliff_loader')]) ; $this->assertEquals($expectedReader, $reader); $expectedLoader = (new Definition()) ->addTag('translation.loader', ['alias' => 'xliff', 'legacy-alias' => 'xlf']) ; $this->assertEquals($expectedLoader, $loader); $this->assertSame(['translation.xliff_loader' => ['xliff', 'xlf']], $translator->getArgument(3)); $expected = ['translation.xliff_loader' => new ServiceClosureArgument(new Reference('translation.xliff_loader'))]; $this->assertEquals($expected, $container->getDefinition((string) $translator->getArgument(0))->getArgument(0)); } public function testValidCommandsViewPathsArgument() { $container = new ContainerBuilder(); $container->register('translator.default') ->setArguments([null, null, null, null]) ; $debugCommand = $container->register('console.command.translation_debug') ->setArguments([null, null, null, null, null, [], []]) ; $updateCommand = $container->register('console.command.translation_extract') ->setArguments([null, null, null, null, null, null, [], []]) ; $container->register('twig.template_iterator') ->setArguments([null, ['other/templates' => null, 'tpl' => 'App']]) ; $container->setParameter('twig.default_path', 'templates'); $pass = new TranslatorPass(); $pass->process($container); $expectedViewPaths = ['other/templates', 'tpl']; $this->assertSame('templates', $debugCommand->getArgument(4)); $this->assertSame('templates', $updateCommand->getArgument(5)); $this->assertSame($expectedViewPaths, $debugCommand->getArgument(6)); $this->assertSame($expectedViewPaths, $updateCommand->getArgument(7)); } public function testCommandsViewPathsArgumentsAreIgnoredWithOldServiceDefinitions() { $container = new ContainerBuilder(); $container->register('translator.default') ->setArguments([null, null, null, null]) ; $debugCommand = $container->register('console.command.translation_debug') ->setArguments([ new Reference('translator'), new Reference('translation.reader'), new Reference('translation.extractor'), '%translator.default_path%', null, ]) ; $updateCommand = $container->register('console.command.translation_extract') ->setArguments([ new Reference('translation.writer'), new Reference('translation.reader'), new Reference('translation.extractor'), '%kernel.default_locale%', '%translator.default_path%', null, ]) ; $container->register('twig.template_iterator') ->setArguments([null, ['other/templates' => null, 'tpl' => 'App']]) ; $container->setParameter('twig.default_path', 'templates'); $pass = new TranslatorPass(); $pass->process($container); $this->assertSame('templates', $debugCommand->getArgument(4)); $this->assertSame('templates', $updateCommand->getArgument(5)); } public function testValidPhpAstExtractorConstraintVisitorArguments() { $container = new ContainerBuilder(); $container->register('translator.default') ->setArguments([null, null, null, null]); $container->register('validator'); $constraintVisitor = $container->register('translation.extractor.visitor.constraint', ConstraintVisitor::class); $container->register('validator.not_blank', NotBlankValidator::class) ->addTag('validator.constraint_validator'); $container->register('validator.isbn', IsbnValidator::class) ->addTag('validator.constraint_validator'); $container->register('validator.length', LengthValidator::class) ->addTag('validator.constraint_validator'); $container->register('validator.time', '%foo.time.validator.class%') ->addTag('validator.constraint_validator'); $container->setParameter('foo.time.validator.class', TimeValidator::class); $pass = new TranslatorPass(); $pass->process($container); $this->assertSame(['NotBlank', 'Isbn', 'Length', 'Time'], $constraintVisitor->getArgument(0)); } } ================================================ FILE: Tests/Dumper/CsvFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\CsvFileDumper; use Symfony\Component\Translation\MessageCatalogue; class CsvFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar', 'bar' => 'foo foo', 'foo;foo' => 'bar']); $dumper = new CsvFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/valid.csv', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/FileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\FileDumper; use Symfony\Component\Translation\MessageCatalogue; class FileDumperTest extends TestCase { public function testDump() { $tempDir = sys_get_temp_dir(); $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new ConcreteFileDumper(); $dumper->dump($catalogue, ['path' => $tempDir]); $this->assertFileExists($tempDir.'/messages.en.concrete'); @unlink($tempDir.'/messages.en.concrete'); } public function testDumpIntl() { $tempDir = sys_get_temp_dir(); $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar'], 'd1'); $catalogue->add(['bar' => 'foo'], 'd1+intl-icu'); $catalogue->add(['bar' => 'foo'], 'd2+intl-icu'); $dumper = new ConcreteFileDumper(); @unlink($tempDir.'/d2.en.concrete'); $dumper->dump($catalogue, ['path' => $tempDir]); $this->assertStringEqualsFile($tempDir.'/d1.en.concrete', 'foo=bar'); @unlink($tempDir.'/d1.en.concrete'); $this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo'); @unlink($tempDir.'/d1+intl-icu.en.concrete'); $this->assertFileDoesNotExist($tempDir.'/d2.en.concrete'); $this->assertStringEqualsFile($tempDir.'/d2+intl-icu.en.concrete', 'bar=foo'); @unlink($tempDir.'/d2+intl-icu.en.concrete'); } public function testDumpCreatesNestedDirectoriesAndFile() { $tempDir = sys_get_temp_dir(); $translationsDir = $tempDir.'/test/translations'; $file = $translationsDir.'/messages.en.concrete'; $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new ConcreteFileDumper(); $dumper->setRelativePathTemplate('test/translations/%domain%.%locale%.%extension%'); $dumper->dump($catalogue, ['path' => $tempDir]); $this->assertFileExists($file); @unlink($file); @rmdir($translationsDir); } } class ConcreteFileDumper extends FileDumper { public function formatCatalogue(MessageCatalogue $messages, $domain, array $options = []): string { return http_build_query($messages->all($domain), '', '&'); } protected function getExtension(): string { return 'concrete'; } } ================================================ FILE: Tests/Dumper/IcuResFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\IcuResFileDumper; use Symfony\Component\Translation\MessageCatalogue; class IcuResFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new IcuResFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resourcebundle/res/en.res', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/IniFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\IniFileDumper; use Symfony\Component\Translation\MessageCatalogue; class IniFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new IniFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.ini', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/JsonFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\JsonFileDumper; use Symfony\Component\Translation\MessageCatalogue; class JsonFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new JsonFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.json', $dumper->formatCatalogue($catalogue, 'messages')); } public function testDumpWithCustomEncoding() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => '"bar"']); $dumper = new JsonFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.dump.json', $dumper->formatCatalogue($catalogue, 'messages', ['json_encoding' => \JSON_HEX_QUOT])); } } ================================================ FILE: Tests/Dumper/MoFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\MoFileDumper; use Symfony\Component\Translation\MessageCatalogue; class MoFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new MoFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.mo', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/PhpFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\PhpFileDumper; use Symfony\Component\Translation\MessageCatalogue; class PhpFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); $dumper = new PhpFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.php', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/PoFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\PoFileDumper; use Symfony\Component\Translation\MessageCatalogue; class PoFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar', 'bar' => 'foo', 'foo_bar' => 'foobar', 'bar_foo' => 'barfoo']); $catalogue->setMetadata('foo_bar', [ 'comments' => [ 'Comment 1', 'Comment 2', ], 'flags' => [ 'fuzzy', 'another', ], 'sources' => [ 'src/file_1', 'src/file_2:50', ], ]); $catalogue->setMetadata('bar_foo', [ 'comments' => 'Comment', 'flags' => 'fuzzy', 'sources' => 'src/file_1', ]); $dumper = new PoFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.po', $dumper->formatCatalogue($catalogue, 'messages')); } public function testDumpPlurals() { $catalogue = new MessageCatalogue('en'); $catalogue->add([ 'foo|foos' => 'bar|bars', '{0} no foos|one foo|%count% foos' => '{0} no bars|one bar|%count% bars', ]); $dumper = new PoFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/plurals.po', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Dumper/QtFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\QtFileDumper; use Symfony\Component\Translation\MessageCatalogue; class QtFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar', 'foo_bar' => 'foobar', 'bar_foo' => 'barfoo'], 'resources'); $catalogue->setMetadata('foo_bar', [ 'comments' => [ 'Comment 1', 'Comment 2', ], 'flags' => [ 'fuzzy', 'another', ], 'sources' => [ 'src/file_1', 'src/file_2:50', ], ], 'resources'); $catalogue->setMetadata('bar_foo', [ 'comments' => 'Comment', 'flags' => 'fuzzy', 'sources' => 'src/file_1', ], 'resources'); $dumper = new QtFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/resources.ts', $dumper->formatCatalogue($catalogue, 'resources')); } } ================================================ FILE: Tests/Dumper/XliffFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\MessageCatalogue; class XliffFileDumperTest extends TestCase { public function testFormatCatalogue() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', 'key' => '', 'key.with.cdata' => ' & ', ]); $catalogue->setMetadata('foo', ['notes' => [['priority' => 1, 'from' => 'bar', 'content' => 'baz']]]); $catalogue->setMetadata('key', ['notes' => [['content' => 'baz'], ['content' => 'qux']]]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-clean.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR']) ); } public function testFormatCatalogueXliff2() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', 'key' => '', 'key.with.cdata' => ' & ', 'translation.key.that.is.longer.than.eighty.characters.should.not.have.name.attribute' => 'value', ]); $catalogue->setMetadata('key', ['target-attributes' => ['order' => 1]]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-2.0-clean.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } public function testFormatIcuCatalogueXliff2() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', ], 'messages'.MessageCatalogue::INTL_DOMAIN_SUFFIX); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-2.0+intl-icu.xlf', $dumper->formatCatalogue($catalogue, 'messages'.MessageCatalogue::INTL_DOMAIN_SUFFIX, ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } public function testFormatCatalogueWithCustomToolInfo() { $options = [ 'default_locale' => 'en_US', 'tool_info' => ['tool-id' => 'foo', 'tool-name' => 'foo', 'tool-version' => '0.0', 'tool-company' => 'Foo'], ]; $catalogue = new MessageCatalogue('en_US'); $catalogue->add(['foo' => 'bar']); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-tool-info.xlf', $dumper->formatCatalogue($catalogue, 'messages', $options) ); } public function testFormatCatalogueWithTargetAttributesMetadata() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', ]); $catalogue->setMetadata('foo', ['target-attributes' => ['state' => 'needs-translation']]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-target-attributes.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR']) ); } public function testFormatCatalogueWithNotesMetadata() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', 'baz' => 'biz', ]); $catalogue->setMetadata('foo', ['notes' => [ ['category' => 'state', 'content' => 'new'], ['category' => 'approved', 'content' => 'true'], ['category' => 'section', 'content' => 'user login', 'priority' => '1'], ]]); $catalogue->setMetadata('baz', ['notes' => [ ['id' => 'x', 'content' => 'x_content'], ['appliesTo' => 'target', 'category' => 'quality', 'content' => 'Fuzzy'], ]]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-notes-meta.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } public function testDumpCatalogueWithXliffExtension() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', 'key' => '', 'key.with.cdata' => ' & ', ]); $catalogue->setMetadata('foo', ['notes' => [['priority' => 1, 'from' => 'bar', 'content' => 'baz']]]); $catalogue->setMetadata('key', ['notes' => [['content' => 'baz'], ['content' => 'qux']]]); $dumper = new XliffFileDumper('xliff'); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-clean.xliff', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR']) ); } public function testEmptyMetadataNotes() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'empty' => 'notes', 'full' => 'notes', ]); $catalogue->setMetadata('empty', ['notes' => []]); $catalogue->setMetadata('full', ['notes' => [['category' => 'file-source', 'priority' => 1, 'content' => 'test/path/to/translation/Example.1.html.twig:27']]]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-2.0-empty-notes.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } public function testFormatCatalogueXliff2WithSegmentAttributes() { $catalogue = new MessageCatalogue('en_US'); $catalogue->add([ 'foo' => 'bar', 'key' => '', ]); $catalogue->setMetadata('foo', ['segment-attributes' => ['state' => 'translated']]); $catalogue->setMetadata('key', ['segment-attributes' => ['state' => 'translated', 'subState' => 'My Value']]); $dumper = new XliffFileDumper(); $this->assertStringEqualsFile( __DIR__.'/../Fixtures/resources-2.0-segment-attributes.xlf', $dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0']) ); } } ================================================ FILE: Tests/Dumper/YamlFileDumperTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Dumper; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\YamlFileDumper; use Symfony\Component\Translation\MessageCatalogue; class YamlFileDumperTest extends TestCase { public function testTreeFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add([ 'foo.bar1' => 'value1', 'foo.bar2' => 'value2', ]); $dumper = new YamlFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/messages.yml', $dumper->formatCatalogue($catalogue, 'messages', ['as_tree' => true, 'inline' => 999])); } public function testLinearFormatCatalogue() { $catalogue = new MessageCatalogue('en'); $catalogue->add([ 'foo.bar1' => 'value1', 'foo.bar2' => 'value2', ]); $dumper = new YamlFileDumper(); $this->assertStringEqualsFile(__DIR__.'/../Fixtures/messages_linear.yml', $dumper->formatCatalogue($catalogue, 'messages')); } } ================================================ FILE: Tests/Exception/ProviderExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Exception; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\ProviderException; use Symfony\Contracts\HttpClient\ResponseInterface; class ProviderExceptionTest extends TestCase { public function testExceptionWithDebugMessage() { $mock = $this->createStub(ResponseInterface::class); $mock->method('getInfo')->willReturn('debug'); $exception = new ProviderException('Exception message', $mock, 503); $this->assertSame('debug', $exception->getDebug()); } public function testExceptionWithNullAsDebugMessage() { $mock = $this->createStub(ResponseInterface::class); $mock->method('getInfo')->willReturn(null); $exception = new ProviderException('Exception message', $mock, 503); $this->assertSame('', $exception->getDebug()); } } ================================================ FILE: Tests/Exception/UnsupportedSchemeExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ClassExistsMock; use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProviderFactory; use Symfony\Component\Translation\Bridge\Loco\LocoProviderFactory; use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProviderFactory; use Symfony\Component\Translation\Bridge\Phrase\PhraseProviderFactory; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; use Symfony\Component\Translation\Provider\Dsn; #[RunTestsInSeparateProcesses] final class UnsupportedSchemeExceptionTest extends TestCase { public static function setUpBeforeClass(): void { ClassExistsMock::register(__CLASS__); ClassExistsMock::withMockedClasses([ CrowdinProviderFactory::class => false, LocoProviderFactory::class => false, LokaliseProviderFactory::class => false, PhraseProviderFactory::class => false, ]); } #[DataProvider('messageWhereSchemeIsPartOfSchemeToPackageMapProvider')] public function testMessageWhereSchemeIsPartOfSchemeToPackageMap(string $scheme, string $package) { $dsn = new Dsn(\sprintf('%s://localhost', $scheme)); $this->assertSame( \sprintf('Unable to synchronize translations via "%s" as the provider is not installed. Try running "composer require %s".', $scheme, $package), (new UnsupportedSchemeException($dsn))->getMessage() ); } public static function messageWhereSchemeIsPartOfSchemeToPackageMapProvider(): \Generator { yield ['crowdin', 'symfony/crowdin-translation-provider']; yield ['loco', 'symfony/loco-translation-provider']; yield ['lokalise', 'symfony/lokalise-translation-provider']; yield ['phrase', 'symfony/phrase-translation-provider']; } #[DataProvider('messageWhereSchemeIsNotPartOfSchemeToPackageMapProvider')] public function testMessageWhereSchemeIsNotPartOfSchemeToPackageMap(string $expected, Dsn $dsn, ?string $name, array $supported) { $this->assertSame( $expected, (new UnsupportedSchemeException($dsn, $name, $supported))->getMessage() ); } public static function messageWhereSchemeIsNotPartOfSchemeToPackageMapProvider(): \Generator { yield [ 'The "somethingElse" scheme is not supported.', new Dsn('somethingElse://localhost'), null, [], ]; yield [ 'The "somethingElse" scheme is not supported.', new Dsn('somethingElse://localhost'), 'foo', [], ]; yield [ 'The "somethingElse" scheme is not supported; supported schemes for translation provider "one" are: "one", "two".', new Dsn('somethingElse://localhost'), 'one', ['one', 'two'], ]; } } ================================================ FILE: Tests/Extractor/PhpAstExtractorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Extractor; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Extractor\PhpAstExtractor; use Symfony\Component\Translation\Extractor\Visitor\ConstraintVisitor; use Symfony\Component\Translation\Extractor\Visitor\TranslatableMessageVisitor; use Symfony\Component\Translation\Extractor\Visitor\TransMethodVisitor; use Symfony\Component\Translation\MessageCatalogue; final class PhpAstExtractorTest extends TestCase { public const OTHER_DOMAIN = 'not_messages'; #[DataProvider('resourcesProvider')] public function testExtraction(iterable|string $resource) { $extractor = new PhpAstExtractor([ new TransMethodVisitor(), new TranslatableMessageVisitor(), new ConstraintVisitor([ 'NotBlank', 'Isbn', 'Length', ], new TranslatableMessageVisitor()), ]); $extractor->setPrefix('prefix'); $catalogue = new MessageCatalogue('en'); $extractor->extract($resource, $catalogue); $expectedHeredoc = << [ 'translatable single-quoted key' => 'prefixtranslatable single-quoted key', 'translatable double-quoted key' => 'prefixtranslatable double-quoted key', 'translatable heredoc key' => 'prefixtranslatable heredoc key', 'translatable nowdoc key' => 'prefixtranslatable nowdoc key', "translatable double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable double-quoted key with whitespace and escaped \$\n\" sequences", 'translatable single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable single-quoted key with whitespace and nonescaped \$\n\' sequences', 'translatable single-quoted key with "quote mark at the end"' => 'prefixtranslatable single-quoted key with "quote mark at the end"', 'translatable '.$expectedHeredoc => 'prefixtranslatable '.$expectedHeredoc, 'translatable '.$expectedNowdoc => 'prefixtranslatable '.$expectedNowdoc, 'translatable concatenated message with heredoc and nowdoc' => 'prefixtranslatable concatenated message with heredoc and nowdoc', 'translatable default domain' => 'prefixtranslatable default domain', 'translatable-fqn single-quoted key' => 'prefixtranslatable-fqn single-quoted key', 'translatable-fqn double-quoted key' => 'prefixtranslatable-fqn double-quoted key', 'translatable-fqn heredoc key' => 'prefixtranslatable-fqn heredoc key', 'translatable-fqn nowdoc key' => 'prefixtranslatable-fqn nowdoc key', "translatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-fqn double-quoted key with whitespace and escaped \$\n\" sequences", 'translatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', 'translatable-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-fqn single-quoted key with "quote mark at the end"', 'translatable-fqn '.$expectedHeredoc => 'prefixtranslatable-fqn '.$expectedHeredoc, 'translatable-fqn '.$expectedNowdoc => 'prefixtranslatable-fqn '.$expectedNowdoc, 'translatable-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-fqn concatenated message with heredoc and nowdoc', 'translatable-fqn default domain' => 'prefixtranslatable-fqn default domain', 'translatable-short single-quoted key' => 'prefixtranslatable-short single-quoted key', 'translatable-short double-quoted key' => 'prefixtranslatable-short double-quoted key', 'translatable-short heredoc key' => 'prefixtranslatable-short heredoc key', 'translatable-short nowdoc key' => 'prefixtranslatable-short nowdoc key', "translatable-short double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short double-quoted key with whitespace and escaped \$\n\" sequences", 'translatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short single-quoted key with whitespace and nonescaped \$\n\' sequences', 'translatable-short single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short single-quoted key with "quote mark at the end"', 'translatable-short '.$expectedHeredoc => 'prefixtranslatable-short '.$expectedHeredoc, 'translatable-short '.$expectedNowdoc => 'prefixtranslatable-short '.$expectedNowdoc, 'translatable-short concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short concatenated message with heredoc and nowdoc', 'translatable-short default domain' => 'prefixtranslatable-short default domain', 'translatable-short-fqn single-quoted key' => 'prefixtranslatable-short-fqn single-quoted key', 'translatable-short-fqn double-quoted key' => 'prefixtranslatable-short-fqn double-quoted key', 'translatable-short-fqn heredoc key' => 'prefixtranslatable-short-fqn heredoc key', 'translatable-short-fqn nowdoc key' => 'prefixtranslatable-short-fqn nowdoc key', "translatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixtranslatable-short-fqn double-quoted key with whitespace and escaped \$\n\" sequences", 'translatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixtranslatable-short-fqn single-quoted key with whitespace and nonescaped \$\n\' sequences', 'translatable-short-fqn single-quoted key with "quote mark at the end"' => 'prefixtranslatable-short-fqn single-quoted key with "quote mark at the end"', 'translatable-short-fqn '.$expectedHeredoc => 'prefixtranslatable-short-fqn '.$expectedHeredoc, 'translatable-short-fqn '.$expectedNowdoc => 'prefixtranslatable-short-fqn '.$expectedNowdoc, 'translatable-short-fqn concatenated message with heredoc and nowdoc' => 'prefixtranslatable-short-fqn concatenated message with heredoc and nowdoc', 'translatable-short-fqn default domain' => 'prefixtranslatable-short-fqn default domain', 'single-quoted key' => 'prefixsingle-quoted key', 'double-quoted key' => 'prefixdouble-quoted key', 'heredoc key' => 'prefixheredoc key', 'nowdoc key' => 'prefixnowdoc key', "double-quoted key with whitespace and escaped \$\n\" sequences" => "prefixdouble-quoted key with whitespace and escaped \$\n\" sequences", 'single-quoted key with whitespace and nonescaped \$\n\' sequences' => 'prefixsingle-quoted key with whitespace and nonescaped \$\n\' sequences', 'single-quoted key with "quote mark at the end"' => 'prefixsingle-quoted key with "quote mark at the end"', $expectedHeredoc => 'prefix'.$expectedHeredoc, $expectedNowdoc => 'prefix'.$expectedNowdoc, 'concatenated message with heredoc and nowdoc' => 'prefixconcatenated message with heredoc and nowdoc', 'default domain' => 'prefixdefault domain', 'mix-named-arguments' => 'prefixmix-named-arguments', 'mix-named-arguments-locale' => 'prefixmix-named-arguments-locale', 'mix-named-arguments-without-domain' => 'prefixmix-named-arguments-without-domain', ], 'not_messages' => [ 'translatable other-domain-test-no-params-short-array' => 'prefixtranslatable other-domain-test-no-params-short-array', 'translatable other-domain-test-no-params-long-array' => 'prefixtranslatable other-domain-test-no-params-long-array', 'translatable other-domain-test-params-short-array' => 'prefixtranslatable other-domain-test-params-short-array', 'translatable other-domain-test-params-long-array' => 'prefixtranslatable other-domain-test-params-long-array', 'translatable typecast' => 'prefixtranslatable typecast', 'translatable-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-fqn other-domain-test-no-params-short-array', 'translatable-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-fqn other-domain-test-no-params-long-array', 'translatable-fqn other-domain-test-params-short-array' => 'prefixtranslatable-fqn other-domain-test-params-short-array', 'translatable-fqn other-domain-test-params-long-array' => 'prefixtranslatable-fqn other-domain-test-params-long-array', 'translatable-fqn typecast' => 'prefixtranslatable-fqn typecast', 'translatable-short other-domain-test-no-params-short-array' => 'prefixtranslatable-short other-domain-test-no-params-short-array', 'translatable-short other-domain-test-no-params-long-array' => 'prefixtranslatable-short other-domain-test-no-params-long-array', 'translatable-short other-domain-test-params-short-array' => 'prefixtranslatable-short other-domain-test-params-short-array', 'translatable-short other-domain-test-params-long-array' => 'prefixtranslatable-short other-domain-test-params-long-array', 'translatable-short typecast' => 'prefixtranslatable-short typecast', 'translatable-short-fqn other-domain-test-no-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-short-array', 'translatable-short-fqn other-domain-test-no-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-no-params-long-array', 'translatable-short-fqn other-domain-test-params-short-array' => 'prefixtranslatable-short-fqn other-domain-test-params-short-array', 'translatable-short-fqn other-domain-test-params-long-array' => 'prefixtranslatable-short-fqn other-domain-test-params-long-array', 'translatable-short-fqn typecast' => 'prefixtranslatable-short-fqn typecast', 'other-domain-test-no-params-short-array' => 'prefixother-domain-test-no-params-short-array', 'other-domain-test-no-params-long-array' => 'prefixother-domain-test-no-params-long-array', 'other-domain-test-params-short-array' => 'prefixother-domain-test-params-short-array', 'other-domain-test-params-long-array' => 'prefixother-domain-test-params-long-array', 'typecast' => 'prefixtypecast', 'ordered-named-arguments-in-trans-method' => 'prefixordered-named-arguments-in-trans-method', 'disordered-named-arguments-in-trans-method' => 'prefixdisordered-named-arguments-in-trans-method', 'variable-assignation-inlined-in-trans-method-call1' => 'prefixvariable-assignation-inlined-in-trans-method-call1', 'variable-assignation-inlined-in-trans-method-call2' => 'prefixvariable-assignation-inlined-in-trans-method-call2', 'variable-assignation-inlined-in-trans-method-call3' => 'prefixvariable-assignation-inlined-in-trans-method-call3', 'variable-assignation-inlined-with-named-arguments-in-trans-method' => 'prefixvariable-assignation-inlined-with-named-arguments-in-trans-method', 'mix-named-arguments-without-parameters' => 'prefixmix-named-arguments-without-parameters', 'mix-named-arguments-disordered' => 'prefixmix-named-arguments-disordered', 'const-domain' => 'prefixconst-domain', ], 'validators' => [ 'message-in-constraint-attribute' => 'prefixmessage-in-constraint-attribute', // 'custom Isbn message from attribute' => 'prefixcustom Isbn message from attribute', 'custom Isbn message from attribute with options as array' => 'prefixcustom Isbn message from attribute with options as array', 'custom Length exact message from attribute from named argument' => 'prefixcustom Length exact message from attribute from named argument', 'custom Length exact message from attribute from named argument 1/2' => 'prefixcustom Length exact message from attribute from named argument 1/2', 'custom Length min message from attribute from named argument 2/2' => 'prefixcustom Length min message from attribute from named argument 2/2', // 'custom Isbn message' => 'prefixcustom Isbn message', 'custom Isbn message with options as array' => 'prefixcustom Isbn message with options as array', 'custom Isbn message from named argument' => 'prefixcustom Isbn message from named argument', 'custom Length exact message from named argument' => 'prefixcustom Length exact message from named argument', 'custom Length exact message from named argument 1/2' => 'prefixcustom Length exact message from named argument 1/2', 'custom Length min message from named argument 2/2' => 'prefixcustom Length min message from named argument 2/2', ], ]; $actualCatalogue = $catalogue->all(); $this->assertEquals($expectedCatalogue, $actualCatalogue); $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable.html.php'; $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable single-quoted key')); $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable other-domain-test-no-params-short-array', 'not_messages')); $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-fqn.html.php'; $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-fqn single-quoted key')); $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-fqn other-domain-test-no-params-short-array', 'not_messages')); $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-short.html.php'; $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-short single-quoted key')); $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-short other-domain-test-no-params-short-array', 'not_messages')); $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translatable-short-fqn.html.php'; $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('translatable-short-fqn single-quoted key')); $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('translatable-short-fqn other-domain-test-no-params-short-array', 'not_messages')); $filename = str_replace(\DIRECTORY_SEPARATOR, '/', __DIR__).'/../Fixtures/extractor-ast/translation.html.php'; $this->assertEquals(['sources' => [$filename.':2']], $catalogue->getMetadata('single-quoted key')); $this->assertEquals(['sources' => [$filename.':37']], $catalogue->getMetadata('other-domain-test-no-params-short-array', 'not_messages')); } public function testExtractionFromIndentedHeredocNowdoc() { $catalogue = new MessageCatalogue('en'); $extractor = new PhpAstExtractor([ new TransMethodVisitor(), new TranslatableMessageVisitor(), new ConstraintVisitor([ 'NotBlank', 'Isbn', 'Length', ], new TranslatableMessageVisitor()), ]); $extractor->setPrefix('prefix'); $extractor->extract(__DIR__.'/../Fixtures/extractor-7.3/translation.html.php', $catalogue); $expectedCatalogue = [ 'messages' => [ "heredoc\nindented\n further" => "prefixheredoc\nindented\n further", "nowdoc\nindented\n further" => "prefixnowdoc\nindented\n further", ], ]; $this->assertEquals($expectedCatalogue, $catalogue->all()); } public static function resourcesProvider(): array { $directory = __DIR__.'/../Fixtures/extractor-ast/'; $phpFiles = []; $splFiles = []; foreach (new \DirectoryIterator($directory) as $fileInfo) { if ($fileInfo->isDot()) { continue; } if (\in_array($fileInfo->getBasename(), ['translatable.html.php', 'translatable-fqn.html.php', 'translatable-short.html.php', 'translatable-short-fqn.html.php', 'translation.html.php', 'validator-constraints.php'], true)) { $phpFiles[] = $fileInfo->getPathname(); } $splFiles[] = $fileInfo->getFileInfo(); } return [ [$directory], [$phpFiles], [glob($directory.'*')], [$splFiles], [new \ArrayObject(glob($directory.'*'))], [new \ArrayObject($splFiles)], ]; } } ================================================ FILE: Tests/Fixtures/empty-translation.po ================================================ msgid "foo" msgstr "" ================================================ FILE: Tests/Fixtures/empty.csv ================================================ ================================================ FILE: Tests/Fixtures/empty.ini ================================================ ================================================ FILE: Tests/Fixtures/empty.json ================================================ ================================================ FILE: Tests/Fixtures/empty.mo ================================================ ================================================ FILE: Tests/Fixtures/empty.po ================================================ ================================================ FILE: Tests/Fixtures/empty.xlf ================================================ ================================================ FILE: Tests/Fixtures/empty.yml ================================================ ================================================ FILE: Tests/Fixtures/encoding.xlf ================================================ foo br bz bar f ================================================ FILE: Tests/Fixtures/escaped-id-plurals.po ================================================ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en\n" msgid "escaped \"foo\"" msgid_plural "escaped \"foos\"" msgstr[0] "escaped \"bar\"" msgstr[1] "escaped \"bars\"" ================================================ FILE: Tests/Fixtures/escaped-id.po ================================================ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en\n" msgid "escaped \"foo\"" msgstr "escaped \"bar\"" ================================================ FILE: Tests/Fixtures/extractor/resource.format.engine ================================================ ================================================ FILE: Tests/Fixtures/extractor/this.is.a.template.format.engine ================================================ ================================================ FILE: Tests/Fixtures/extractor/translatable-fqn.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor/translatable-short.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor/translatable.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor/translation.html.php ================================================ This template is used for translation message extraction tests trans('single-quoted key'); ?> trans('double-quoted key'); ?> trans(<< trans(<<<'EOF' nowdoc key EOF ); ?> trans( "double-quoted key with whitespace and escaped \$\n\" sequences" ); ?> trans( 'single-quoted key with whitespace and nonescaped \$\n\' sequences' ); ?> trans(<< trans(<<<'EOF' nowdoc key with whitespace and nonescaped \$\n sequences EOF ); ?> trans('single-quoted key with "quote mark at the end"'); ?> trans('concatenated'.' message'.<< trans('other-domain-test-no-params-short-array', [], 'not_messages'); ?> trans('other-domain-test-no-params-long-array', [], 'not_messages'); ?> trans('other-domain-test-params-short-array', ['foo' => 'bar'], 'not_messages'); ?> trans('other-domain-test-params-long-array', ['foo' => 'bar'], 'not_messages'); ?> trans('typecast', ['a' => (int) '123'], 'not_messages'); ?> trans('default domain', [], null); ?> ================================================ FILE: Tests/Fixtures/extractor-7.3/translation.html.php ================================================ This template is used for translation message extraction tests trans(<< trans(<<<'EOF' nowdoc indented further EOF ); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/resource.format.engine ================================================ ================================================ FILE: Tests/Fixtures/extractor-ast/this.is.a.template.format.engine ================================================ ================================================ FILE: Tests/Fixtures/extractor-ast/translatable-fqn.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/translatable-short-fqn.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/translatable-short.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/translatable.html.php ================================================ This template is used for translation message extraction tests 'bar'], 'not_messages'); ?> 'bar'], 'not_messages'); ?> (int) '123'], 'not_messages'); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/translation.html.php ================================================ This template is used for translation message extraction tests trans('single-quoted key'); ?> trans('double-quoted key'); ?> trans(<< trans(<<<'EOF' nowdoc key EOF ); ?> trans( "double-quoted key with whitespace and escaped \$\n\" sequences" ); ?> trans( 'single-quoted key with whitespace and nonescaped \$\n\' sequences' ); ?> trans(<< trans(<<<'EOF' nowdoc key with whitespace and nonescaped \$\n sequences EOF ); ?> trans('single-quoted key with "quote mark at the end"'); ?> trans('concatenated'.' message'.<< trans('other-domain-test-no-params-short-array', [], 'not_messages'); ?> trans('other-domain-test-no-params-long-array', [], 'not_messages'); ?> trans('other-domain-test-params-short-array', ['foo' => 'bar'], 'not_messages'); ?> trans('other-domain-test-params-long-array', ['foo' => 'bar'], 'not_messages'); ?> trans('typecast', ['a' => (int) '123'], 'not_messages'); ?> trans('default domain', [], null); ?> trans(id: 'ordered-named-arguments-in-trans-method', parameters: [], domain: 'not_messages'); ?> trans(domain: 'not_messages', id: 'disordered-named-arguments-in-trans-method', parameters: []); ?> trans($key = 'variable-assignation-inlined-in-trans-method-call1', $parameters = [], $domain = 'not_messages'); ?> trans('variable-assignation-inlined-in-trans-method-call2', $parameters = [], $domain = 'not_messages'); ?> trans('variable-assignation-inlined-in-trans-method-call3', [], $domain = 'not_messages'); ?> trans(domain: $domain = 'not_messages', id: $key = 'variable-assignation-inlined-with-named-arguments-in-trans-method', parameters: $parameters = []); ?> trans('mix-named-arguments', parameters: ['foo' => 'bar']); ?> trans('mix-named-arguments-locale', parameters: ['foo' => 'bar'], locale: 'de'); ?> trans('mix-named-arguments-without-domain', parameters: ['foo' => 'bar']); ?> trans('mix-named-arguments-without-parameters', domain: 'not_messages'); ?> trans('mix-named-arguments-disordered', domain: 'not_messages', parameters: []); ?> trans(...); // should not fail ?> trans('const-domain', [], PhpAstExtractorTest::OTHER_DOMAIN); ?> ================================================ FILE: Tests/Fixtures/extractor-ast/validator-constraints.php ================================================ This template is used for translation message extraction tests 'isbn10', 'message' => 'custom Isbn message from attribute with options as array', ])] public string $isbn2; } class Foo2 { public function index() { $constraint1 = new Assert\Isbn('isbn10', 'custom Isbn message'); // no way to handle those arguments (not named, not in associative array). $constraint2 = new Assert\Isbn([ 'type' => 'isbn10', 'message' => 'custom Isbn message with options as array', ]); $constraint3 = new Assert\Isbn(message: 'custom Isbn message from named argument'); $constraint4 = new Assert\Length(exactMessage: 'custom Length exact message from named argument'); $constraint5 = new Assert\Length(exactMessage: 'custom Length exact message from named argument 1/2', minMessage: 'custom Length min message from named argument 2/2'); } } ================================================ FILE: Tests/Fixtures/fuzzy-translations.po ================================================ #, php-format msgid "foo1" msgstr "bar1" #, fuzzy, php-format msgid "foo2" msgstr "fuzzy bar2" msgid "foo3" msgstr "bar3" ================================================ FILE: Tests/Fixtures/invalid-xml-resources.xlf ================================================ foo bar extra key test with note ================================================ FILE: Tests/Fixtures/malformed.json ================================================ { "foo" "bar" } ================================================ FILE: Tests/Fixtures/messages.yml ================================================ foo: bar1: value1 bar2: value2 ================================================ FILE: Tests/Fixtures/messages_linear.yml ================================================ foo.bar1: value1 foo.bar2: value2 ================================================ FILE: Tests/Fixtures/missing-plurals.po ================================================ msgid "foo" msgid_plural "foos" msgstr[3] "bars" msgstr[1] "bar" ================================================ FILE: Tests/Fixtures/non-string.yml ================================================ root: foo1: foo2: '' bar: 'bar' ================================================ FILE: Tests/Fixtures/non-valid.xlf ================================================ foo bar ================================================ FILE: Tests/Fixtures/non-valid.yml ================================================ foo ================================================ FILE: Tests/Fixtures/plurals.po ================================================ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en\n" msgid "foo" msgid_plural "foos" msgstr[0] "bar" msgstr[1] "bars" msgid "{0} no foos|one foo|%count% foos" msgstr "{0} no bars|one bar|%count% bars" ================================================ FILE: Tests/Fixtures/resname.xlf ================================================ bar bar source baz baz foo qux source ================================================ FILE: Tests/Fixtures/resourcebundle/dat/en.txt ================================================ en{ symfony{"Symfony is great"} } ================================================ FILE: Tests/Fixtures/resourcebundle/dat/fr.txt ================================================ fr{ symfony{"Symfony est génial"} } ================================================ FILE: Tests/Fixtures/resourcebundle/dat/packagelist.txt ================================================ en.res fr.res ================================================ FILE: Tests/Fixtures/resources-2.0+intl-icu.xlf ================================================ foo bar ================================================ FILE: Tests/Fixtures/resources-2.0-clean.xlf ================================================ foo bar key key.with.cdata & ]]> translation.key.that.is.longer.than.eighty.characters.should.not.have.name.attribute value ================================================ FILE: Tests/Fixtures/resources-2.0-empty-notes.xlf ================================================ empty notes test/path/to/translation/Example.1.html.twig:27 full notes ================================================ FILE: Tests/Fixtures/resources-2.0-multi-segment-unit.xlf ================================================ true foo foo (translated) bar bar (translated) ================================================ FILE: Tests/Fixtures/resources-2.0-name.xlf ================================================ bar bar source baz baz foo qux source ================================================ FILE: Tests/Fixtures/resources-2.0-segment-attributes.xlf ================================================ foo bar key ================================================ FILE: Tests/Fixtures/resources-2.0.xlf ================================================ Quetzal Quetzal foo XLIFF 文書を編集、または処理 するアプリケーションです。 bar XLIFF データ・マネージャ ================================================ FILE: Tests/Fixtures/resources-2.1.xlf ================================================ Quetzal Quetzal foo XLIFF 文書を編集、または処理 するアプリケーションです。 bar XLIFF データ・マネージャ ================================================ FILE: Tests/Fixtures/resources-2.2-pgs-combined.xlf ================================================ did not invite anyone to her party. n'a invité personne à sa fête. invited one guest to her party. a invité un convive à sa fête. invited guests to her party. a invité convives à sa fête. did not invite anyone to his party. n'a invité personne à sa fête. invited one guest to his party. a invité un convive à sa fête. invited guests to his party. a invité convives à sa fête. did not invite anyone to their party. n'a invité personne à leur fête. invited one guest to their party. a invité un convive à leur fête. invited guests to their party. a invité convives à leur fête. ================================================ FILE: Tests/Fixtures/resources-2.2-pgs-gender.xlf ================================================ You are invited to her party Vous êtes invité à sa fête You are invited to his party Vous êtes invité à sa fête You are invited to their party Vous êtes invité à leur fête ================================================ FILE: Tests/Fixtures/resources-2.2-pgs-plural.xlf ================================================ You deleted no file. Vous n'avez supprimé aucun fichier. You deleted one file. Vous avez supprimé un fichier. You deleted files. Vous avez supprimé fichiers. ================================================ FILE: Tests/Fixtures/resources-2.2.xlf ================================================ Quetzal Quetzal foo XLIFF 文書を編集、または処理 するアプリケーションです。 bar XLIFF データ・マネージャ ================================================ FILE: Tests/Fixtures/resources-clean.xlf ================================================
foo bar baz key baz qux key.with.cdata & ]]>
================================================ FILE: Tests/Fixtures/resources-clean.xliff ================================================
foo bar baz key baz qux key.with.cdata & ]]>
================================================ FILE: Tests/Fixtures/resources-multi-files.xlf ================================================ foo bar extra key test with note ================================================ FILE: Tests/Fixtures/resources-notes-meta.xlf ================================================ new true user login foo bar x_content Fuzzy baz biz ================================================ FILE: Tests/Fixtures/resources-target-attributes.xlf ================================================
foo bar
================================================ FILE: Tests/Fixtures/resources-tool-info.xlf ================================================
foo bar
================================================ FILE: Tests/Fixtures/resources.csv ================================================ "foo"; "bar" #"bar"; "foo" # all incorrect examples: "incorrect"; "number"; "columns"; "will"; "be"; "ignored" "incorrect" ================================================ FILE: Tests/Fixtures/resources.dump.json ================================================ {"foo":"\u0022bar\u0022"} ================================================ FILE: Tests/Fixtures/resources.ini ================================================ foo="bar" ================================================ FILE: Tests/Fixtures/resources.json ================================================ { "foo": "bar" } ================================================ FILE: Tests/Fixtures/resources.php ================================================ 'bar', ); ================================================ FILE: Tests/Fixtures/resources.po ================================================ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: en\n" msgid "foo" msgstr "bar" msgid "bar" msgstr "foo" # Comment 1 # Comment 2 #, fuzzy,another #: src/file_1 src/file_2:50 msgid "foo_bar" msgstr "foobar" # Comment #, fuzzy #: src/file_1 msgid "bar_foo" msgstr "barfoo" ================================================ FILE: Tests/Fixtures/resources.ts ================================================ resources foo bar foo_bar foobar bar_foo barfoo ================================================ FILE: Tests/Fixtures/resources.xlf ================================================ foo bar extra key test with note skipped skipped ================================================ FILE: Tests/Fixtures/resources.yml ================================================ foo: bar ================================================ FILE: Tests/Fixtures/valid.csv ================================================ foo;bar bar;"foo foo" "foo;foo";bar ================================================ FILE: Tests/Fixtures/with-attributes.xlf ================================================ foo bar extra bar key baz qux ================================================ FILE: Tests/Fixtures/withdoctype.xlf ================================================ foo bar ================================================ FILE: Tests/Fixtures/withnote.xlf ================================================ foo bar foo extrasource bar key baz qux ================================================ FILE: Tests/Formatter/IntlFormatterTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Formatter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Formatter\IntlFormatter; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; #[RequiresPhpExtension('intl')] class IntlFormatterTest extends TestCase { #[DataProvider('provideDataForFormat')] public function testFormat($expected, $message, $arguments) { $this->assertEquals($expected, trim((new IntlFormatter())->formatIntl($message, 'en', $arguments))); } public function testInvalidFormat() { $this->expectException(InvalidArgumentException::class); (new IntlFormatter())->formatIntl('{foo', 'en', [2]); } public function testFormatWithNamedArguments() { if (version_compare(\INTL_ICU_VERSION, '4.8', '<')) { $this->markTestSkipped('Format with named arguments can only be run with ICU 4.8 or higher and PHP >= 5.5'); } $chooseMessage = <<<'_MSG_' {gender_of_host, select, female {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to her party.} =2 {{host} invites {guest} and one other person to her party.} other {{host} invites {guest} as one of the # people invited to her party.}}} male {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to his party.} =2 {{host} invites {guest} and one other person to his party.} other {{host} invites {guest} as one of the # people invited to his party.}}} other {{num_guests, plural, offset:1 =0 {{host} does not give a party.} =1 {{host} invites {guest} to their party.} =2 {{host} invites {guest} and one other person to their party.} other {{host} invites {guest} as one of the # people invited to their party.}}}} _MSG_; $message = (new IntlFormatter())->formatIntl($chooseMessage, 'en', [ 'gender_of_host' => 'male', 'num_guests' => 10, 'host' => 'Fabien', 'guest' => 'Guilherme', ]); $this->assertEquals('Fabien invites Guilherme as one of the 9 people invited to his party.', $message); } public static function provideDataForFormat() { return [ [ 'There is one apple', 'There is one apple', [], ], [ '4,560 monkeys on 123 trees make 37.073 monkeys per tree', '{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree', [4560, 123, 4560 / 123], ], [ '', '', [], ], ]; } #[DataProvider('percentAndBracketsAreTrimmedProvider')] public function testPercentsAndBracketsAreTrimmed(string $expected, string $message, array $parameters) { $formatter = new IntlFormatter(); $this->assertInstanceof(IntlFormatterInterface::class, $formatter); $this->assertSame($expected, $formatter->formatIntl($message, 'en', $parameters)); } public static function percentAndBracketsAreTrimmedProvider(): array { return [ ['Hello Fab', 'Hello {name}', ['name' => 'Fab']], ['Hello Fab', 'Hello {name}', ['%name%' => 'Fab']], ['Hello Fab', 'Hello {name}', ['{{ name }}' => 'Fab']], ]; } } ================================================ FILE: Tests/Formatter/MessageFormatterTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Formatter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Formatter\MessageFormatter; class MessageFormatterTest extends TestCase { #[DataProvider('getTransMessages')] public function testFormat($expected, $message, $parameters = []) { $this->assertEquals($expected, $this->getMessageFormatter()->format($message, 'en', $parameters)); } public static function getTransMessages() { return [ [ 'There is one apple', 'There is one apple', ], [ 'There are 5 apples', 'There are %count% apples', ['%count%' => 5], ], [ 'There are 5 apples', 'There are {{count}} apples', ['{{count}}' => 5], ], ]; } private function getMessageFormatter() { return new MessageFormatter(); } } ================================================ FILE: Tests/IdentityTranslatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Contracts\Translation\Test\TranslatorTest; use Symfony\Contracts\Translation\TranslatorInterface; class IdentityTranslatorTest extends TranslatorTest { private string $defaultLocale; protected function setUp(): void { parent::setUp(); $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } protected function tearDown(): void { parent::tearDown(); \Locale::setDefault($this->defaultLocale); } public function getTranslator(): TranslatorInterface { return new IdentityTranslator(); } } ================================================ FILE: Tests/Loader/CsvFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\CsvFileLoader; class CsvFileLoaderTest extends TestCase { public function testLoad() { $loader = new CsvFileLoader(); $resource = __DIR__.'/../Fixtures/resources.csv'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadDoesNothingIfEmpty() { $loader = new CsvFileLoader(); $resource = __DIR__.'/../Fixtures/empty.csv'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new CsvFileLoader())->load(__DIR__.'/../Fixtures/not-exists.csv', 'en', 'domain1'); } public function testLoadNonLocalResource() { $this->expectException(InvalidResourceException::class); (new CsvFileLoader())->load('http://example.com/resources.csv', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/IcuDatFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\IcuDatFileLoader; #[RequiresPhpExtension('intl')] class IcuDatFileLoaderTest extends LocalizedTestCase { public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); (new IcuDatFileLoader())->load(__DIR__.'/../Fixtures/resourcebundle/corrupted/resources', 'es', 'domain2'); } public function testDatEnglishLoad() { // bundled resource is build using pkgdata command which at least in ICU 4.2 comes in extremely! buggy form // you must specify an temporary build directory which is not the same as current directory and // MUST reside on the same partition. pkgdata -p resources -T /srv -d.packagelist.txt $loader = new IcuDatFileLoader(); $resource = __DIR__.'/../Fixtures/resourcebundle/dat/resources'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['symfony' => 'Symfony 2 is great'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource.'.dat')], $catalogue->getResources()); } public function testDatFrenchLoad() { $loader = new IcuDatFileLoader(); $resource = __DIR__.'/../Fixtures/resourcebundle/dat/resources'; $catalogue = $loader->load($resource, 'fr', 'domain1'); $this->assertEquals(['symfony' => 'Symfony 2 est génial'], $catalogue->all('domain1')); $this->assertEquals('fr', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource.'.dat')], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new IcuDatFileLoader())->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/IcuResFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\IcuResFileLoader; #[RequiresPhpExtension('intl')] class IcuResFileLoaderTest extends LocalizedTestCase { public function testLoad() { // resource is build using genrb command $loader = new IcuResFileLoader(); $resource = __DIR__.'/../Fixtures/resourcebundle/res'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new DirectoryResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new IcuResFileLoader())->load(__DIR__.'/../Fixtures/non-existing.txt', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); (new IcuResFileLoader())->load(__DIR__.'/../Fixtures/resourcebundle/corrupted', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/IniFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\IniFileLoader; class IniFileLoaderTest extends TestCase { public function testLoad() { $loader = new IniFileLoader(); $resource = __DIR__.'/../Fixtures/resources.ini'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadDoesNothingIfEmpty() { $loader = new IniFileLoader(); $resource = __DIR__.'/../Fixtures/empty.ini'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new IniFileLoader())->load(__DIR__.'/../Fixtures/non-existing.ini', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/JsonFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\JsonFileLoader; class JsonFileLoaderTest extends TestCase { public function testLoad() { $loader = new JsonFileLoader(); $resource = __DIR__.'/../Fixtures/resources.json'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadDoesNothingIfEmpty() { $loader = new JsonFileLoader(); $resource = __DIR__.'/../Fixtures/empty.json'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new JsonFileLoader())->load(__DIR__.'/../Fixtures/non-existing.json', 'en', 'domain1'); } public function testParseException() { $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage('Error parsing JSON: Syntax error, malformed JSON'); (new JsonFileLoader())->load(__DIR__.'/../Fixtures/malformed.json', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/LocalizedTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; abstract class LocalizedTestCase extends TestCase { protected function setUp(): void { if (!\extension_loaded('intl')) { $this->markTestSkipped('Extension intl is required.'); } } } ================================================ FILE: Tests/Loader/MoFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\MoFileLoader; class MoFileLoaderTest extends TestCase { public function testLoad() { $loader = new MoFileLoader(); $resource = __DIR__.'/../Fixtures/resources.mo'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadPlurals() { $loader = new MoFileLoader(); $resource = __DIR__.'/../Fixtures/plurals.mo'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([ 'foo|foos' => 'bar|bars', '{0} no foos|one foo|%count% foos' => '{0} no bars|one bar|%count% bars', ], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new MoFileLoader())->load(__DIR__.'/../Fixtures/non-existing.mo', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); (new MoFileLoader())->load(__DIR__.'/../Fixtures/empty.mo', 'en', 'domain1'); } public function testLoadEmptyTranslation() { $loader = new MoFileLoader(); $resource = __DIR__.'/../Fixtures/empty-translation.mo'; $catalogue = $loader->load($resource, 'en', 'message'); $this->assertEquals([], $catalogue->all('message')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } } ================================================ FILE: Tests/Loader/PhpFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\PhpFileLoader; class PhpFileLoaderTest extends TestCase { public function testLoad() { $loader = new PhpFileLoader(); $resource = __DIR__.'/../Fixtures/resources.php'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new PhpFileLoader())->load(__DIR__.'/../Fixtures/non-existing.php', 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { $this->expectException(InvalidResourceException::class); (new PhpFileLoader())->load('http://example.com/resources.php', 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/PoFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\PoFileLoader; class PoFileLoaderTest extends TestCase { public function testLoad() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/resources.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar', 'bar' => 'foo'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadPlurals() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/plurals.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([ 'foo|foos' => 'bar|bars', '{0} no foos|one foo|%count% foos' => '{0} no bars|one bar|%count% bars', ], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadDoesNothingIfEmpty() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/empty.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new PoFileLoader())->load(__DIR__.'/../Fixtures/non-existing.po', 'en', 'domain1'); } public function testLoadEmptyTranslation() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/empty-translation.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => ''], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testEscapedId() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/escaped-id.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $messages = $catalogue->all('domain1'); $this->assertArrayHasKey('escaped "foo"', $messages); $this->assertEquals('escaped "bar"', $messages['escaped "foo"']); } public function testEscapedIdPlurals() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/escaped-id-plurals.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $messages = $catalogue->all('domain1'); $this->assertArrayHasKey('escaped "foo"|escaped "foos"', $messages); $this->assertEquals('escaped "bar"|escaped "bars"', $messages['escaped "foo"|escaped "foos"']); } public function testSkipFuzzyTranslations() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/fuzzy-translations.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $messages = $catalogue->all('domain1'); $this->assertArrayHasKey('foo1', $messages); $this->assertArrayNotHasKey('foo2', $messages); $this->assertArrayHasKey('foo3', $messages); } public function testMissingPlurals() { $loader = new PoFileLoader(); $resource = __DIR__.'/../Fixtures/missing-plurals.po'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([ 'foo|foos' => '-|bar|-|bars', ], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); } } ================================================ FILE: Tests/Loader/QtFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\QtFileLoader; class QtFileLoaderTest extends TestCase { public function testLoad() { $loader = new QtFileLoader(); $resource = __DIR__.'/../Fixtures/resources.ts'; $catalogue = $loader->load($resource, 'en', 'resources'); $this->assertEquals([ 'foo' => 'bar', 'foo_bar' => 'foobar', 'bar_foo' => 'barfoo', ], $catalogue->all('resources')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new QtFileLoader())->load(__DIR__.'/../Fixtures/non-existing.ts', 'en', 'domain1'); } public function testLoadNonLocalResource() { $this->expectException(InvalidResourceException::class); (new QtFileLoader())->load('http://domain1.com/resources.ts', 'en', 'domain1'); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); (new QtFileLoader())->load(__DIR__.'/../Fixtures/invalid-xml-resources.xlf', 'en', 'domain1'); } public function testLoadEmptyResource() { $resource = __DIR__.'/../Fixtures/empty.xlf'; $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage(\sprintf('Unable to load "%s".', $resource)); (new QtFileLoader())->load($resource, 'en', 'domain1'); } } ================================================ FILE: Tests/Loader/XliffFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\XliffFileLoader; use Symfony\Component\Translation\MessageCatalogueInterface; class XliffFileLoaderTest extends TestCase { public function testLoadFile() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); $this->assertContainsOnlyString($catalogue->all('domain1')); } public function testLoadRawXliff() { $loader = new XliffFileLoader(); $resource = << foo bar extra key test with note baz baz baz buz XLIFF; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertSame([], libxml_get_errors()); $this->assertContainsOnlyString($catalogue->all('domain1')); $this->assertSame(['foo', 'extra', 'key', 'test'], array_keys($catalogue->all('domain1'))); } public function testLoadWithInternalErrorsEnabled() { $internalErrors = libxml_use_internal_errors(true); $this->assertSame([], libxml_get_errors()); $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); libxml_clear_errors(); libxml_use_internal_errors($internalErrors); } public function testLoadWithExternalEntitiesDisabled() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadWithResname() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/resname.xlf', 'en', 'domain1'); $this->assertEquals(['foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo', 'qux' => 'qux source'], $catalogue->all('domain1')); } public function testIncompleteResource() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/resources.xlf', 'en', 'domain1'); $this->assertEquals(['foo' => 'bar', 'extra' => 'extra', 'key' => '', 'test' => 'with'], $catalogue->all('domain1')); } public function testEncoding() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/encoding.xlf', 'en', 'domain1'); $this->assertEquals(mb_convert_encoding('föö', 'ISO-8859-1', 'UTF-8'), $catalogue->get('bar', 'domain1')); $this->assertEquals(mb_convert_encoding('bär', 'ISO-8859-1', 'UTF-8'), $catalogue->get('foo', 'domain1')); $this->assertEquals( [ 'source' => 'foo', 'notes' => [['content' => mb_convert_encoding('bäz', 'ISO-8859-1', 'UTF-8')]], 'id' => '1', 'file' => [ 'original' => 'file.ext', ], ], $catalogue->getMetadata('foo', 'domain1') ); } public function testTargetAttributesAreStoredCorrectly() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/with-attributes.xlf', 'en', 'domain1'); $metadata = $catalogue->getMetadata('foo', 'domain1'); $this->assertEquals('translated', $metadata['target-attributes']['state']); } public function testLoadInvalidResource() { $this->expectException(InvalidResourceException::class); (new XliffFileLoader())->load(__DIR__.'/../Fixtures/resources.php', 'en', 'domain1'); } public function testLoadResourceDoesNotValidate() { $this->expectException(InvalidResourceException::class); (new XliffFileLoader())->load(__DIR__.'/../Fixtures/non-valid.xlf', 'en', 'domain1'); } public function testLoadNonExistingResource() { $this->expectException(NotFoundResourceException::class); (new XliffFileLoader())->load(__DIR__.'/../Fixtures/non-existing.xlf', 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { $this->expectException(InvalidResourceException::class); (new XliffFileLoader())->load('http://example.com/resources.xlf', 'en', 'domain1'); } public function testDocTypeIsNotAllowed() { $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage('Document types are not allowed.'); (new XliffFileLoader())->load(__DIR__.'/../Fixtures/withdoctype.xlf', 'en', 'domain1'); } public function testParseEmptyFile() { $resource = __DIR__.'/../Fixtures/empty.xlf'; $this->expectException(InvalidResourceException::class); $this->expectExceptionMessage(\sprintf('Unable to load "%s":', $resource)); (new XliffFileLoader())->load($resource, 'en', 'domain1'); } public function testLoadNotes() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/withnote.xlf', 'en', 'domain1'); $this->assertEquals( [ 'source' => 'foo', 'notes' => [['priority' => 1, 'content' => 'foo']], 'id' => '1', 'file' => [ 'original' => 'file.ext', ], ], $catalogue->getMetadata('foo', 'domain1') ); // message without target $this->assertEquals( [ 'source' => 'extrasource', 'notes' => [['content' => 'bar', 'from' => 'foo']], 'id' => '2', 'file' => [ 'original' => 'file.ext', ], ], $catalogue->getMetadata('extra', 'domain1') ); // message with empty target $this->assertEquals( [ 'source' => 'key', 'notes' => [ ['content' => 'baz'], ['priority' => 2, 'from' => 'bar', 'content' => 'qux'], ], 'id' => '123', 'file' => [ 'original' => 'file.ext', ], ], $catalogue->getMetadata('key', 'domain1') ); } public function testLoadVersion2() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-2.0.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); $domains = $catalogue->all(); $this->assertCount(3, $domains['domain1']); $this->assertContainsOnlyString($catalogue->all('domain1')); // target attributes $this->assertEquals(['target-attributes' => ['order' => 1]], $catalogue->getMetadata('bar', 'domain1')); } public function testLoadVersion21() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-2.1.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); $domains = $catalogue->all(); $this->assertCount(3, $domains['domain1']); $this->assertContainsOnlyString($catalogue->all('domain1')); // target attributes $this->assertEquals(['target-attributes' => ['order' => 1]], $catalogue->getMetadata('bar', 'domain1')); } public function testLoadVersion22() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-2.2.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); $domains = $catalogue->all(); $this->assertCount(3, $domains['domain1']); $this->assertContainsOnlyString($catalogue->all('domain1')); // target attributes $this->assertEquals(['target-attributes' => ['order' => 1]], $catalogue->getMetadata('bar', 'domain1')); } public function testLoadVersion2WithNoteMeta() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-notes-meta.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); $this->assertSame([], libxml_get_errors()); // test for "foo" metadata $this->assertTrue($catalogue->defines('foo', 'domain1')); $metadata = $catalogue->getMetadata('foo', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(3, $metadata['notes']); $this->assertEquals('state', $metadata['notes'][0]['category']); $this->assertEquals('new', $metadata['notes'][0]['content']); $this->assertEquals('approved', $metadata['notes'][1]['category']); $this->assertEquals('true', $metadata['notes'][1]['content']); $this->assertEquals('section', $metadata['notes'][2]['category']); $this->assertEquals('1', $metadata['notes'][2]['priority']); $this->assertEquals('user login', $metadata['notes'][2]['content']); // test for "baz" metadata $this->assertTrue($catalogue->defines('baz', 'domain1')); $metadata = $catalogue->getMetadata('baz', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(2, $metadata['notes']); $this->assertEquals('x', $metadata['notes'][0]['id']); $this->assertEquals('x_content', $metadata['notes'][0]['content']); $this->assertEquals('target', $metadata['notes'][1]['appliesTo']); $this->assertEquals('quality', $metadata['notes'][1]['category']); $this->assertEquals('Fuzzy', $metadata['notes'][1]['content']); } public function testLoadVersion2WithMultiSegmentUnit() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-2.0-multi-segment-unit.xlf'; $catalog = $loader->load($resource, 'en', 'domain1'); $this->assertSame('en', $catalog->getLocale()); $this->assertEquals([new FileResource($resource)], $catalog->getResources()); $this->assertFalse(libxml_get_last_error()); // test for "foo" metadata $this->assertTrue($catalog->defines('foo', 'domain1')); $metadata = $catalog->getMetadata('foo', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(1, $metadata['notes']); $this->assertSame('processed', $metadata['notes'][0]['category']); $this->assertSame('true', $metadata['notes'][0]['content']); // test for "bar" metadata $this->assertTrue($catalog->defines('bar', 'domain1')); $metadata = $catalog->getMetadata('bar', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(1, $metadata['notes']); $this->assertSame('processed', $metadata['notes'][0]['category']); $this->assertSame('true', $metadata['notes'][0]['content']); } public function testLoadWithMultipleFileNodes() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/resources-multi-files.xlf', 'en', 'domain1'); $this->assertEquals( [ 'source' => 'foo', 'id' => '1', 'file' => [ 'original' => 'file.ext', ], ], $catalogue->getMetadata('foo', 'domain1') ); $this->assertEquals( [ 'source' => 'test', 'notes' => [['content' => 'note']], 'id' => '4', 'file' => [ 'original' => 'otherfile.ext', ], ], $catalogue->getMetadata('test', 'domain1') ); } public function testLoadVersion2WithName() { $loader = new XliffFileLoader(); $catalogue = $loader->load(__DIR__.'/../Fixtures/resources-2.0-name.xlf', 'en', 'domain1'); $this->assertEquals(['foo' => 'bar', 'bar' => 'baz', 'baz' => 'foo', 'qux' => 'qux source'], $catalogue->all('domain1')); } public function testLoadVersion2WithSegmentAttributes() { $loader = new XliffFileLoader(); $resource = __DIR__.'/../Fixtures/resources-2.0-segment-attributes.xlf'; $catalogue = $loader->load($resource, 'en', 'domain1'); // test for "foo" metadata $this->assertTrue($catalogue->defines('foo', 'domain1')); $metadata = $catalogue->getMetadata('foo', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(1, $metadata['segment-attributes']); $this->assertArrayHasKey('state', $metadata['segment-attributes']); $this->assertSame('translated', $metadata['segment-attributes']['state']); // test for "key" metadata $this->assertTrue($catalogue->defines('key', 'domain1')); $metadata = $catalogue->getMetadata('key', 'domain1'); $this->assertNotEmpty($metadata); $this->assertCount(2, $metadata['segment-attributes']); $this->assertArrayHasKey('state', $metadata['segment-attributes']); $this->assertSame('translated', $metadata['segment-attributes']['state']); $this->assertArrayHasKey('subState', $metadata['segment-attributes']); $this->assertSame('My Value', $metadata['segment-attributes']['subState']); } public function testLoadVersion22WithPgsPlural() { $catalogue = new XliffFileLoader()->load(__DIR__.'/../Fixtures/resources-2.2-pgs-plural.xlf', 'fr', 'domain1'); $intlDomain = 'domain1'.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; $this->assertTrue($catalogue->defines('file_deleted', $intlDomain)); $this->assertSame( '{file_count, plural, =0 {Vous n\'avez supprimé aucun fichier.} =1 {Vous avez supprimé un fichier.} other {Vous avez supprimé # fichiers.}}', $catalogue->get('file_deleted', $intlDomain) ); $this->assertSame('plural:file_count', $catalogue->getMetadata('file_deleted', $intlDomain)['pgs-switch']); } public function testLoadVersion22WithPgsGender() { $catalogue = new XliffFileLoader()->load(__DIR__.'/../Fixtures/resources-2.2-pgs-gender.xlf', 'fr', 'domain1'); $intlDomain = 'domain1'.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; $this->assertTrue($catalogue->defines('party_invite', $intlDomain)); $this->assertSame( '{host_gender, select, feminine {Vous êtes invité à sa fête} masculine {Vous êtes invité à sa fête} other {Vous êtes invité à leur fête}}', $catalogue->get('party_invite', $intlDomain) ); } public function testLoadVersion22WithPgsCombined() { $catalogue = new XliffFileLoader()->load(__DIR__.'/../Fixtures/resources-2.2-pgs-combined.xlf', 'fr', 'domain1'); $intlDomain = 'domain1'.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; $this->assertTrue($catalogue->defines('party_host', $intlDomain)); $expected = <<assertSame($expected, $catalogue->get('party_host', $intlDomain)); } } ================================================ FILE: Tests/Loader/YamlFileLoaderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Loader; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\Translation\Exception\InvalidResourceException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Loader\YamlFileLoader; class YamlFileLoaderTest extends TestCase { public function testLoad() { $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/resources.yml'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonStringMessages() { $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/non-string.yml'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertSame(['root.foo2' => '', 'root.bar' => 'bar'], $catalogue->all('domain1')); } public function testLoadDoesNothingIfEmpty() { $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/empty.yml'; $catalogue = $loader->load($resource, 'en', 'domain1'); $this->assertEquals([], $catalogue->all('domain1')); $this->assertEquals('en', $catalogue->getLocale()); $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } public function testLoadNonExistingResource() { $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/non-existing.yml'; $this->expectException(NotFoundResourceException::class); $loader->load($resource, 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfFileNotLocal() { $loader = new YamlFileLoader(); $resource = 'http://example.com/resources.yml'; $this->expectException(InvalidResourceException::class); $loader->load($resource, 'en', 'domain1'); } public function testLoadThrowsAnExceptionIfNotAnArray() { $loader = new YamlFileLoader(); $resource = __DIR__.'/../Fixtures/non-valid.yml'; $this->expectException(InvalidResourceException::class); $loader->load($resource, 'en', 'domain1'); } } ================================================ FILE: Tests/LocaleFallbackProviderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\LocaleFallbackProvider; class LocaleFallbackProviderTest extends TestCase { public function testConstructorValidatesLocales() { $this->expectException(InvalidArgumentException::class); new LocaleFallbackProvider(['en', 'invalid locale!']); } public function testComputeFallbackLocalesValidatesLocale() { $this->expectException(InvalidArgumentException::class); (new LocaleFallbackProvider())->computeFallbackLocales('invalid locale!'); } public function testComputeFallbackLocalesShortensSubTags() { $provider = new LocaleFallbackProvider(); $this->assertSame(['en'], $provider->computeFallbackLocales('en_US')); } #[DataProvider('provideIcuParentLocales')] public function testComputeFallbackLocalesUsesIcuParents(string $locale, array $expected) { $provider = new LocaleFallbackProvider(); $this->assertSame($expected, $provider->computeFallbackLocales($locale)); } public static function provideIcuParentLocales(): array { return [ 'ICU root parent terminates chain' => ['az_Cyrl', []], 'ICU explicit parent chain' => ['en_150', ['en_001', 'en']], 'locale sub-tag shortening' => ['sl_Latn_IT', ['sl_Latn', 'sl']], ]; } public function testComputeFallbackLocalesAppendsUltimateFallbacks() { $provider = new LocaleFallbackProvider(['de', 'fr']); $result = $provider->computeFallbackLocales('en_US'); $this->assertSame(['en', 'de', 'fr'], $result); } public function testComputeFallbackLocalesExcludesOriginFromUltimateFallbacks() { $provider = new LocaleFallbackProvider(['en_US', 'fr']); $result = $provider->computeFallbackLocales('en_US'); $this->assertSame(['en', 'fr'], $result); } public function testComputeFallbackLocalesReturnsUniqueLocales() { $provider = new LocaleFallbackProvider(['en', 'fr']); // en_US -> en (sub-tag shortening) -> en (ultimate fallback, duplicate) $result = $provider->computeFallbackLocales('en_US'); $this->assertSame(['en', 'fr'], $result); } public function testComputeFallbackLocalesForRootIcuParentReturnsEmpty() { // az_Cyrl has ICU explicit parent 'root', meaning no fallback chain $provider = new LocaleFallbackProvider(); $this->assertSame([], $provider->computeFallbackLocales('az_Cyrl')); } #[DataProvider('provideValidLocales')] public function testValidateLocalePassesForValidLocales(string $locale) { LocaleFallbackProvider::validateLocale($locale); $this->addToAssertionCount(1); } public static function provideValidLocales(): array { return [ ['en'], ['en_US'], ['en-US'], ['fr_FR.UTF8'], ['sr@latin'], [''], ]; } #[DataProvider('provideInvalidLocales')] public function testValidateLocaleThrowsForInvalidLocales(string $locale) { $this->expectException(InvalidArgumentException::class); LocaleFallbackProvider::validateLocale($locale); } public static function provideInvalidLocales(): array { return [ ['fr FR'], ['français'], ['fr+en'], ['utf#8'], ['fr&en'], ['fr~FR'], [' fr'], ['fr '], ['fr*'], ['fr/FR'], ['fr\\FR'], ]; } } ================================================ FILE: Tests/LocaleSwitcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Translation\LocaleSwitcher; use Symfony\Contracts\Translation\LocaleAwareInterface; #[RequiresPhpExtension('intl')] class LocaleSwitcherTest extends TestCase { private string $intlLocale; protected function setUp(): void { $this->intlLocale = \Locale::getDefault(); } protected function tearDown(): void { \Locale::setDefault($this->intlLocale); } public function testCanSwitchLocale() { \Locale::setDefault('en'); $service = new DummyLocaleAware('en'); $switcher = new LocaleSwitcher('en', [$service]); $this->assertSame('en', \Locale::getDefault()); $this->assertSame('en', $service->getLocale()); $this->assertSame('en', $switcher->getLocale()); $switcher->setLocale('fr'); $this->assertSame('fr', \Locale::getDefault()); $this->assertSame('fr', $service->getLocale()); $this->assertSame('fr', $switcher->getLocale()); } public function testCanSwitchLocaleForCallback() { \Locale::setDefault('en'); $service = new DummyLocaleAware('en'); $switcher = new LocaleSwitcher('en', [$service]); $this->assertSame('en', \Locale::getDefault()); $this->assertSame('en', $service->getLocale()); $this->assertSame('en', $switcher->getLocale()); $switcher->runWithLocale('fr', function (string $locale) use ($switcher, $service) { $this->assertSame('fr', \Locale::getDefault()); $this->assertSame('fr', $service->getLocale()); $this->assertSame('fr', $switcher->getLocale()); $this->assertSame('fr', $locale); }); $this->assertSame('en', \Locale::getDefault()); $this->assertSame('en', $service->getLocale()); $this->assertSame('en', $switcher->getLocale()); } public function testWithRequestContext() { $context = new RequestContext(); $service = new LocaleSwitcher('en', [], $context); $this->assertSame('en', $service->getLocale()); $service->setLocale('fr'); $this->assertSame('fr', $service->getLocale()); $this->assertSame('fr', $context->getParameter('_locale')); $service->reset(); $this->assertSame('en', $service->getLocale()); $this->assertSame('en', $context->getParameter('_locale')); } } class DummyLocaleAware implements LocaleAwareInterface { public function __construct(private string $locale) { } public function setLocale(string $locale): void { $this->locale = $locale; } public function getLocale(): string { return $this->locale; } } ================================================ FILE: Tests/LoggingTranslatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Translation\LoggingTranslator; use Symfony\Component\Translation\Translator; class LoggingTranslatorTest extends TestCase { public function testTransWithNoTranslationIsLogged() { $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->exactly(1)) ->method('warning') ->with('Translation not found.') ; $translator = new Translator('ar'); $loggableTranslator = new LoggingTranslator($translator, $logger); $loggableTranslator->trans('bar'); } } ================================================ FILE: Tests/MessageCatalogueTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Translation\Exception\LogicException; use Symfony\Component\Translation\MessageCatalogue; class MessageCatalogueTest extends TestCase { public function testGetLocale() { $catalogue = new MessageCatalogue('en'); $this->assertEquals('en', $catalogue->getLocale()); } public function testGetDomains() { $catalogue = new MessageCatalogue('en', ['domain1' => [], 'domain2' => [], 'domain2+intl-icu' => [], 'domain3+intl-icu' => []]); $this->assertEquals(['domain1', 'domain2', 'domain3'], $catalogue->getDomains()); } public function testAll() { $catalogue = new MessageCatalogue('en', $messages = ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]); $this->assertEquals(['foo' => 'foo'], $catalogue->all('domain1')); $this->assertEquals([], $catalogue->all('domain88')); $this->assertEquals($messages, $catalogue->all()); $messages = ['domain1+intl-icu' => ['foo' => 'bar']] + $messages + [ 'domain2+intl-icu' => ['bar' => 'foo'], 'domain3+intl-icu' => ['biz' => 'biz'], ]; $catalogue = new MessageCatalogue('en', $messages); $this->assertEquals(['foo' => 'bar'], $catalogue->all('domain1')); $this->assertEquals(['bar' => 'foo'], $catalogue->all('domain2')); $this->assertEquals(['biz' => 'biz'], $catalogue->all('domain3')); $messages = [ 'domain1' => ['foo' => 'bar'], 'domain2' => ['bar' => 'foo'], 'domain3' => ['biz' => 'biz'], ]; $this->assertEquals($messages, $catalogue->all()); } public function testAllIntlIcu() { $messages = [ 'domain1+intl-icu' => ['foo' => 'bar'], 'domain2+intl-icu' => ['bar' => 'foo'], 'domain2' => ['biz' => 'biz'], ]; $catalogue = new MessageCatalogue('en', $messages); // separated domains $this->assertSame(['foo' => 'bar'], $catalogue->all('domain1+intl-icu')); $this->assertSame(['bar' => 'foo'], $catalogue->all('domain2+intl-icu')); // merged, intl-icu ignored $this->assertSame(['bar' => 'foo', 'biz' => 'biz'], $catalogue->all('domain2')); // intl-icu ignored $messagesExpected = [ 'domain1' => ['foo' => 'bar'], 'domain2' => ['bar' => 'foo', 'biz' => 'biz'], ]; $this->assertSame($messagesExpected, $catalogue->all()); } public function testHas() { $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2+intl-icu' => ['bar' => 'bar']]); $this->assertTrue($catalogue->has('foo', 'domain1')); $this->assertTrue($catalogue->has('bar', 'domain2')); $this->assertFalse($catalogue->has('bar', 'domain1')); $this->assertFalse($catalogue->has('foo', 'domain88')); } public function testGetSet() { $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar'], 'domain2+intl-icu' => ['bar' => 'foo']]); $catalogue->set('foo1', 'foo1', 'domain1'); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1')); $this->assertEquals('foo', $catalogue->get('bar', 'domain2')); } public function testAdd() { $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]); $catalogue->add(['foo1' => 'foo1'], 'domain1'); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1')); $catalogue->add(['foo' => 'bar'], 'domain1'); $this->assertEquals('bar', $catalogue->get('foo', 'domain1')); $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1')); $catalogue->add(['foo' => 'bar'], 'domain88'); $this->assertEquals('bar', $catalogue->get('foo', 'domain88')); } public function testAddIntlIcu() { $catalogue = new MessageCatalogue('en', ['domain1+intl-icu' => ['foo' => 'foo']]); $catalogue->add(['foo1' => 'foo1'], 'domain1'); $catalogue->add(['foo' => 'bar'], 'domain1'); $this->assertSame('bar', $catalogue->get('foo', 'domain1')); $this->assertSame('foo1', $catalogue->get('foo1', 'domain1')); } public function testReplace() { $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain1+intl-icu' => ['bar' => 'bar']]); $catalogue->replace($messages = ['foo1' => 'foo1'], 'domain1'); $this->assertEquals($messages, $catalogue->all('domain1')); } public function testAddCatalogue() { $r = $this->createStub(ResourceInterface::class); $r->method('__toString')->willReturn('r'); $r1 = $this->createStub(ResourceInterface::class); $r1->method('__toString')->willReturn('r1'); $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo']]); $catalogue->addResource($r); $catalogue1 = new MessageCatalogue('en', ['domain1' => ['foo1' => 'foo1'], 'domain2+intl-icu' => ['bar' => 'bar']]); $catalogue1->addResource($r1); $catalogue->addCatalogue($catalogue1); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1')); $this->assertEquals('bar', $catalogue->get('bar', 'domain2')); $this->assertEquals('bar', $catalogue->get('bar', 'domain2+intl-icu')); $this->assertEquals([$r, $r1], $catalogue->getResources()); } public function testAddFallbackCatalogue() { $r = $this->createStub(ResourceInterface::class); $r->method('__toString')->willReturn('r'); $r1 = $this->createStub(ResourceInterface::class); $r1->method('__toString')->willReturn('r1'); $r2 = $this->createStub(ResourceInterface::class); $r2->method('__toString')->willReturn('r2'); $catalogue = new MessageCatalogue('fr_FR', ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]); $catalogue->addResource($r); $catalogue1 = new MessageCatalogue('fr', ['domain1' => ['foo' => 'bar', 'foo1' => 'foo1']]); $catalogue1->addResource($r1); $catalogue2 = new MessageCatalogue('en'); $catalogue2->addResource($r2); $catalogue->addFallbackCatalogue($catalogue1); $catalogue1->addFallbackCatalogue($catalogue2); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); $this->assertEquals('foo1', $catalogue->get('foo1', 'domain1')); $this->assertEquals([$r, $r1, $r2], $catalogue->getResources()); } public function testAddFallbackCatalogueWithParentCircularReference() { $main = new MessageCatalogue('en_US'); $fallback = new MessageCatalogue('fr_FR'); $fallback->addFallbackCatalogue($main); $this->expectException(LogicException::class); $main->addFallbackCatalogue($fallback); } public function testAddFallbackCatalogueWithFallbackCircularReference() { $fr = new MessageCatalogue('fr'); $en = new MessageCatalogue('en'); $es = new MessageCatalogue('es'); $fr->addFallbackCatalogue($en); $es->addFallbackCatalogue($en); $this->expectException(LogicException::class); $en->addFallbackCatalogue($fr); } public function testAddCatalogueWhenLocaleIsNotTheSameAsTheCurrentOne() { $catalogue = new MessageCatalogue('en'); $this->expectException(LogicException::class); $catalogue->addCatalogue(new MessageCatalogue('fr', [])); } public function testGetAddResource() { $catalogue = new MessageCatalogue('en'); $r = $this->createStub(ResourceInterface::class); $r->method('__toString')->willReturn('r'); $catalogue->addResource($r); $catalogue->addResource($r); $r1 = $this->createStub(ResourceInterface::class); $r1->method('__toString')->willReturn('r1'); $catalogue->addResource($r1); $this->assertEquals([$r, $r1], $catalogue->getResources()); } public function testMetadataDelete() { $catalogue = new MessageCatalogue('en'); $this->assertEquals([], $catalogue->getMetadata('', ''), 'Metadata is empty'); $catalogue->deleteMetadata('key', 'messages'); $catalogue->deleteMetadata('', 'messages'); $catalogue->deleteMetadata(); } public function testMetadataSetGetDelete() { $catalogue = new MessageCatalogue('en'); $catalogue->setMetadata('key', 'value'); $this->assertEquals('value', $catalogue->getMetadata('key', 'messages'), "Metadata 'key' = 'value'"); $catalogue->setMetadata('key2', []); $this->assertEquals([], $catalogue->getMetadata('key2', 'messages'), 'Metadata key2 is array'); $catalogue->deleteMetadata('key2', 'messages'); $this->assertNull($catalogue->getMetadata('key2', 'messages'), 'Metadata key2 should is deleted.'); $catalogue->deleteMetadata('key2', 'domain'); $this->assertNull($catalogue->getMetadata('key2', 'domain'), 'Metadata key2 should is deleted.'); } public function testMetadataMerge() { $cat1 = new MessageCatalogue('en'); $cat1->setMetadata('a', 'b'); $this->assertEquals(['messages' => ['a' => 'b']], $cat1->getMetadata('', ''), 'Cat1 contains messages metadata.'); $cat2 = new MessageCatalogue('en'); $cat2->setMetadata('b', 'c', 'domain'); $this->assertEquals(['domain' => ['b' => 'c']], $cat2->getMetadata('', ''), 'Cat2 contains domain metadata.'); $cat1->addCatalogue($cat2); $this->assertEquals(['messages' => ['a' => 'b'], 'domain' => ['b' => 'c']], $cat1->getMetadata('', ''), 'Cat1 contains merged metadata.'); } } ================================================ FILE: Tests/Provider/DsnTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Provider; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\MissingRequiredOptionException; use Symfony\Component\Translation\Provider\Dsn; final class DsnTest extends TestCase { #[DataProvider('constructProvider')] public function testConstruct(string $dsnString, string $scheme, string $host, ?string $user = null, ?string $password = null, ?int $port = null, array $options = [], ?string $path = null) { $dsn = new Dsn($dsnString); $this->assertSame($dsnString, $dsn->getOriginalDsn()); $this->assertSame($scheme, $dsn->getScheme()); $this->assertSame($host, $dsn->getHost()); $this->assertSame($user, $dsn->getUser()); $this->assertSame($password, $dsn->getPassword()); $this->assertSame($port, $dsn->getPort()); $this->assertSame($path, $dsn->getPath()); $this->assertSame($options, $dsn->getOptions()); } public static function constructProvider(): iterable { yield 'simple dsn' => [ 'scheme://localhost', 'scheme', 'localhost', ]; yield 'simple dsn including @ sign, but no user/password/token' => [ 'scheme://@localhost', 'scheme', 'localhost', ]; yield 'simple dsn including : sign and @ sign, but no user/password/token' => [ 'scheme://:@localhost', 'scheme', 'localhost', ]; yield 'simple dsn including user, : sign and @ sign, but no password' => [ 'scheme://user1:@localhost', 'scheme', 'localhost', 'user1', ]; yield 'simple dsn including : sign, password, and @ sign, but no user' => [ 'scheme://:pass@localhost', 'scheme', 'localhost', null, 'pass', ]; yield 'dsn with user and pass' => [ 'scheme://u$er:pa$s@localhost', 'scheme', 'localhost', 'u$er', 'pa$s', ]; yield 'dsn with user and pass and custom port' => [ 'scheme://u$er:pa$s@localhost:8000', 'scheme', 'localhost', 'u$er', 'pa$s', 8000, ]; yield 'dsn with user and pass, custom port and custom path' => [ 'scheme://u$er:pa$s@localhost:8000/channel', 'scheme', 'localhost', 'u$er', 'pa$s', 8000, [], '/channel', ]; yield 'dsn with user and pass, custom port, custom path and custom option' => [ 'scheme://u$er:pa$s@localhost:8000/channel?from=FROM', 'scheme', 'localhost', 'u$er', 'pa$s', 8000, [ 'from' => 'FROM', ], '/channel', ]; yield 'dsn with user and pass, custom port, custom path and custom options' => [ 'scheme://u$er:pa$s@localhost:8000/channel?from=FROM&to=TO', 'scheme', 'localhost', 'u$er', 'pa$s', 8000, [ 'from' => 'FROM', 'to' => 'TO', ], '/channel', ]; yield 'dsn with user and pass, custom port, custom path and custom options and custom options keep the same order' => [ 'scheme://u$er:pa$s@localhost:8000/channel?to=TO&from=FROM', 'scheme', 'localhost', 'u$er', 'pa$s', 8000, [ 'to' => 'TO', 'from' => 'FROM', ], '/channel', ]; } #[DataProvider('invalidDsnProvider')] public function testInvalidDsn(string $dsnString, string $exceptionMessage) { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage($exceptionMessage); new Dsn($dsnString); } public static function invalidDsnProvider(): iterable { yield [ 'some://', 'The translation provider DSN is invalid.', ]; yield [ '//loco', 'The translation provider DSN must contain a scheme.', ]; yield [ 'file:///some/path', 'The translation provider DSN must contain a host (use "default" by default).', ]; } #[DataProvider('getOptionProvider')] public function testGetOption($expected, string $dsnString, string $option, ?string $default = null) { $dsn = new Dsn($dsnString); $this->assertSame($expected, $dsn->getOption($option, $default)); } public static function getOptionProvider(): iterable { yield [ 'foo', 'scheme://localhost?with_value=foo', 'with_value', ]; yield [ '', 'scheme://localhost?empty=', 'empty', ]; yield [ '0', 'scheme://localhost?zero=0', 'zero', ]; yield [ 'default-value', 'scheme://localhost?option=value', 'non_existent_property', 'default-value', ]; } #[DataProvider('getRequiredOptionProvider')] public function testGetRequiredOption(string $expectedValue, string $options, string $option) { $dsn = new Dsn(\sprintf('scheme://localhost?%s', $options)); $this->assertSame($expectedValue, $dsn->getRequiredOption($option)); } public static function getRequiredOptionProvider(): iterable { yield [ 'value', 'with_value=value', 'with_value', ]; yield [ '0', 'timeout=0', 'timeout', ]; } #[DataProvider('getRequiredOptionThrowsMissingRequiredOptionExceptionProvider')] public function testGetRequiredOptionThrowsMissingRequiredOptionException(string $expectedExceptionMessage, string $options, string $option) { $dsn = new Dsn(\sprintf('scheme://localhost?%s', $options)); $this->expectException(MissingRequiredOptionException::class); $this->expectExceptionMessage($expectedExceptionMessage); $dsn->getRequiredOption($option); } public static function getRequiredOptionThrowsMissingRequiredOptionExceptionProvider(): iterable { yield [ 'The option "foo_bar" is required but missing.', 'with_value=value', 'foo_bar', ]; yield [ 'The option "with_empty_string" is required but missing.', 'with_empty_string=', 'with_empty_string', ]; } } ================================================ FILE: Tests/Provider/FilteringProviderTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Provider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Provider\FilteringProvider; use Symfony\Component\Translation\Provider\ProviderInterface; use Symfony\Component\Translation\TranslatorBag; class FilteringProviderTest extends TestCase { public function testReadDelegatesWithFilteredLocales() { $innerProvider = $this->createMock(ProviderInterface::class); $expectedBag = new TranslatorBag(); $innerProvider->expects($this->once()) ->method('read') ->with(['messages'], ['en', 'fr']) ->willReturn($expectedBag); $filteringProvider = new FilteringProvider( $innerProvider, ['en', 'fr', null, ''], ['messages', 'validators'] ); $result = $filteringProvider->read(['messages', 'custom'], ['', null, 'en', 'fr']); $this->assertSame($expectedBag, $result); } } ================================================ FILE: Tests/Provider/NullProviderFactoryTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Provider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\UnsupportedSchemeException; use Symfony\Component\Translation\Provider\Dsn; use Symfony\Component\Translation\Provider\NullProvider; use Symfony\Component\Translation\Provider\NullProviderFactory; /** * @author Mathieu Santostefano */ class NullProviderFactoryTest extends TestCase { public function testCreateThrowsUnsupportedSchemeException() { $this->expectException(UnsupportedSchemeException::class); (new NullProviderFactory())->create(new Dsn('foo://localhost')); } public function testCreate() { $this->assertInstanceOf(NullProvider::class, (new NullProviderFactory())->create(new Dsn('null://null'))); } } ================================================ FILE: Tests/Provider/TranslationProviderCollectionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Provider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Provider\NullProvider; use Symfony\Component\Translation\Provider\TranslationProviderCollection; class TranslationProviderCollectionTest extends TestCase { public function testKeys() { $this->assertSame(['foo', 'baz'], $this->createProviderCollection()->keys()); } public function testKeysWithGenerator() { $this->assertSame(['foo', 'baz'], (new TranslationProviderCollection( (static function () { yield 'foo' => new NullProvider(); yield 'baz' => new NullProvider(); })() ))->keys()); } public function testToString() { $this->assertSame('[foo,baz]', (string) $this->createProviderCollection()); } public function testHas() { $this->assertTrue($this->createProviderCollection()->has('foo')); } public function testGet() { $provider = new NullProvider(); $this->assertSame($provider, (new TranslationProviderCollection([ 'foo' => $provider, 'baz' => new NullProvider(), ]))->get('foo')); } public function testGetThrowsException() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Provider "invalid" not found. Available: "[foo,baz]".'); $this->createProviderCollection()->get('invalid'); } private function createProviderCollection(): TranslationProviderCollection { return new TranslationProviderCollection([ 'foo' => new NullProvider(), 'baz' => new NullProvider(), ]); } } ================================================ FILE: Tests/PseudoLocalizationTranslatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Translation\PseudoLocalizationTranslator; final class PseudoLocalizationTranslatorTest extends TestCase { #[DataProvider('provideTrans')] public function testTrans(string $expected, string $input, array $options = []) { mt_srand(987); $this->assertSame($expected, (new PseudoLocalizationTranslator(new IdentityTranslator(), $options))->trans($input)); } public static function provideTrans(): array { return [ ['[ƒöö⭐ ≤þ≥ƁÅŔ≤⁄þ≥]', 'foo⭐

BAR

'], // Test defaults ['before after', 'before after', self::getIsolatedOptions(['parse_html' => true])], ['ƀéƒöŕé  åƒţéŕ', 'before after', self::getIsolatedOptions(['parse_html' => true, 'accents' => true])], ['ƀéƒöŕé  åƒţéŕ', 'before after', self::getIsolatedOptions(['parse_html' => true, 'localizable_html_attributes' => ['data-label', 'title'], 'accents' => true])], [' ¡″♯€‰⅋´{}⁎⁺،‐·⁄⓪①②③④⑤⑥⑦⑧⑨∶⁏≤≂≥¿՞ÅƁÇÐÉƑĜĤÎĴĶĻṀÑÖÞǪŔŠŢÛṼŴẊÝŽ⁅∖⁆˄‿‵åƀçðéƒĝĥîĵķļɱñöþǫŕšţûṽŵẋýž(¦)˞', ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~', self::getIsolatedOptions(['accents' => true])], ['foo

bar

~~~~~~~~~~ ~~', 'foo

bar

', self::getIsolatedOptions(['expansion_factor' => 2.0])], ['foo

bar

~~~ ~~', 'foo

bar

', self::getIsolatedOptions(['parse_html' => true, 'expansion_factor' => 2.0])], // Only the visible text length is expanded ['foobar ~~', 'foobar', self::getIsolatedOptions(['expansion_factor' => 1.35])], // 6*1.35 = 8.1 but we round up to 9 ['[foobar]', 'foobar', self::getIsolatedOptions(['brackets' => true])], ['[foobar ~~~]', 'foobar', self::getIsolatedOptions(['expansion_factor' => 2.0, 'brackets' => true])], // The added brackets are taken into account in the expansion ['

ƀåŕ

', '

bar

', self::getIsolatedOptions(['parse_html' => true, 'localizable_html_attributes' => ['data-foo'], 'accents' => true])], ['

ƀåéŕ

', '

baér

', self::getIsolatedOptions(['parse_html' => true, 'localizable_html_attributes' => ['data-foo'], 'accents' => true])], ['

ƀåŕ

', '

bar

', self::getIsolatedOptions(['parse_html' => true, 'accents' => true])], ['

″≤″

', '

"<"

', self::getIsolatedOptions(['parse_html' => true, 'accents' => true])], ['Symfony is an Open Source, community-driven project with thousands of contributors. ~~~~~~~ ~~ ~~~~ ~~~~~~~ ~~~~~~~ ~~ ~~~~ ~~~~~~~~~~~~~ ~~~~~~~~~~~~~ ~~~~~~~ ~~ ~~~', 'Symfony is an Open Source, community-driven project with thousands of contributors.', self::getIsolatedOptions(['expansion_factor' => 2.0])], ['

👇👇👇👇👇👇👇

', '

👇👇👇👇👇👇👇

', self::getIsolatedOptions(['parse_html' => true])], ]; } #[DataProvider('provideInvalidExpansionFactor')] public function testInvalidExpansionFactor(float $expansionFactor) { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The expansion factor must be greater than or equal to 1.'); new PseudoLocalizationTranslator(new IdentityTranslator(), [ 'expansion_factor' => $expansionFactor, ]); } public static function provideInvalidExpansionFactor(): array { return [ [0], [0.99], [-1], ]; } private static function getIsolatedOptions(array $options): array { return array_replace([ 'parse_html' => false, 'localizable_html_attributes' => [], 'accents' => false, 'expansion_factor' => 1.0, 'brackets' => false, ], $options); } } // @php-cs-fixer-ignore random_api_migration As logic is coupled with mt_rand() in src ================================================ FILE: Tests/StaticMessageTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\StaticMessage; use Symfony\Component\Translation\Translator; class StaticMessageTest extends TestCase { public function testTrans() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'Symfony is great!' => 'Symfony est super !', ], 'fr', ''); $translatable = new StaticMessage('Symfony is great!'); $this->assertSame('Symfony is great!', $translatable->trans($translator, 'fr')); } } ================================================ FILE: Tests/TranslatableTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Translation\Translator; class TranslatableTest extends TestCase { #[DataProvider('getTransTests')] public function testTrans(string $expected, TranslatableMessage $translatable, array $translation, string $locale) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', $translation, $locale, $translatable->getDomain()); $this->assertSame($expected, $translatable->trans($translator, $locale)); } #[DataProvider('getFlattenedTransTests')] public function testFlattenedTrans($expected, $messages, $translatable) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', $messages, 'fr', ''); $this->assertSame($expected, $translatable->trans($translator, 'fr')); } public static function getTransTests() { return [ ['Symfony est super !', new TranslatableMessage('Symfony is great!', [], ''), [ 'Symfony is great!' => 'Symfony est super !', ], 'fr'], ['Symfony est awesome !', new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'], ''), [ 'Symfony is %what%!' => 'Symfony est %what% !', ], 'fr'], ['Symfony est superbe !', new TranslatableMessage('Symfony is %what%!', ['%what%' => new TranslatableMessage('awesome', [], '')], ''), [ 'Symfony is %what%!' => 'Symfony est %what% !', 'awesome' => 'superbe', ], 'fr'], ]; } public static function getFlattenedTransTests() { $messages = [ 'symfony' => [ 'is' => [ 'great' => 'Symfony est super!', ], ], 'foo' => [ 'bar' => [ 'baz' => 'Foo Bar Baz', ], 'baz' => 'Foo Baz', ], ]; return [ ['Symfony est super!', $messages, new TranslatableMessage('symfony.is.great', [], '')], ['Foo Bar Baz', $messages, new TranslatableMessage('foo.bar.baz', [], '')], ['Foo Baz', $messages, new TranslatableMessage('foo.baz', [], '')], ]; } } ================================================ FILE: Tests/TranslatorBagTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\TranslatorBag; class TranslatorBagTest extends TestCase { public function testAll() { $catalogue = new MessageCatalogue('en', $messages = ['domain1' => ['foo' => 'foo'], 'domain2' => ['bar' => 'bar']]); $bag = new TranslatorBag(); $bag->addCatalogue($catalogue); $this->assertEquals(['en' => $messages], $this->getAllMessagesFromTranslatorBag($bag)); $messages = ['domain1+intl-icu' => ['foo' => 'bar']] + $messages + [ 'domain2+intl-icu' => ['bar' => 'foo'], 'domain3+intl-icu' => ['biz' => 'biz'], ]; $catalogue = new MessageCatalogue('en', $messages); $bag = new TranslatorBag(); $bag->addCatalogue($catalogue); $this->assertEquals([ 'en' => [ 'domain1' => ['foo' => 'bar'], 'domain2' => ['bar' => 'foo'], 'domain3' => ['biz' => 'biz'], ], ], $this->getAllMessagesFromTranslatorBag($bag)); } public function testDiff() { $catalogueA = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'bar' => 'bar'], 'domain2' => ['baz' => 'baz', 'qux' => 'qux']]); $bagA = new TranslatorBag(); $bagA->addCatalogue($catalogueA); $catalogueB = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2' => ['baz' => 'baz', 'corge' => 'corge']]); $bagB = new TranslatorBag(); $bagB->addCatalogue($catalogueB); $bagResult = $bagA->diff($bagB); $this->assertEquals([ 'en' => [ 'domain1' => ['bar' => 'bar'], 'domain2' => ['qux' => 'qux'], ], ], $this->getAllMessagesFromTranslatorBag($bagResult)); } public function testDiffWithIntlDomain() { $catalogueA = new MessageCatalogue('en', [ 'domain1+intl-icu' => ['foo' => 'foo', 'bar' => 'bar'], 'domain2' => ['baz' => 'baz', 'qux' => 'qux'], ]); $bagA = new TranslatorBag(); $bagA->addCatalogue($catalogueA); $catalogueB = new MessageCatalogue('en', [ 'domain1' => ['foo' => 'foo'], 'domain2' => ['baz' => 'baz', 'corge' => 'corge'], ]); $bagB = new TranslatorBag(); $bagB->addCatalogue($catalogueB); $bagResult = $bagA->diff($bagB); $this->assertEquals([ 'en' => [ 'domain1' => ['bar' => 'bar'], 'domain2' => ['qux' => 'qux'], ], ], $this->getAllMessagesFromTranslatorBag($bagResult)); } public function testIntersect() { $catalogueA = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'bar' => 'bar'], 'domain2' => ['baz' => 'baz', 'qux' => 'qux']]); $bagA = new TranslatorBag(); $bagA->addCatalogue($catalogueA); $catalogueB = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo', 'baz' => 'baz'], 'domain2' => ['baz' => 'baz', 'corge' => 'corge']]); $bagB = new TranslatorBag(); $bagB->addCatalogue($catalogueB); $bagResult = $bagA->intersect($bagB); $this->assertEquals([ 'en' => [ 'domain1' => ['foo' => 'foo'], 'domain2' => ['baz' => 'baz'], ], ], $this->getAllMessagesFromTranslatorBag($bagResult)); } private function getAllMessagesFromTranslatorBag(TranslatorBag $translatorBag): array { $allMessages = []; foreach ($translatorBag->getCatalogues() as $catalogue) { $allMessages[$catalogue->getLocale()] = $catalogue->all(); } return $allMessages; } } ================================================ FILE: Tests/TranslatorCacheTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Resource\SelfCheckingResourceInterface; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Translator; class TranslatorCacheTest extends TestCase { protected string $tmpDir; protected function setUp(): void { $this->tmpDir = sys_get_temp_dir().'/sf_translation'; $this->deleteTmpDir(); } protected function tearDown(): void { $this->deleteTmpDir(); } protected function deleteTmpDir() { if (!file_exists($dir = $this->tmpDir)) { return; } $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->tmpDir), \RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $path) { if (preg_match('#[/\\\\]\.\.?$#', $path->__toString())) { continue; } if ($path->isDir()) { @rmdir($path->__toString()); } else { @unlink($path->__toString()); } } rmdir($this->tmpDir); } #[DataProvider('runForDebugAndProduction')] public function testThatACacheIsUsed($debug) { if (!class_exists(\MessageFormatter::class)) { $this->markTestSkipped(\sprintf('Skipping test as the required "%s" class does not exist. Consider installing the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.', \MessageFormatter::class)); } $locale = 'any_locale'; $format = 'some_format'; $msgid = 'test'; // Prime the cache $translator = new Translator($locale, null, $this->tmpDir, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, [$msgid => 'OK'], $locale); $translator->addResource($format, [$msgid.'+intl' => 'OK'], $locale, 'messages+intl-icu'); $translator->trans($msgid); $translator->trans($msgid.'+intl', [], 'messages+intl-icu'); // Try again and see we get a valid result whilst no loader can be used $translator = new Translator($locale, null, $this->tmpDir, $debug); $translator->addLoader($format, $this->createFailingLoader()); $translator->addResource($format, [$msgid => 'OK'], $locale); $translator->addResource($format, [$msgid.'+intl' => 'OK'], $locale, 'messages+intl-icu'); $this->assertEquals('OK', $translator->trans($msgid), '-> caching does not work in '.($debug ? 'debug' : 'production')); $this->assertEquals('OK', $translator->trans($msgid.'+intl', [], 'messages+intl-icu')); } public function testCatalogueIsReloadedWhenResourcesAreNoLongerFresh() { /* * The testThatACacheIsUsed() test showed that we don't need the loader as long as the cache * is fresh. * * Now we add a Resource that is never fresh and make sure that the * cache is discarded (the loader is called twice). * * We need to run this for debug=true only because in production the cache * will never be revalidated. */ $locale = 'any_locale'; $format = 'some_format'; $msgid = 'test'; $catalogue = new MessageCatalogue($locale, []); $catalogue->addResource(new StaleResource()); // better use a helper class than a mock, because it gets serialized in the cache and re-loaded /** @var MockObject&LoaderInterface $loader */ $loader = $this->createMock(LoaderInterface::class); $loader ->expects($this->exactly(2)) ->method('load') ->willReturn($catalogue) ; // 1st pass $translator = new Translator($locale, null, $this->tmpDir, true); $translator->addLoader($format, $loader); $translator->addResource($format, null, $locale); $translator->trans($msgid); // 2nd pass $translator = new Translator($locale, null, $this->tmpDir, true); $translator->addLoader($format, $loader); $translator->addResource($format, null, $locale); $translator->trans($msgid); } #[DataProvider('runForDebugAndProduction')] public function testDifferentTranslatorsForSameLocaleDoNotOverwriteEachOthersCache($debug) { /* * Similar to the previous test. After we used the second translator, make * sure there's still a usable cache for the first one. */ $locale = 'any_locale'; $format = 'some_format'; $msgid = 'test'; // Create a Translator and prime its cache $translator = new Translator($locale, null, $this->tmpDir, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, [$msgid => 'OK'], $locale); $translator->trans($msgid); // Create another Translator with a different catalogue for the same locale $translator = new Translator($locale, null, $this->tmpDir, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, [$msgid => 'FAIL'], $locale); $translator->trans($msgid); // Now the first translator must still have a usable cache. $translator = new Translator($locale, null, $this->tmpDir, $debug); $translator->addLoader($format, $this->createFailingLoader()); $translator->addResource($format, [$msgid => 'OK'], $locale); $this->assertEquals('OK', $translator->trans($msgid), '-> the cache was overwritten by another translator instance in '.($debug ? 'debug' : 'production')); } public function testGeneratedCacheFilesAreOnlyBelongRequestedLocales() { $translator = new Translator('a', null, $this->tmpDir); $translator->setFallbackLocales(['b']); $translator->trans('bar'); $cachedFiles = glob($this->tmpDir.'/*.php'); $this->assertCount(1, $cachedFiles); } public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales() { /* * Because the cache file contains a catalogue including all of its fallback * catalogues, we must take the set of fallback locales into consideration when * loading a catalogue from the cache. */ $translator = new Translator('a', null, $this->tmpDir); $translator->setFallbackLocales(['b']); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (a)'], 'a'); $translator->addResource('array', ['bar' => 'bar (b)'], 'b'); $this->assertEquals('bar (b)', $translator->trans('bar')); // Remove fallback locale $translator->setFallbackLocales([]); $this->assertEquals('bar', $translator->trans('bar')); // Use a fresh translator with no fallback locales, result should be the same $translator = new Translator('a', null, $this->tmpDir); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (a)'], 'a'); $translator->addResource('array', ['bar' => 'bar (b)'], 'b'); $this->assertEquals('bar', $translator->trans('bar')); } public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardlessOfCaching() { /* * As a safeguard against potential BC breaks, make sure that primary and fallback * catalogues (reachable via getFallbackCatalogue()) always contain the full set of * messages provided by the loader. This must also be the case when these catalogues * are (internally) read from a cache. * * Optimizations inside the translator must not change this behavior. */ /* * Create a translator that loads two catalogues for two different locales. * The catalogues contain distinct sets of messages. */ $translator = new Translator('a', null, $this->tmpDir); $translator->setFallbackLocales(['b']); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (a)'], 'a'); $translator->addResource('array', ['foo' => 'foo (b)'], 'b'); $translator->addResource('array', ['bar' => 'bar (b)'], 'b'); $translator->addResource('array', ['baz' => 'baz (b)'], 'b', 'messages+intl-icu'); $catalogue = $translator->getCatalogue('a'); $this->assertFalse($catalogue->defines('bar')); // Sure, the "a" catalogue does not contain that message. $fallback = $catalogue->getFallbackCatalogue(); $this->assertTrue($fallback->defines('foo')); // "foo" is present in "a" and "b" /* * Now, repeat the same test. * Behind the scenes, the cache is used. But that should not matter, right? */ $translator = new Translator('a', null, $this->tmpDir); $translator->setFallbackLocales(['b']); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (a)'], 'a'); $translator->addResource('array', ['foo' => 'foo (b)'], 'b'); $translator->addResource('array', ['bar' => 'bar (b)'], 'b'); $translator->addResource('array', ['baz' => 'baz (b)'], 'b', 'messages+intl-icu'); $catalogue = $translator->getCatalogue('a'); $this->assertFalse($catalogue->defines('bar')); $fallback = $catalogue->getFallbackCatalogue(); $this->assertTrue($fallback->defines('foo')); $this->assertTrue($fallback->defines('baz', 'messages+intl-icu')); } public function testRefreshCacheWhenResourcesAreNoLongerFresh() { $resource = $this->createStub(SelfCheckingResourceInterface::class); $loader = $this->createMock(LoaderInterface::class); $resource->method('isFresh')->willReturn(false); $loader ->expects($this->exactly(2)) ->method('load') ->willReturn($this->getCatalogue('fr', [], [$resource])); // prime the cache $translator = new Translator('fr', null, $this->tmpDir, true); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'foo', 'fr'); $translator->trans('foo'); // prime the cache second time $translator = new Translator('fr', null, $this->tmpDir, true); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'foo', 'fr'); $translator->trans('foo'); } public function testCachedCatalogueIsReDumpedWhenCacheVaryChange() { $translator = new Translator('a', null, $this->tmpDir, false, []); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'bar'], 'a', 'messages'); // Cached catalogue is dumped $this->assertSame('bar', $translator->trans('foo', [], 'messages', 'a')); $translator = new Translator('a', null, $this->tmpDir, false, ['vary']); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'ccc'], 'a', 'messages'); $this->assertSame('ccc', $translator->trans('foo', [], 'messages', 'a')); } protected function getCatalogue($locale, $messages, $resources = []) { $catalogue = new MessageCatalogue($locale); foreach ($messages as $key => $translation) { $catalogue->set($key, $translation); } foreach ($resources as $resource) { $catalogue->addResource($resource); } return $catalogue; } public static function runForDebugAndProduction() { return [[true], [false]]; } private function createFailingLoader(): LoaderInterface { $loader = $this->createMock(LoaderInterface::class); $loader ->expects($this->never()) ->method('load'); return $loader; } } class StaleResource implements SelfCheckingResourceInterface { public function isFresh(int $timestamp): bool { return false; } public function getResource() { } public function __toString(): string { return ''; } } ================================================ FILE: Tests/TranslatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\YamlFileLoader; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\TranslatableMessage; use Symfony\Component\Translation\Translator; class TranslatorTest extends TestCase { private string $defaultLocale; protected function setUp(): void { $this->defaultLocale = \Locale::getDefault(); \Locale::setDefault('en'); } protected function tearDown(): void { \Locale::setDefault($this->defaultLocale); } #[DataProvider('getInvalidLocalesTests')] public function testConstructorInvalidLocale($locale) { $this->expectException(InvalidArgumentException::class); new Translator($locale); } #[DataProvider('getValidLocalesTests')] public function testConstructorValidLocale($locale) { $translator = new Translator($locale); $this->assertSame($locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'), $translator->getLocale()); } public function testSetGetLocale() { $translator = new Translator('en'); $this->assertEquals('en', $translator->getLocale()); $translator->setLocale('fr'); $this->assertEquals('fr', $translator->getLocale()); } #[DataProvider('getInvalidLocalesTests')] public function testSetInvalidLocale(string $locale) { $translator = new Translator('fr'); $this->expectException(InvalidArgumentException::class); $translator->setLocale($locale); } #[DataProvider('getValidLocalesTests')] public function testSetValidLocale(string $locale) { $translator = new Translator($locale); $translator->setLocale($locale); $this->assertEquals($locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'), $translator->getLocale()); } public function testGetCatalogue() { $translator = new Translator('en'); $this->assertEquals(new MessageCatalogue('en'), $translator->getCatalogue()); $translator->setLocale('fr'); $this->assertEquals(new MessageCatalogue('fr'), $translator->getCatalogue('fr')); } public function testGetCatalogueReturnsConsolidatedCatalogue() { /* * This will be useful once we refactor so that different domains will be loaded lazily (on-demand). * In that case, getCatalogue() will probably have to load all missing domains in order to return * one complete catalogue. */ $locale = 'whatever'; $translator = new Translator($locale); $translator->addLoader('loader-a', new ArrayLoader()); $translator->addLoader('loader-b', new ArrayLoader()); $translator->addResource('loader-a', ['foo' => 'foofoo'], $locale, 'domain-a'); $translator->addResource('loader-b', ['bar' => 'foobar'], $locale, 'domain-b'); /* * Test that we get a single catalogue comprising messages * from different loaders and different domains */ $catalogue = $translator->getCatalogue($locale); $this->assertTrue($catalogue->defines('foo', 'domain-a')); $this->assertTrue($catalogue->defines('bar', 'domain-b')); } public function testSetFallbackLocales() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); $translator->addResource('array', ['bar' => 'foobar'], 'fr'); // force catalogue loading $translator->trans('bar'); $translator->setFallbackLocales(['fr']); $this->assertEquals('foobar', $translator->trans('bar')); } public function testSetFallbackLocalesMultiple() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (en)'], 'en'); $translator->addResource('array', ['bar' => 'bar (fr)'], 'fr'); // force catalogue loading $translator->trans('bar'); $translator->setFallbackLocales(['fr_FR', 'fr']); $this->assertEquals('bar (fr)', $translator->trans('bar')); } #[DataProvider('getInvalidLocalesTests')] public function testSetFallbackInvalidLocales($locale) { $this->expectException(InvalidArgumentException::class); $translator = new Translator('fr'); $translator->setFallbackLocales(['fr', $locale]); } #[DataProvider('getValidLocalesTests')] public function testSetFallbackValidLocales($locale) { $translator = new Translator($locale); $translator->setFallbackLocales(['fr', $locale]); // no assertion. this method just asserts that no exception is thrown $this->addToAssertionCount(1); } public function testTransWithFallbackLocale() { $translator = new Translator('fr_FR'); $translator->setFallbackLocales(['en']); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['bar' => 'foobar'], 'en'); $this->assertEquals('foobar', $translator->trans('bar')); } #[DataProvider('getInvalidLocalesTests')] public function testAddResourceInvalidLocales($locale) { $translator = new Translator('fr'); $this->expectException(InvalidArgumentException::class); $translator->addResource('array', ['foo' => 'foofoo'], $locale); } #[DataProvider('getValidLocalesTests')] public function testAddResourceValidLocales(string $locale) { $translator = new Translator('fr'); $translator->addResource('array', ['foo' => 'foofoo'], $locale); // no assertion. this method just asserts that no exception is thrown $this->addToAssertionCount(1); } public function testAddResourceAfterTrans() { $translator = new Translator('fr'); $translator->addLoader('array', new ArrayLoader()); $translator->setFallbackLocales(['en']); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); $this->assertEquals('foofoo', $translator->trans('foo')); $translator->addResource('array', ['bar' => 'foobar'], 'en'); $this->assertEquals('foobar', $translator->trans('bar')); } #[DataProvider('getTransFileTests')] public function testTransWithoutFallbackLocaleFile(string $format, string $loader) { $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; $translator = new Translator('en'); $translator->addLoader($format, new $loaderClass()); $translator->addResource($format, __DIR__.'/Fixtures/non-existing', 'en'); $translator->addResource($format, __DIR__.'/Fixtures/resources.'.$format, 'en'); $this->expectException(NotFoundResourceException::class); // force catalogue loading $translator->trans('foo'); } #[DataProvider('getTransFileTests')] public function testTransWithFallbackLocaleFile(string $format, string $loader) { $loaderClass = 'Symfony\\Component\\Translation\\Loader\\'.$loader; $translator = new Translator('en_GB'); $translator->addLoader($format, new $loaderClass()); $translator->addResource($format, __DIR__.'/Fixtures/non-existing', 'en_GB'); $translator->addResource($format, __DIR__.'/Fixtures/resources.'.$format, 'en', 'resources'); $this->assertEquals('bar', $translator->trans('foo', [], 'resources')); } public function testTransWithIcuFallbackLocale() { $translator = new Translator('en_GB'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en_GB'); $translator->addResource('array', ['bar' => 'foobar'], 'en_001'); $translator->addResource('array', ['baz' => 'foobaz'], 'en'); $this->assertSame('foofoo', $translator->trans('foo')); $this->assertSame('foobar', $translator->trans('bar')); $this->assertSame('foobaz', $translator->trans('baz')); } public function testTransWithIcuVariantFallbackLocale() { $translator = new Translator('en_GB_scouse'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en_GB_scouse'); $translator->addResource('array', ['bar' => 'foobar'], 'en_GB'); $translator->addResource('array', ['baz' => 'foobaz'], 'en_001'); $translator->addResource('array', ['bar' => 'en', 'qux' => 'fooqux'], 'en'); $translator->addResource('array', ['bar' => 'nl_NL', 'fallback' => 'nl_NL'], 'nl_NL'); $translator->addResource('array', ['bar' => 'nl', 'fallback' => 'nl'], 'nl'); $translator->setFallbackLocales(['nl_NL', 'nl']); $this->assertSame('foofoo', $translator->trans('foo')); $this->assertSame('foobar', $translator->trans('bar')); $this->assertSame('foobaz', $translator->trans('baz')); $this->assertSame('fooqux', $translator->trans('qux')); $this->assertSame('nl_NL', $translator->trans('fallback')); } public function testTransWithIcuRootFallbackLocale() { $translator = new Translator('az_Cyrl'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'az_Cyrl'); $translator->addResource('array', ['bar' => 'foobar'], 'az'); $this->assertSame('foofoo', $translator->trans('foo')); $this->assertSame('bar', $translator->trans('bar')); } #[DataProvider('getFallbackLocales')] public function testTransWithFallbackLocaleBis($expectedLocale, $locale) { $translator = new Translator($locale); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], $locale); $translator->addResource('array', ['bar' => 'foobar'], $expectedLocale); $this->assertEquals('foobar', $translator->trans('bar')); } public static function getFallbackLocales() { $locales = [ ['en', 'en_US'], ['en', 'en-US'], ['sl_Latn_IT', 'sl_Latn_IT_nedis'], ['sl_Latn', 'sl_Latn_IT'], ]; if (\function_exists('locale_parse')) { $locales[] = ['sl_Latn_IT', 'sl-Latn-IT-nedis']; $locales[] = ['sl_Latn', 'sl-Latn-IT']; } else { $locales[] = ['sl-Latn-IT', 'sl-Latn-IT-nedis']; $locales[] = ['sl-Latn', 'sl-Latn-IT']; } return $locales; } public function testTransWithFallbackLocaleTer() { $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (en_US)'], 'en_US'); $translator->addResource('array', ['foo' => 'foo (en)', 'bar' => 'bar (en)'], 'en'); $translator->setFallbackLocales(['en_US', 'en']); $this->assertEquals('foo (en_US)', $translator->trans('foo')); $this->assertEquals('bar (en)', $translator->trans('bar')); } public function testTransNonExistentWithFallback() { $translator = new Translator('fr'); $translator->setFallbackLocales(['en']); $translator->addLoader('array', new ArrayLoader()); $this->assertEquals('non-existent', $translator->trans('non-existent')); } public function testWhenAResourceHasNoRegisteredLoader() { $translator = new Translator('en'); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); $this->expectException(RuntimeException::class); $translator->trans('foo'); } public function testNestedFallbackCatalogueWhenUsingMultipleLocales() { $translator = new Translator('fr'); $translator->setFallbackLocales(['ru', 'en']); $translator->getCatalogue('fr'); $this->assertNotNull($translator->getCatalogue('ru')->getFallbackCatalogue()); } public function testFallbackCatalogueResources() { $translator = new Translator('en_GB'); $translator->addLoader('yml', new YamlFileLoader()); $translator->addResource('yml', __DIR__.'/Fixtures/empty.yml', 'en_GB'); $translator->addResource('yml', __DIR__.'/Fixtures/resources.yml', 'en'); // force catalogue loading $this->assertEquals('bar', $translator->trans('foo', [])); $resources = $translator->getCatalogue('en')->getResources(); $this->assertCount(1, $resources); $this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources); $resources = $translator->getCatalogue('en_GB')->getResources(); $this->assertCount(2, $resources); $this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'empty.yml', $resources); $this->assertContainsEquals(__DIR__.\DIRECTORY_SEPARATOR.'Fixtures'.\DIRECTORY_SEPARATOR.'resources.yml', $resources); } #[DataProvider('getTransTests')] public function testTrans($expected, $id, $translation, $parameters, $locale, $domain) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [(string) $id => $translation], $locale, $domain); $this->assertEquals($expected, $translator->trans($id, $parameters, $domain, $locale)); } #[DataProvider('getTransICUTests')] public function testTransICU(...$args) { if (!class_exists(\MessageFormatter::class)) { $this->markTestSkipped(\sprintf('Skipping test as the required "%s" class does not exist. Consider installing the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.', \MessageFormatter::class)); } $this->testTrans(...$args); } #[DataProvider('getInvalidLocalesTests')] public function testTransInvalidLocale($locale) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); $this->expectException(InvalidArgumentException::class); $translator->trans('foo', [], '', $locale); } #[DataProvider('getValidLocalesTests')] public function testTransValidLocale(string $locale) { $translator = new Translator($locale); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['test' => 'OK'], $locale); $this->assertEquals('OK', $translator->trans('test')); $this->assertEquals('OK', $translator->trans('test', [], null, $locale)); } #[DataProvider('getFlattenedTransTests')] public function testFlattenedTrans(string $expected, $messages, $id) { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', $messages, 'fr', ''); $this->assertEquals($expected, $translator->trans($id, [], '', 'fr')); } public function testTransNullId() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foofoo'], 'en'); $this->assertSame('', $translator->trans(null)); (\Closure::bind(function () use ($translator) { $this->assertSame([], $translator->catalogues); }, $this, Translator::class))(); } public static function getTransFileTests() { return [ ['csv', 'CsvFileLoader'], ['ini', 'IniFileLoader'], ['mo', 'MoFileLoader'], ['po', 'PoFileLoader'], ['php', 'PhpFileLoader'], ['ts', 'QtFileLoader'], ['xlf', 'XliffFileLoader'], ['yml', 'YamlFileLoader'], ['json', 'JsonFileLoader'], ]; } public static function getTransTests(): array { $param = new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'], ''); return [ ['Symfony est super !', 'Symfony is great!', 'Symfony est super !', [], 'fr', ''], ['Symfony est awesome !', 'Symfony is %what%!', 'Symfony est %what% !', ['%what%' => 'awesome'], 'fr', ''], ['Symfony est Symfony est awesome ! !', 'Symfony is %what%!', 'Symfony est %what% !', ['%what%' => $param], 'fr', ''], ['Symfony est super !', new StringClass('Symfony is great!'), 'Symfony est super !', [], 'fr', ''], ['', null, '', [], 'fr', ''], ]; } public static function getTransICUTests() { $id = '{apples, plural, =0 {There are no apples} one {There is one apple} other {There are # apples}}'; return [ ['There are no apples', $id, $id, ['{apples}' => 0], 'en', 'test'.MessageCatalogue::INTL_DOMAIN_SUFFIX], ['There is one apple', $id, $id, ['{apples}' => 1], 'en', 'test'.MessageCatalogue::INTL_DOMAIN_SUFFIX], ['There are 3 apples', $id, $id, ['{apples}' => 3], 'en', 'test'.MessageCatalogue::INTL_DOMAIN_SUFFIX], ]; } public static function getFlattenedTransTests() { $messages = [ 'symfony' => [ 'is' => [ 'great' => 'Symfony est super!', ], ], 'foo' => [ 'bar' => [ 'baz' => 'Foo Bar Baz', ], 'baz' => 'Foo Baz', ], ]; return [ ['Symfony est super!', $messages, 'symfony.is.great'], ['Foo Bar Baz', $messages, 'foo.bar.baz'], ['Foo Baz', $messages, 'foo.baz'], ]; } public static function getInvalidLocalesTests() { return [ ['fr FR'], ['français'], ['fr+en'], ['utf#8'], ['fr&en'], ['fr~FR'], [' fr'], ['fr '], ['fr*'], ['fr/FR'], ['fr\\FR'], ]; } public static function getValidLocalesTests() { return [ [''], ['fr'], ['francais'], ['FR'], ['frFR'], ['fr-FR'], ['fr_FR'], ['fr.FR'], ['fr-FR.UTF8'], ['sr@latin'], ]; } #[RequiresPhpExtension('intl')] public function testIntlFormattedDomain() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['some_message' => 'Hello %name%'], 'en'); $this->assertSame('Hello Bob', $translator->trans('some_message', ['%name%' => 'Bob'])); $translator->addResource('array', ['some_message' => 'Hi {name}'], 'en', 'messages+intl-icu'); $this->assertSame('Hi Bob', $translator->trans('some_message', ['%name%' => 'Bob'])); } public function testIntlDomainOverlapseWithIntlResourceBefore() { $intlFormatterMock = $this->createMock(IntlFormatterInterface::class); $intlFormatterMock->expects($this->once())->method('formatIntl')->with('hello intl', 'en', [])->willReturn('hello intl'); $messageFormatter = new MessageFormatter(null, $intlFormatterMock); $translator = new Translator('en', $messageFormatter); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu'); $translator->addResource('array', ['some_message' => 'hello'], 'en', 'messages'); $this->assertSame('hello', $translator->trans('some_message', [], 'messages')); $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu'); $this->assertSame('hello intl', $translator->trans('some_message', [], 'messages')); } public function testMissingLoaderForResourceError() { $translator = new Translator('en'); $translator->addResource('twig', 'messages.en.twig', 'en'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('No loader is registered for the "twig" format when loading the "messages.en.twig" resource.'); $translator->getCatalogue('en'); } public function testTransWithGlobalParameters() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['welcome' => 'Welcome {name}!'], 'en'); $translator->addResource('array', ['welcome' => 'Bienvenue {name}!'], 'fr'); $translator->addGlobalParameter('{name}', 'Global name'); $this->assertSame('Welcome Global name!', $translator->trans('welcome')); $this->assertSame('Bienvenue Global name!', $translator->trans('welcome', [], null, 'fr')); $this->assertSame('Welcome John!', $translator->trans('welcome', ['{name}' => 'John'])); $this->assertSame('Bienvenue Jean!', $translator->trans('welcome', ['{name}' => 'Jean'], null, 'fr')); } public function testTransWithGlobalTranslatableParameters() { $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['welcome' => 'Welcome on {link}!'], 'en'); $translator->addResource('array', ['welcome' => 'Bienvenue sur {link}!'], 'fr'); $translator->addResource('array', ['url' => 'example.com/admin'], 'en', 'globals'); $translator->addResource('array', ['url' => 'example.fr/admin'], 'fr', 'globals'); $translator->addGlobalParameter('{link}', new TranslatableMessage('url', [], 'globals')); $this->assertSame('Welcome on example.com/admin!', $translator->trans('welcome')); $this->assertSame('Bienvenue sur example.fr/admin!', $translator->trans('welcome', [], null, 'fr')); $this->assertSame('Welcome on other.com!', $translator->trans('welcome', ['{link}' => 'other.com'])); $this->assertSame('Bienvenue sur autre.fr!', $translator->trans('welcome', ['{link}' => 'autre.fr'], null, 'fr')); } #[RequiresPhpExtension('intl')] public function testTransICUWithGlobalParameters() { $domain = 'test.'.MessageCatalogue::INTL_DOMAIN_SUFFIX; $translator = new Translator('en'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', [ 'apples' => '{apples, plural, =0 {There are no apples} one {There is one apple} other {There are # apples}}', ], 'en', $domain); $translator->addGlobalParameter('{apples}', 42); $this->assertSame('There are 42 apples', $translator->trans('apples', [], $domain)); $this->assertSame('There is one apple', $translator->trans('apples', ['{apples}' => 1], $domain)); } } class StringClass { public function __construct( protected string $str, ) { } public function __toString(): string { return $this->str; } } ================================================ FILE: Tests/Util/ArrayConverterTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Util; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Util\ArrayConverter; class ArrayConverterTest extends TestCase { #[DataProvider('messagesData')] public function testDump($input, $expectedOutput) { $this->assertEquals($expectedOutput, ArrayConverter::expandToTree($input)); } public static function messagesData() { return [ [ // input [ 'foo1' => 'bar', 'foo.bar' => 'value', ], // expected output [ 'foo1' => 'bar', 'foo' => ['bar' => 'value'], ], ], [ // input [ 'foo.bar' => 'value1', 'foo.bar.test' => 'value2', ], // expected output [ 'foo' => [ 'bar' => 'value1', 'bar.test' => 'value2', ], ], ], [ // input [ 'foo.level2.level3.level4' => 'value1', 'foo.level2' => 'value2', 'foo.bar' => 'value3', ], // expected output [ 'foo' => [ 'level2' => 'value2', 'level2.level3.level4' => 'value1', 'bar' => 'value3', ], ], ], [ // input [ 'foo.' => 'foo.', '.bar' => '.bar', 'abc.abc' => 'value', 'bcd.bcd.' => 'value', '.cde.cde.' => 'value', '.def.def' => 'value', ], // expected output [ 'foo.' => 'foo.', '.bar' => '.bar', 'abc' => [ 'abc' => 'value', ], 'bcd' => [ 'bcd.' => 'value', ], '.cde' => [ 'cde.' => 'value', ], '.def' => [ 'def' => 'value', ], ], ], ]; } } ================================================ FILE: Tests/Writer/TranslationWriterTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Tests\Writer; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Dumper\DumperInterface; use Symfony\Component\Translation\Dumper\XliffFileDumper; use Symfony\Component\Translation\Dumper\YamlFileDumper; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Writer\TranslationWriter; class TranslationWriterTest extends TestCase { public function testWrite() { $dumper = $this->createMock(DumperInterface::class); $dumper ->expects($this->once()) ->method('dump'); $writer = new TranslationWriter(); $writer->addDumper('test', $dumper); $writer->write(new MessageCatalogue('en'), 'test'); } public function testGetFormats() { $writer = new TranslationWriter(); $writer->addDumper('foo', new YamlFileDumper()); $writer->addDumper('bar', new XliffFileDumper()); $this->assertEquals(['foo', 'bar'], $writer->getFormats()); } public function testFormatIsNotSupported() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('There is no dumper associated with format "foo".'); $writer = new TranslationWriter(); $writer->write(new MessageCatalogue('en'), 'foo'); } public function testUnwritableDirectory() { $writer = new TranslationWriter(); $writer->addDumper('foo', new YamlFileDumper()); $path = tempnam(sys_get_temp_dir(), ''); file_put_contents($path, ''); $this->expectException(RuntimeException::class); $this->expectExceptionMessage(\sprintf('Translation Writer was not able to create directory "%s".', $path)); $writer->write(new MessageCatalogue('en'), 'foo', ['path' => $path]); } } ================================================ FILE: TranslatableMessage.php ================================================ * * 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 { public function __construct( private string $message, private array $parameters = [], private ?string $domain = null, ) { } 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 { $parameters = $this->getParameters(); foreach ($parameters as $k => $v) { if ($v instanceof TranslatableInterface) { $parameters[$k] = $v->trans($translator, $locale); } } return $translator->trans($this->getMessage(), $parameters, $this->getDomain(), $locale); } } ================================================ FILE: Translator.php ================================================ * * 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\Config\ConfigCacheFactory; use Symfony\Component\Config\ConfigCacheFactoryInterface; use Symfony\Component\Config\ConfigCacheInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; 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\Contracts\Translation\LocaleAwareInterface; use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; // Help opcache.preload discover always-needed symbols class_exists(MessageCatalogue::class); /** * @author Fabien Potencier */ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface { /** * @var MessageCatalogueInterface[] */ protected array $catalogues = []; private string $locale; /** * @var string[] */ private array $fallbackLocales = []; /** * @var LoaderInterface[] */ private array $loaders = []; private array $resources = []; private MessageFormatterInterface $formatter; private ?ConfigCacheFactoryInterface $configCacheFactory; private bool $hasIntlFormatter; private LocaleFallbackProvider $localeFallbackProvider; /** * @var array */ private array $globalParameters = []; /** * @var array */ private array $globalTranslatedParameters = []; /** * @throws InvalidArgumentException If a locale contains invalid characters */ public function __construct( string $locale, ?MessageFormatterInterface $formatter = null, private ?string $cacheDir = null, private bool $debug = false, private array $cacheVary = [], ) { $this->setLocale($locale); $this->formatter = $formatter ??= new MessageFormatter(); $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface; $this->localeFallbackProvider = new LocaleFallbackProvider(); } public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory): void { $this->configCacheFactory = $configCacheFactory; } /** * Adds a Loader. * * @param string $format The name of the loader (@see addResource()) */ public function addLoader(string $format, LoaderInterface $loader): void { $this->loaders[$format] = $loader; } /** * Adds a Resource. * * @param string $format The name of the loader (@see addLoader()) * @param mixed $resource The resource name * * @throws InvalidArgumentException If the locale contains invalid characters */ public function addResource(string $format, mixed $resource, string $locale, ?string $domain = null): void { $domain ??= 'messages'; $this->assertValidLocale($locale); $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; $this->resources[$locale][] = [$format, $resource, $domain]; if (\in_array($locale, $this->fallbackLocales, true)) { $this->catalogues = []; } else { unset($this->catalogues[$locale]); } } public function setLocale(string $locale): void { $this->assertValidLocale($locale); $this->locale = $locale; } public function getLocale(): string { return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); } /** * Sets the fallback locales. * * @param string[] $locales * * @throws InvalidArgumentException If a locale contains invalid characters */ public function setFallbackLocales(array $locales): void { if ($this->fallbackLocales === $locales) { return; } $this->localeFallbackProvider = new LocaleFallbackProvider($locales); $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales; $this->catalogues = []; } /** * @internal */ public function getFallbackLocales(): array { return $this->fallbackLocales; } public function addGlobalParameter(string $id, string|int|float|TranslatableInterface $value): void { $this->globalParameters[$id] = $value; $this->globalTranslatedParameters = []; } public function getGlobalParameters(): array { return $this->globalParameters; } public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string { if (null === $id || '' === $id) { return ''; } $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; } } foreach ($parameters as $key => $value) { if ($value instanceof TranslatableInterface) { $parameters[$key] = $value->trans($this, $locale); } } if (null === $globalParameters = &$this->globalTranslatedParameters[$locale]) { $globalParameters = $this->globalParameters; foreach ($globalParameters as $key => $value) { if ($value instanceof TranslatableInterface) { $globalParameters[$key] = $value->trans($this, $locale); } } } if ($globalParameters) { $parameters += $globalParameters; } $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); } return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters); } public function getCatalogue(?string $locale = null): MessageCatalogueInterface { if (!$locale) { $locale = $this->getLocale(); } else { $this->assertValidLocale($locale); } if (!isset($this->catalogues[$locale])) { $this->loadCatalogue($locale); } return $this->catalogues[$locale]; } public function getCatalogues(): array { return array_values($this->catalogues); } /** * Gets the loaders. * * @return LoaderInterface[] */ protected function getLoaders(): array { return $this->loaders; } protected function loadCatalogue(string $locale): void { if (null === $this->cacheDir) { $this->initializeCatalogue($locale); } else { $this->initializeCacheCatalogue($locale); } } protected function initializeCatalogue(string $locale): void { $this->assertValidLocale($locale); try { $this->doLoadCatalogue($locale); } catch (NotFoundResourceException $e) { if (!$this->computeFallbackLocales($locale)) { throw $e; } } $this->loadFallbackCatalogues($locale); } private function initializeCacheCatalogue(string $locale): void { if (isset($this->catalogues[$locale])) { /* Catalogue already initialized. */ return; } $this->assertValidLocale($locale); $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale), function (ConfigCacheInterface $cache) use ($locale) { $this->dumpCatalogue($locale, $cache); } ); if (isset($this->catalogues[$locale])) { /* Catalogue has been initialized as it was written out to cache. */ return; } /* Read catalogue from cache. */ $this->catalogues[$locale] = include $cache->getPath(); } private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void { $this->initializeCatalogue($locale); $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); $content = \sprintf(<<getAllMessages($this->catalogues[$locale]), true), $fallbackContent ); $cache->write($content, $this->catalogues[$locale]->getResources()); } private function getFallbackContent(MessageCatalogue $catalogue): string { $fallbackContent = ''; $current = ''; $replacementPattern = '/[^a-z0-9_]/i'; $fallbackCatalogue = $catalogue->getFallbackCatalogue(); while ($fallbackCatalogue) { $fallback = $fallbackCatalogue->getLocale(); $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); $fallbackContent .= \sprintf(<<<'EOF' $catalogue%s = new MessageCatalogue('%s', %s); $catalogue%s->addFallbackCatalogue($catalogue%s); EOF, $fallbackSuffix, $fallback, var_export($this->getAllMessages($fallbackCatalogue), true), $currentSuffix, $fallbackSuffix ); $current = $fallbackCatalogue->getLocale(); $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); } return $fallbackContent; } private function getCatalogueCachePath(string $locale): string { return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('xxh128', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; } /** * @internal */ protected function doLoadCatalogue(string $locale): void { $this->catalogues[$locale] = new MessageCatalogue($locale); if (isset($this->resources[$locale])) { foreach ($this->resources[$locale] as $resource) { if (!isset($this->loaders[$resource[0]])) { if (\is_string($resource[1])) { throw new RuntimeException(\sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1])); } throw new RuntimeException(\sprintf('No loader is registered for the "%s" format.', $resource[0])); } $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2])); } } } private function loadFallbackCatalogues(string $locale): void { $current = $this->catalogues[$locale]; foreach ($this->computeFallbackLocales($locale) as $fallback) { if (!isset($this->catalogues[$fallback])) { $this->initializeCatalogue($fallback); } $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback])); foreach ($this->catalogues[$fallback]->getResources() as $resource) { $fallbackCatalogue->addResource($resource); } $current->addFallbackCatalogue($fallbackCatalogue); $current = $fallbackCatalogue; } } protected function computeFallbackLocales(string $locale): array { return $this->localeFallbackProvider->computeFallbackLocales($locale); } /** * Asserts that the locale is valid, throws an Exception if not. * * @throws InvalidArgumentException If the locale contains invalid characters */ protected function assertValidLocale(string $locale): void { LocaleFallbackProvider::validateLocale($locale); } /** * Provides the ConfigCache factory implementation, falling back to a * default implementation if necessary. */ private function getConfigCacheFactory(): ConfigCacheFactoryInterface { $this->configCacheFactory ??= new ConfigCacheFactory($this->debug); return $this->configCacheFactory; } private function getAllMessages(MessageCatalogueInterface $catalogue): array { $allMessages = []; foreach ($catalogue->all() as $domain => $messages) { if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages; $messages = array_diff_key($messages, $intlMessages); } if ($messages) { $allMessages[$domain] = $messages; } } return $allMessages; } } ================================================ FILE: TranslatorBag.php ================================================ * * 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 array $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); } } public function getCatalogue(?string $locale = null): MessageCatalogueInterface { if (null === $locale || !isset($this->catalogues[$locale])) { $this->catalogues[$locale] = new MessageCatalogue($locale); } return $this->catalogues[$locale]; } 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 ($catalogue->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( array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)), $domain ); } $diff->addCatalogue($obsoleteCatalogue); } return $diff; } } ================================================ FILE: TranslatorBagInterface.php ================================================ * * 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; /** * @author Abdellatif Ait boudad */ interface TranslatorBagInterface { /** * Gets the catalogue by locale. * * @param string|null $locale The locale or null to use the default * * @throws InvalidArgumentException If the locale contains invalid characters */ public function getCatalogue(?string $locale = null): MessageCatalogueInterface; /** * Returns all catalogues of the instance. * * @return MessageCatalogueInterface[] */ public function getCatalogues(): array; } ================================================ FILE: Util/ArrayConverter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; /** * ArrayConverter generates tree like structure from a message catalogue. * e.g. this * 'foo.bar1' => 'test1', * 'foo.bar2' => 'test2' * converts to follows: * foo: * bar1: test1 * bar2: test2. * * @author Gennady Telegin */ class ArrayConverter { /** * Converts linear messages array to tree-like array. * For example: ['foo.bar' => 'value'] will be converted to ['foo' => ['bar' => 'value']]. * * @param array $messages Linear messages array */ public static function expandToTree(array $messages): array { $tree = []; foreach ($messages as $id => $value) { $referenceToElement = &self::getElementByPath($tree, self::getKeyParts($id)); $referenceToElement = $value; unset($referenceToElement); } return $tree; } private static function &getElementByPath(array &$tree, array $parts): mixed { $elem = &$tree; $parentOfElem = null; foreach ($parts as $i => $part) { if (isset($elem[$part]) && \is_string($elem[$part])) { /* Process next case: * 'foo': 'test1', * 'foo.bar': 'test2' * * $tree['foo'] was string before we found array {bar: test2}. * Treat new element as string too, e.g. add $tree['foo.bar'] = 'test2'; */ $elem = &$elem[implode('.', \array_slice($parts, $i))]; break; } $parentOfElem = &$elem; $elem = &$elem[$part]; } if ($elem && \is_array($elem) && $parentOfElem) { /* Process next case: * 'foo.bar': 'test1' * 'foo': 'test2' * * $tree['foo'] was array = {bar: 'test1'} before we found string constant `foo`. * Cancel treating $tree['foo'] as array and cancel back it expansion, * e.g. make it $tree['foo.bar'] = 'test1' again. */ self::cancelExpand($parentOfElem, $part, $elem); } return $elem; } private static function cancelExpand(array &$tree, string $prefix, array $node): void { $prefix .= '.'; foreach ($node as $id => $value) { if (\is_string($value)) { $tree[$prefix.$id] = $value; } else { self::cancelExpand($tree, $prefix.$id, $value); } } } /** * @return string[] */ private static function getKeyParts(string $key): array { $parts = explode('.', $key); $partsCount = \count($parts); $result = []; $buffer = ''; foreach ($parts as $index => $part) { if (0 === $index && '' === $part) { $buffer = '.'; continue; } if ($index === $partsCount - 1 && '' === $part) { $buffer .= '.'; $result[] = $buffer; continue; } if (isset($parts[$index + 1]) && '' === $parts[$index + 1]) { $buffer .= $part; continue; } if ($buffer) { $result[] = $buffer.$part; $buffer = ''; continue; } $result[] = $part; } return $result; } } ================================================ FILE: Util/XliffUtils.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Util; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidResourceException; /** * Provides some utility methods for XLIFF translation files, such as validating * their contents according to the XSD schema. * * @author Fabien Potencier */ class XliffUtils { /** * Gets xliff file version based on the root "version" attribute. * * Defaults to 1.2 for backwards compatibility. * * @throws InvalidArgumentException */ public static function getVersionNumber(\DOMDocument $dom): string { foreach ($dom->getElementsByTagName('xliff') as $xliff) { $version = $xliff->attributes->getNamedItem('version'); if ($version) { return $version->nodeValue; } $namespace = $xliff->attributes->getNamedItem('xmlns'); if ($namespace) { if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { throw new InvalidArgumentException(\sprintf('Not a valid XLIFF namespace "%s".', $namespace)); } return substr($namespace, 34); } } // Falls back to v1.2 return '1.2'; } /** * Validates and parses the given file into a DOMDocument. * * @throws InvalidResourceException */ public static function validateSchema(\DOMDocument $dom): array { $xliffVersion = static::getVersionNumber($dom); $internalErrors = libxml_use_internal_errors(true); if ($shouldEnable = self::shouldEnableEntityLoader()) { $disableEntities = libxml_disable_entity_loader(false); } try { $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); if (!$isValid) { return self::getXmlErrors($internalErrors); } } finally { if ($shouldEnable) { libxml_disable_entity_loader($disableEntities); } } $dom->normalizeDocument(); libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return []; } private static function shouldEnableEntityLoader(): bool { static $dom, $schema; if (null === $dom) { $dom = new \DOMDocument(); $dom->loadXML(''); $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); register_shutdown_function(static function () use ($tmpfile) { @unlink($tmpfile); }); $schema = ' '; file_put_contents($tmpfile, ' '); } return !@$dom->schemaValidateSource($schema); } public static function getErrorsAsString(array $xmlErrors): string { $errorsAsString = ''; foreach ($xmlErrors as $error) { $errorsAsString .= \sprintf("[%s %s] %s (in %s - line %d, column %d)\n", \LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR', $error['code'], $error['message'], $error['file'], $error['line'], $error['column'] ); } return $errorsAsString; } private static function getSchema(string $xliffVersion): string { if ('1.2' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-1.2-transitional.xsd'); $xmlUri = 'http://www.w3.org/2001/xml.xsd'; } elseif (\in_array($xliffVersion, ['2.0', '2.1'], true)) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.0.xsd'); $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; } elseif ('2.2' === $xliffVersion) { $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.2.xsd'); $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; } else { throw new InvalidArgumentException(\sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); } return self::fixXmlLocation($schemaSource, $xmlUri); } /** * Internally changes the URI of a dependent xsd to be loaded locally. */ private static function fixXmlLocation(string $schemaSource, string $xmlUri): string { $newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd'; $parts = explode('/', $newPath); $locationstart = 'file:///'; if (0 === stripos($newPath, 'phar://')) { $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); if ($tmpfile) { copy($newPath, $tmpfile); $parts = explode('/', str_replace('\\', '/', $tmpfile)); } else { array_shift($parts); $locationstart = 'phar:///'; } } $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); return str_replace($xmlUri, $newPath, $schemaSource); } /** * Returns the XML errors of the internal XML parser. */ private static function getXmlErrors(bool $internalErrors): array { $errors = []; foreach (libxml_get_errors() as $error) { $errors[] = [ 'level' => \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', 'code' => $error->code, 'message' => trim($error->message), 'file' => $error->file ?: 'n/a', 'line' => $error->line, 'column' => $error->column, ]; } libxml_clear_errors(); libxml_use_internal_errors($internalErrors); return $errors; } } ================================================ FILE: Writer/TranslationWriter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Dumper\DumperInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib */ class TranslationWriter implements TranslationWriterInterface { /** * @var array */ private array $dumpers = []; /** * Adds a dumper to the writer. */ public function addDumper(string $format, DumperInterface $dumper): void { $this->dumpers[$format] = $dumper; } /** * Obtains the list of supported formats. */ public function getFormats(): array { return array_keys($this->dumpers); } /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []): void { if (!isset($this->dumpers[$format])) { throw new InvalidArgumentException(\sprintf('There is no dumper associated with format "%s".', $format)); } // get the right dumper $dumper = $this->dumpers[$format]; if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0o777, true) && !is_dir($options['path'])) { throw new RuntimeException(\sprintf('Translation Writer was not able to create directory "%s".', $options['path'])); } // save $dumper->dump($catalogue, $options); } } ================================================ FILE: Writer/TranslationWriterInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Translation\Writer; use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\MessageCatalogue; /** * TranslationWriter writes translation messages. * * @author Michel Salib */ interface TranslationWriterInterface { /** * Writes translation from the catalogue according to the selected format. * * @param string $format The format to use to dump the messages * @param array $options Options that are passed to the dumper * * @throws InvalidArgumentException */ public function write(MessageCatalogue $catalogue, string $format, array $options = []): void; } ================================================ FILE: composer.json ================================================ { "name": "symfony/translation", "type": "library", "description": "Provides tools to internationalize your application", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.4", "symfony/polyfill-mbstring": "^1.0", "symfony/translation-contracts": "^3.6.1" }, "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", "symfony/config": "^7.4|^8.0", "symfony/console": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", "symfony/http-kernel": "^7.4|^8.0", "symfony/intl": "^7.4|^8.0", "symfony/polyfill-intl-icu": "^1.21", "symfony/routing": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", "symfony/yaml": "^7.4|^8.0" }, "conflict": { "nikic/php-parser": "<5.0", "symfony/http-client-contracts": "<2.5", "symfony/service-contracts": "<2.5" }, "provide": { "symfony/translation-implementation": "2.3|3.0" }, "autoload": { "files": [ "Resources/functions.php" ], "psr-4": { "Symfony\\Component\\Translation\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } ================================================ FILE: phpunit.xml.dist ================================================ ./Tests/ ./ ./Tests ./vendor