Current File : //var/www/prestashop/src/Core/Translation/Storage/Provider/ModuleCatalogueLayersProvider.php |
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/OSL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to https://devdocs.prestashop.com/ for more information.
*
* @author PrestaShop SA and Contributors <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
*/
declare(strict_types=1);
namespace PrestaShop\PrestaShop\Core\Translation\Storage\Provider;
use PrestaShop\PrestaShop\Core\Translation\Exception\TranslationFilesNotFoundException;
use PrestaShop\PrestaShop\Core\Translation\Exception\UnsupportedLocaleException;
use PrestaShop\PrestaShop\Core\Translation\Storage\Extractor\LegacyModuleExtractorInterface;
use PrestaShop\PrestaShop\Core\Translation\Storage\Loader\DatabaseTranslationLoader;
use PrestaShop\PrestaShop\Core\Translation\Storage\Normalizer\DomainNormalizer;
use PrestaShop\PrestaShop\Core\Translation\Storage\Provider\Finder\DefaultCatalogueFinder;
use PrestaShop\PrestaShop\Core\Translation\Storage\Provider\Finder\FileTranslatedCatalogueFinder;
use PrestaShop\PrestaShop\Core\Translation\Storage\Provider\Finder\UserTranslatedCatalogueFinder;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
/**
* Returns the 3 layers of translation catalogues related to the Module translations.
* The default catalogue is searched in app/Resources/translations/default, in any file starting with "ModulesMODULENAME"
* If not found, default catalogue is extracted for module's templates
* The file catalogue is searched in app/Resources/translations/LOCALE, in any file starting with "ModulesMODULENAME"
* If not found, we scan the directory modules/MODULENAME/translations/LOCALE
* The user catalogue is stored in DB, domain starting with ModulesMODULENAME and theme is NULL.
*
* @see CatalogueLayersProviderInterface to understand the 3 layers.
*/
class ModuleCatalogueLayersProvider implements CatalogueLayersProviderInterface
{
/**
* We need a connection to DB to load user translated catalogue.
*
* @var DatabaseTranslationLoader
*/
private $databaseTranslationLoader;
/**
* @var DefaultCatalogueFinder
*/
private $defaultCatalogueFinder;
/**
* @var FileTranslatedCatalogueFinder
*/
private $fileTranslatedCatalogueFinder;
/**
* @var FileTranslatedCatalogueFinder
*/
private $builtInFileTranslatedCatalogueFinder;
/**
* @var UserTranslatedCatalogueFinder
*/
private $userTranslatedCatalogueFinder;
/**
* @var string
*/
private $moduleName;
/**
* @var string
*/
private $modulesDirectory;
/**
* @var string
*/
private $translationsDirectory;
/**
* @var MessageCatalogue[]
*/
private $defaultCatalogueCache;
/**
* @var LegacyModuleExtractorInterface
*/
private $legacyModuleExtractor;
/**
* @var LoaderInterface
*/
private $legacyFileLoader;
/**
* @var array<int, string>
*/
private $filenameFilters;
/**
* @var array<int, string>
*/
private $translationDomains;
/**
* @param DatabaseTranslationLoader $databaseTranslationLoader
* @param LegacyModuleExtractorInterface $legacyModuleExtractor
* @param LoaderInterface $legacyFileLoader
* @param string $modulesDirectory
* @param string $translationsDirectory
* @param string $moduleName
* @param array<int, string> $filenameFilters
* @param array<int, string> $translationDomains
*/
public function __construct(
DatabaseTranslationLoader $databaseTranslationLoader,
LegacyModuleExtractorInterface $legacyModuleExtractor,
LoaderInterface $legacyFileLoader,
string $modulesDirectory,
string $translationsDirectory,
string $moduleName,
array $filenameFilters,
array $translationDomains
) {
$this->databaseTranslationLoader = $databaseTranslationLoader;
$this->moduleName = $moduleName;
$this->modulesDirectory = $modulesDirectory;
$this->translationsDirectory = $translationsDirectory;
$this->legacyModuleExtractor = $legacyModuleExtractor;
$this->legacyFileLoader = $legacyFileLoader;
$this->filenameFilters = $filenameFilters;
$this->translationDomains = $translationDomains;
}
/**
* {@inheritdoc}
*/
public function getDefaultCatalogue(string $locale): MessageCatalogue
{
// There are 2 kind of modules : Native (built with core) and Non-Native (the modules installed by user himself)
// For Native modules, translations are in the default core translations directory
// For non native modules, the catalogue is built from templates
// First we search in translation directory in case the module is native
try {
$defaultCatalogue = $this->getDefaultCatalogueFinder()->getCatalogue($locale);
} catch (TranslationFilesNotFoundException $e) {
$defaultCatalogue = new MessageCatalogue($locale);
}
// Then we extract the catalogue from the module's templates and add it to the initial default catalogue, this way
// even native modules will display wordings that may not be present in the XLF files
$extractedCatalogue = $this->getDefaultCatalogueExtractedFromTemplates($locale);
// We merge both catalogues
foreach ($extractedCatalogue->getDomains() as $domain) {
$defaultCatalogue->add($extractedCatalogue->all($domain), $domain);
}
return $defaultCatalogue;
}
/**
* @param string $locale
*
* @return MessageCatalogue
*/
public function getFileTranslatedCatalogue(string $locale): MessageCatalogue
{
try { // First we search in the module's translation directory
return $this->getModuleBuiltInFileTranslatedCatalogueFinder()->getCatalogue($locale);
} catch (TranslationFilesNotFoundException $exception) {
// If no translation file was found in the module, No Exception
// we search in the Core's files
}
try {
return $this->getCoreFileTranslatedCatalogueFinder()->getCatalogue($locale);
} catch (TranslationFilesNotFoundException $exception) {
// And finally if no translation was found in the Core files, we search in the legacy files
return $this->buildTranslationCatalogueFromLegacyFiles($locale);
}
}
/**
* {@inheritdoc}
*/
public function getUserTranslatedCatalogue(string $locale): MessageCatalogue
{
return $this->getUserTranslatedCatalogueFinder()->getCatalogue($locale);
}
/**
* @return DefaultCatalogueFinder
*
* @throws TranslationFilesNotFoundException
*/
private function getDefaultCatalogueFinder(): DefaultCatalogueFinder
{
if (null === $this->defaultCatalogueFinder) {
$this->defaultCatalogueFinder = new DefaultCatalogueFinder(
$this->translationsDirectory . DIRECTORY_SEPARATOR . 'default',
$this->filenameFilters
);
}
return $this->defaultCatalogueFinder;
}
/**
* @return FileTranslatedCatalogueFinder
*
* @throws TranslationFilesNotFoundException
*/
private function getCoreFileTranslatedCatalogueFinder(): FileTranslatedCatalogueFinder
{
if (null === $this->fileTranslatedCatalogueFinder) {
$this->fileTranslatedCatalogueFinder = new FileTranslatedCatalogueFinder(
$this->translationsDirectory,
$this->filenameFilters
);
}
return $this->fileTranslatedCatalogueFinder;
}
/**
* @return UserTranslatedCatalogueFinder
*/
private function getUserTranslatedCatalogueFinder(): UserTranslatedCatalogueFinder
{
if (null === $this->userTranslatedCatalogueFinder) {
$this->userTranslatedCatalogueFinder = new UserTranslatedCatalogueFinder(
$this->databaseTranslationLoader,
$this->translationDomains
);
}
return $this->userTranslatedCatalogueFinder;
}
/**
* @return FileTranslatedCatalogueFinder
*
* @throws TranslationFilesNotFoundException
*/
private function getModuleBuiltInFileTranslatedCatalogueFinder(): FileTranslatedCatalogueFinder
{
if (null === $this->builtInFileTranslatedCatalogueFinder) {
$this->builtInFileTranslatedCatalogueFinder = new FileTranslatedCatalogueFinder(
implode(DIRECTORY_SEPARATOR, [
$this->modulesDirectory,
$this->moduleName,
'translations',
]),
$this->filenameFilters
);
}
return $this->builtInFileTranslatedCatalogueFinder;
}
/**
* Builds the catalogue including the translated wordings ONLY
*
* @param string $locale
*
* @return MessageCatalogue
*/
private function buildTranslationCatalogueFromLegacyFiles(string $locale): MessageCatalogue
{
// the message catalogue needs to be indexed by original wording, but legacy files are indexed by hash
// therefore, we need to build the default catalogue (by analyzing source code)
// then cross reference the wordings found in the default catalogue
// with the hashes found in the module's legacy translation file.
$legacyFilesCatalogue = new MessageCatalogue($locale);
$catalogueFromPhpAndSmartyFiles = $this->getDefaultCatalogue($locale);
try {
$catalogueFromLegacyTranslationFiles = $this->legacyFileLoader->load(
$this->getBuiltInModuleDirectory(),
$locale
);
} catch (UnsupportedLocaleException $exception) {
// this happens when there is no translation file found for the desired locale
return $catalogueFromPhpAndSmartyFiles;
}
foreach ($catalogueFromPhpAndSmartyFiles->all() as $currentDomain => $items) {
foreach (array_keys($items) as $translationKey) {
$legacyKey = md5($translationKey);
if ($catalogueFromLegacyTranslationFiles->has($legacyKey, $currentDomain)) {
$legacyFilesCatalogue->set(
$translationKey,
$catalogueFromLegacyTranslationFiles->get($legacyKey, $currentDomain),
// use current domain and not module domain, otherwise we'd lose the third part from the domain
$currentDomain
);
}
}
}
return $legacyFilesCatalogue;
}
/**
* Returns the translations directory within the module files
*
* @return string
*/
private function getBuiltInModuleDirectory(): string
{
return implode(DIRECTORY_SEPARATOR, [
$this->modulesDirectory,
$this->moduleName,
'translations',
]) . DIRECTORY_SEPARATOR;
}
/**
* Returns the cached default catalogue
*
* @param string $locale
*
* @return MessageCatalogue
*/
private function getDefaultCatalogueExtractedFromTemplates(string $locale): MessageCatalogue
{
$catalogueCacheKey = $this->moduleName . '|' . $locale;
if (!isset($this->defaultCatalogueCache[$catalogueCacheKey])) {
$this->defaultCatalogueCache[$catalogueCacheKey] = $this->buildFreshDefaultCatalogueFromTemplates($locale);
}
return $this->defaultCatalogueCache[$catalogueCacheKey];
}
/**
* Builds the default catalogue
*
* @param string $locale
*
* @return MessageCatalogue
*/
private function buildFreshDefaultCatalogueFromTemplates(string $locale): MessageCatalogue
{
$defaultCatalogue = new MessageCatalogue($locale);
try {
// analyze template files and extract wordings
/** @var MessageCatalogue $additionalDefaultCatalogue */
$additionalDefaultCatalogue = $this->legacyModuleExtractor->extract($this->moduleName, $locale);
$defaultCatalogue = $this->convertDomainsAndFilterCatalogue($additionalDefaultCatalogue);
} catch (UnsupportedLocaleException $exception) {
// Do nothing as support of legacy files is deprecated
}
return $defaultCatalogue;
}
/**
* Replaces dots in the catalogue's domain names
* and filters out domains not corresponding to the one from this module
*
* When extracted from templates, the domain names are in format Modules.MODULENAME.DOMAIN.DOMAIN
* The required catalogue domains format is something like ModulesModulenameDomain... : Camelcased with max 3 levels
*
* @param MessageCatalogue $catalogue
*
* @return MessageCatalogue
*/
private function convertDomainsAndFilterCatalogue(MessageCatalogue $catalogue): MessageCatalogue
{
$normalizer = new DomainNormalizer();
$newCatalogue = new MessageCatalogue($catalogue->getLocale());
foreach ($catalogue->getDomains() as $domain) {
// remove dots
$newDomain = $normalizer->normalize($domain);
// add delimiters
// only add if the domain is relevant to this module
foreach ($this->filenameFilters as $pattern) {
if (preg_match($pattern, $newDomain)) {
$newCatalogue->add(
$catalogue->all($domain),
$newDomain
);
break;
}
}
}
return $newCatalogue;
}
}