core/lib/Thelia/Action/Translation.php line 257
<?php
/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Thelia\Action;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Filesystem\Filesystem;
use Thelia\Core\Event\Cache\CacheEvent;
use Thelia\Core\Event\TheliaEvents;
use Thelia\Core\Event\Translation\TranslationEvent;
use Thelia\Core\Translation\Translator;
use Thelia\Log\Tlog;
/**
* Class Translation.
*
* @author Manuel Raynaud <manu@raynaud.io>
*/
class Translation extends BaseAction implements EventSubscriberInterface
{
/** @var ContainerInterface */
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getTranslatableStrings(TranslationEvent $event): void
{
$strings = [];
$stringCount = $this->walkDir(
$event->getDirectory(),
$event->getMode(),
$event->getLocale(),
$event->getDomain(),
$strings
);
$event
->setTranslatableStrings($strings)
->setTranslatableStringCount($stringCount)
;
}
/**
* Recursively examine files in a directory tree, and extract translatable strings.
*
* Returns an array of translatable strings, each item having with the following structure:
* 'files' an array of file names in which the string appears,
* 'text' the translatable text
* 'translation' => the text translation, or an empty string if none available.
* 'dollar' => true if the translatable text contains a $
*
* @param string $directory the path to the directory to examine
* @param string $walkMode type of file scanning: WALK_MODE_PHP or WALK_MODE_TEMPLATE
* @param string $currentLocale the current locale
* @param string $domain the translation domain (fontoffice, backoffice, module, etc...)
* @param array $strings the list of strings
*
* @throws \InvalidArgumentException if $walkMode contains an invalid value
*
* @return number the total number of translatable texts
*/
protected function walkDir(string $directory, string $walkMode, string $currentLocale, string $domain, array &$strings)
{
$numTexts = 0;
if ($walkMode == TranslationEvent::WALK_MODE_PHP) {
$prefix = '\-\>[\s]*trans[\s]*\([\s]*';
$allowedExts = ['php'];
} elseif ($walkMode == TranslationEvent::WALK_MODE_TEMPLATE) {
$prefix = '\{intl(?:.*?)[\s]l=[\s]*';
$allowedExts = ['html', 'tpl', 'xml', 'txt'];
} else {
throw new \InvalidArgumentException(
Translator::getInstance()->trans(
'Invalid value for walkMode parameter: %value',
['%value' => $walkMode]
)
);
}
try {
Tlog::getInstance()->debug("Walking in $directory, in mode $walkMode");
/** @var \DirectoryIterator $fileInfo */
foreach (new \DirectoryIterator($directory) as $fileInfo) {
if ($fileInfo->isDot()) {
continue;
}
if ($fileInfo->isDir()) {
$numTexts += $this->walkDir(
$fileInfo->getPathName(),
$walkMode,
$currentLocale,
$domain,
$strings
);
}
if ($fileInfo->isFile()) {
$ext = $fileInfo->getExtension();
if (\in_array($ext, $allowedExts)) {
if ($content = file_get_contents($fileInfo->getPathName())) {
$short_path = $this->normalizePath($fileInfo->getPathName());
Tlog::getInstance()->debug("Examining file $short_path\n");
$matches = [];
if (preg_match_all(
'/'.$prefix.'((?<![\\\\])[\'"])((?:.(?!(?<![\\\\])\1))*.?)*?\1/ms',
$content,
$matches
)) {
Tlog::getInstance()->debug('Strings found: ', $matches[2]);
$idx = 0;
foreach ($matches[2] as $match) {
$hash = md5($match);
if (isset($strings[$hash])) {
if (!\in_array($short_path, $strings[$hash]['files'])) {
$strings[$hash]['files'][] = $short_path;
}
} else {
++$numTexts;
// remove \' (or \"), that will prevent the translator to work properly, as
// "abc \def\" ghi" will be passed as abc "def" ghi to the translator.
$quote = $matches[1][$idx];
$match = str_replace("\\$quote", $quote, $match);
// Ignore empty strings
if (\strlen($match) == 0) {
continue;
}
$strings[$hash] = [
'files' => [$short_path],
'text' => $match,
'translation' => Translator::getInstance()->trans(
$match,
[],
$domain,
$currentLocale,
false,
false
),
'custom_fallback' => Translator::getInstance()->trans(
sprintf(
Translator::GLOBAL_FALLBACK_KEY,
$domain,
$match
),
[],
Translator::GLOBAL_FALLBACK_DOMAIN,
$currentLocale,
false,
false
),
'global_fallback' => Translator::getInstance()->trans(
$match,
[],
Translator::GLOBAL_FALLBACK_DOMAIN,
$currentLocale,
false,
false
),
'dollar' => strstr($match, '$') !== false,
];
}
++$idx;
}
}
}
}
}
}
} catch (\UnexpectedValueException $ex) {
// Directory does not exists => ignore it.
}
return $numTexts;
}
public function writeTranslationFile(TranslationEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$file = $event->getTranslationFilePath();
$fs = new Filesystem();
if (!$fs->exists($file) && true === $event->isCreateFileIfNotExists()) {
$dir = \dirname($file);
if (!$fs->exists($file)) {
$fs->mkdir($dir);
$this->cacheClear($dispatcher);
}
}
if ($fp = @fopen($file, 'w')) {
fwrite($fp, '<'."?php\n\n");
fwrite($fp, "return array(\n");
$texts = $event->getTranslatableStrings();
$translations = $event->getTranslatedStrings();
// Sort keys alphabetically while keeping index
asort($texts);
foreach ($texts as $key => $text) {
// Write only defined (not empty) translations
if (!empty($translations[$key])) {
$text = str_replace("'", "\'", $text);
$translation = str_replace("'", "\'", $translations[$key]);
fwrite($fp, sprintf(" '%s' => '%s',\n", $text, $translation));
}
}
fwrite($fp, ");\n");
@fclose($fp);
} else {
throw new \RuntimeException(
Translator::getInstance()->trans(
'Failed to open translation file %file. Please be sure that this file is writable by your Web server',
['%file' => $file]
)
);
}
}
public function writeFallbackFile(TranslationEvent $event, $eventName, EventDispatcherInterface $dispatcher): void
{
$file = THELIA_LOCAL_DIR.'I18n'.DS.$event->getLocale().'.php';
$fs = new Filesystem();
$translations = [];
if (!$fs->exists($file)) {
if (true === $event->isCreateFileIfNotExists()) {
$dir = \dirname($file);
$fs->mkdir($dir);
$this->cacheClear($dispatcher);
} else {
throw new \RuntimeException(
Translator::getInstance()->trans(
'Failed to open translation file %file. Please be sure that this file is writable by your Web server',
['%file' => $file]
)
);
}
} else {
/*$loader = new PhpFileLoader();
$catalogue = $loade r->load($file);
$translations = $catalogue->all();
*/
$translations = require $file;
if (!\is_array($translations)) {
$translations = [];
}
}
if ($fp = @fopen($file, 'w')) {
$texts = $event->getTranslatableStrings();
$customs = $event->getCustomFallbackStrings();
$globals = $event->getGlobalFallbackStrings();
// just reset current translations for this domain to remove strings that do not exist anymore
$translations[$event->getDomain()] = [];
foreach ($texts as $key => $text) {
if (!empty($customs[$key])) {
$translations[$event->getDomain()][$text] = $customs[$key];
}
if (!empty($globals[$key])) {
$translations[$text] = $globals[$key];
} else {
unset($translations[$text]);
}
}
fwrite($fp, '<'."?php\n\n");
fwrite($fp, "return [\n");
// Sort keys alphabetically while keeping index
ksort($translations);
foreach ($translations as $key => $text) {
// Write only defined (not empty) translations
if (!empty($translations[$key])) {
if (\is_array($translations[$key])) {
$key = str_replace("'", "\'", $key);
fwrite($fp, sprintf(" '%s' => [\n", $key));
ksort($translations[$key]);
foreach ($translations[$key] as $subKey => $subText) {
$subKey = str_replace("'", "\'", $subKey);
$translation = str_replace("'", "\'", $subText);
fwrite($fp, sprintf(" '%s' => '%s',\n", $subKey, $translation));
}
fwrite($fp, " ],\n");
} else {
$key = str_replace("'", "\'", $key);
$translation = str_replace("'", "\'", $text);
fwrite($fp, sprintf(" '%s' => '%s',\n", $key, $translation));
}
}
}
fwrite($fp, "];\n");
@fclose($fp);
}
}
protected function normalizePath($path)
{
$path = str_replace(
str_replace('\\', '/', THELIA_ROOT),
'',
str_replace('\\', '/', realpath($path))
);
return ltrim($path, '/');
}
protected function cacheClear(EventDispatcherInterface $dispatcher): void
{
$cacheEvent = new CacheEvent(
$this->container->getParameter('kernel.cache_dir')
);
$dispatcher->dispatch($cacheEvent, TheliaEvents::CACHE_CLEAR);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
TheliaEvents::TRANSLATION_GET_STRINGS => ['getTranslatableStrings', 128],
TheliaEvents::TRANSLATION_WRITE_FILE => [
['writeTranslationFile', 128],
['writeFallbackFile', 128],
],
];
}
}