mautic / app /bundles /CoreBundle /Helper /LanguageHelper.php
chrisbryan17's picture
Upload folder using huggingface_hub
d2897cd verified
<?php
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;
}
}