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; | |
| } | |
| } | |