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