Spaces:
No application file
No application file
namespace Mautic\CoreBundle\Helper; | |
use GuzzleHttp\Client; | |
use Mautic\CoreBundle\Helper\Language\Installer; | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\Finder\Finder; | |
use Symfony\Contracts\Translation\TranslatorInterface; | |
/** | |
* Helper class for managing Mautic's installed languages. | |
*/ | |
class LanguageHelper | |
{ | |
private string $cacheFile; | |
private Installer $installer; | |
private array $supportedLanguages = []; | |
private string $installedTranslationsDirectory; | |
private string $defaultTranslationsDirectory; | |
public function __construct( | |
private PathsHelper $pathsHelper, | |
private LoggerInterface $logger, | |
private CoreParametersHelper $coreParametersHelper, | |
private Client $client, | |
private TranslatorInterface $translator | |
) { | |
$this->defaultTranslationsDirectory = __DIR__.'/../Translations'; | |
$this->installedTranslationsDirectory = $this->pathsHelper->getSystemPath('translations_root').'/translations'; | |
$this->installer = new Installer($this->installedTranslationsDirectory); | |
// Moved to outside environment folder so that it doesn't get wiped on each config update | |
$this->cacheFile = $pathsHelper->getSystemPath('cache').'/../languageList.txt'; | |
} | |
/** | |
* @return array<string> | |
*/ | |
public function getSupportedLanguages(): array | |
{ | |
if (!empty($this->supportedLanguages)) { | |
return $this->supportedLanguages; | |
} | |
$this->loadSupportedLanguages(); | |
return $this->supportedLanguages; | |
} | |
/** | |
* Extracts a downloaded package for the specified language. | |
* | |
* This will attempt to download the package if it is not found | |
*/ | |
public function extractLanguagePackage($languageCode): array | |
{ | |
$packagePath = $this->pathsHelper->getSystemPath('cache').'/'.$languageCode.'.zip'; | |
// Make sure the package actually exists | |
if (!file_exists($packagePath)) { | |
// Let's try to fetch it | |
$result = $this->fetchPackage($languageCode); | |
// If there was a failure, there's nothing else we can do here | |
if ($result['error']) { | |
return $result; | |
} | |
} | |
$zipper = new \ZipArchive(); | |
$archive = $zipper->open($packagePath); | |
if (true !== $archive) { | |
$error = match ($archive) { | |
\ZipArchive::ER_EXISTS => 'mautic.core.update.archive_file_exists', | |
\ZipArchive::ER_INCONS, \ZipArchive::ER_INVAL, \ZipArchive::ER_MEMORY => 'mautic.core.update.archive_zip_corrupt', | |
\ZipArchive::ER_NOENT => 'mautic.core.update.archive_no_such_file', | |
\ZipArchive::ER_NOZIP => 'mautic.core.update.archive_not_valid_zip', | |
default => 'mautic.core.update.archive_could_not_open', | |
}; | |
return [ | |
'error' => true, | |
'message' => $error, | |
]; | |
} | |
// Extract the archive file now | |
$tempDir = $this->pathsHelper->getSystemPath('tmp'); | |
if (!$zipper->extractTo($tempDir)) { | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.update.archive_failed_to_extract', | |
]; | |
} | |
$this->installer->install($tempDir, $languageCode) | |
->cleanup(); | |
$zipper->close(); | |
// We can remove the package now | |
@unlink($packagePath); | |
return [ | |
'error' => false, | |
'message' => 'mautic.core.language.helper.language.saved.successfully', | |
]; | |
} | |
/** | |
* Fetches the list of available languages. | |
* | |
* @param bool $overrideCache | |
* | |
* @return array | |
*/ | |
public function fetchLanguages($overrideCache = false, $returnError = true) | |
{ | |
$overrideFile = $this->coreParametersHelper->get('language_list_file'); | |
if (!empty($overrideFile) && is_readable($overrideFile)) { | |
$overrideData = json_decode(file_get_contents($overrideFile), true); | |
if (isset($overrideData['languages'])) { | |
return $overrideData['languages']; | |
} elseif (isset($overrideData['name'])) { | |
return $overrideData; | |
} | |
return []; | |
} | |
// Check if we have a cache file and try to return cached data if so | |
if (!$overrideCache && is_readable($this->cacheFile)) { | |
$cacheData = json_decode(file_get_contents($this->cacheFile), true); | |
// If we're within the cache time, return the cached data | |
if ($cacheData['checkedTime'] > strtotime('-12 hours')) { | |
return $cacheData['languages']; | |
} | |
} | |
// Get the language data | |
try { | |
$data = $this->client->get( | |
$this->coreParametersHelper->get('translations_list_url'), | |
[\GuzzleHttp\RequestOptions::TIMEOUT => 10] | |
); | |
$manifest = json_decode($data->getBody(), true); | |
$languages = []; | |
// translate the manifest (plain array) to a format | |
// expected everywhere else inside mautic (locale keyed sorted array) | |
foreach ($manifest['languages'] as $lang) { | |
$languages[$lang['locale']] = $lang; | |
} | |
ksort($languages); | |
} catch (\Exception $exception) { | |
// Log the error | |
$this->logger->error('An error occurred while attempting to fetch the language list: '.$exception->getMessage()); | |
return (!$returnError) | |
? [] | |
: [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.fetching.languages', | |
]; | |
} | |
if (200 != $data->getStatusCode()) { | |
// Log the error | |
$this->logger->error( | |
sprintf( | |
'An unexpected %1$s code was returned while attempting to fetch the language. The message received was: %2$s', | |
$data->code, | |
(string) $data->getBody() | |
) | |
); | |
return (!$returnError) | |
? [] | |
: [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.fetching.languages', | |
]; | |
} | |
// Store to cache | |
$cacheData = [ | |
'checkedTime' => time(), | |
'languages' => $languages, | |
]; | |
file_put_contents($this->cacheFile, json_encode($cacheData)); | |
return $languages; | |
} | |
/** | |
* Fetches a language package from the remote server. | |
* | |
* @param string $languageCode | |
*/ | |
public function fetchPackage($languageCode): array | |
{ | |
// Check if we have a cache file, generate it if not | |
if (!is_readable($this->cacheFile)) { | |
$this->fetchLanguages(); | |
} | |
if (!is_readable($this->cacheFile)) { | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.fetching.languages', | |
]; | |
} | |
$cacheData = json_decode(file_get_contents($this->cacheFile), true); | |
// Make sure the language actually exists | |
if (!isset($cacheData['languages'][$languageCode])) { | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.invalid.language', | |
'vars' => [ | |
'%language%' => $languageCode, | |
], | |
]; | |
} | |
$langUrl = $this->coreParametersHelper->get('translations_fetch_url').$languageCode.'.zip'; | |
// GET the update data | |
try { | |
$data = $this->client->get($langUrl); | |
} catch (\Exception $exception) { | |
$this->logger->error('An error occurred while attempting to fetch the package: '.$exception->getMessage()); | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.fetching.package.exception', | |
'vars' => [ | |
'%exception%' => $exception->getMessage(), | |
], | |
]; | |
} | |
if ($data->getStatusCode() >= 300 && $data->getStatusCode() < 400) { | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.follow.redirects', | |
'vars' => [ | |
'%url%' => $langUrl, | |
], | |
]; | |
} elseif (200 != $data->getStatusCode()) { | |
return [ | |
'error' => true, | |
'message' => 'mautic.core.language.helper.error.on.language.server.side', | |
'vars' => [ | |
'%code%' => $data->getStatusCode(), | |
], | |
]; | |
} | |
// Set the filesystem target | |
$target = $this->pathsHelper->getSystemPath('cache').'/'.$languageCode.'.zip'; | |
// Write the response to the filesystem | |
file_put_contents($target, $data->getBody()); | |
// Return an array for the sake of consistency | |
return [ | |
'error' => false, | |
]; | |
} | |
/** | |
* Returns Mautic translation files. | |
* | |
* @param string[] $forBundles empty array means all bundles | |
* | |
* @return array<string,string[]> | |
*/ | |
public function getLanguageFiles(array $forBundles = []): array | |
{ | |
$files = []; | |
$mauticBundles = $this->coreParametersHelper->get('bundles'); | |
$pluginBundles = $this->coreParametersHelper->get('plugin.bundles'); | |
foreach (array_merge($mauticBundles, $pluginBundles) as $bundle) { | |
// Apply the bundle filter. | |
if (!empty($forBundles) && !in_array($bundle['bundle'], $forBundles)) { | |
continue; | |
} | |
// Parse the namespace into a filepath | |
$translationsDir = $bundle['directory'].'/Translations/en_US'; | |
if (is_dir($translationsDir)) { | |
$files[$bundle['bundle']] = []; | |
// Get files within the directory | |
$finder = new Finder(); | |
$finder->files()->in($translationsDir)->name('*.ini'); | |
/** @var \Symfony\Component\Finder\SplFileInfo $file */ | |
foreach ($finder as $file) { | |
$files[$bundle['bundle']][] = $file->getPathname(); | |
} | |
asort($files[$bundle['bundle']]); | |
} | |
} | |
return $files; | |
} | |
public function createLanguageFile(string $filePath, string $content): void | |
{ | |
$bundleDir = dirname($filePath, 1); | |
$languageDir = dirname($filePath, 2); | |
foreach ([$languageDir, $bundleDir] as $dir) { | |
if (is_dir($dir)) { | |
continue; | |
} | |
if (!mkdir($dir)) { | |
throw new \RuntimeException($this->translator->trans('mautic.core.command.transifex_error_creating_directory', ['%directory%' => $dir])); | |
} | |
} | |
if (!file_put_contents($filePath, $content)) { | |
throw new \RuntimeException($this->translator->trans('mautic.core.command.transifex_error_creating_file', ['%file%' => $filePath])); | |
} | |
} | |
private function loadSupportedLanguages(): void | |
{ | |
// Find available translations | |
$finder = new Finder(); | |
$finder | |
->directories() | |
->in($this->defaultTranslationsDirectory) | |
->in($this->installedTranslationsDirectory) | |
->ignoreDotFiles(true) | |
->depth('== 0'); | |
foreach ($finder as $dir) { | |
$locale = $dir->getFilename(); | |
// Check config exists | |
$configFile = $dir->getRealpath().'/config.json'; | |
if (!file_exists($configFile)) { | |
return; | |
} | |
$config = json_decode(file_get_contents($configFile), true); | |
$this->supportedLanguages[$locale] = (!empty($config['name'])) ? $config['name'] : $locale; | |
} | |
} | |
/** | |
* @return array<string> | |
*/ | |
public function getLanguageChoices(): array | |
{ | |
// Get the list of available languages | |
$languages = $this->fetchLanguages(false, false); | |
$choices = []; | |
foreach ($languages as $code => $langData) { | |
$choices[$langData['name']] = $code; | |
} | |
$choices = array_merge($choices, array_flip($this->getSupportedLanguages())); | |
// Alpha sort the languages by name | |
ksort($choices, SORT_FLAG_CASE | SORT_NATURAL); | |
return $choices; | |
} | |
} | |