diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 6e7194c..3c0745e 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1591,14 +1591,17 @@ function _install_prepare_import($langcode) { if ($info['major']) { $core = $info['major'] . '.x'; $data = array( - 'name' => 'drupal', + 'id' => 'drupal', + 'name' => 'Drupal core', 'project_type' => 'module', 'core' => $core, 'version' => $version, 'server_pattern' => $install_state['server_pattern'], 'status' => 1, ); - \Drupal::service('locale.project')->set($data['name'], $data); + $project = new \Drupal\locale\LocaleTranslatableProject($data); + $project->setLangcode($langcode); + \Drupal::service('locale.project')->set($project); module_load_include('compare.inc', 'locale'); locale_translation_check_projects_local(array('drupal'), array($install_state['parameters']['langcode'])); } diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index b174074..2211fd6 100644 --- a/core/modules/locale/locale.batch.inc +++ b/core/modules/locale/locale.batch.inc @@ -6,6 +6,7 @@ */ use GuzzleHttp\Exception\RequestException; +use Drupal\locale\LocaleTranslatableProject; /** * Load the common translation API. @@ -20,7 +21,7 @@ * Checks the presence and creation time po translation files in located at * remote server location and local file system. * - * @param string $project + * @param string $project_id * Machine name of the project for which to check the translation status. * @param string $langcode * Language code of the language for which to check the translation. @@ -33,53 +34,62 @@ * @param array $context * The batch context. */ -function locale_translation_batch_status_check($project, $langcode, $options = array(), &$context) { - $failure = $checked = FALSE; +function locale_translation_batch_status_check($project_id, $langcode, $options = array(), &$context) { + + $failure = FALSE; + $checked = FALSE; $options += array( 'finish_feedback' => TRUE, 'use_remote' => TRUE, ); - $source = locale_translation_get_status(array($project), array($langcode)); - $source = $source[$project][$langcode]; - // Check the status of local translation files. - if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { - if ($file = locale_translation_source_check_file($source)) { - locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file); - } - $checked = TRUE; - } + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->setLangcode($langcode); - // Check the status of remote translation files. - if ($options['use_remote'] && isset($source->files[LOCALE_TRANSLATION_REMOTE])) { - $remote_file = $source->files[LOCALE_TRANSLATION_REMOTE]; - if ($result = locale_translation_http_check($remote_file->uri)) { - // Update the file object with the result data. In case of a redirect we - // store the resulting uri. - if (isset($result['last_modified'])) { - $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri; - $remote_file->timestamp = $result['last_modified']; - locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_REMOTE, $remote_file); + // Check the status of local translation files. + if ($source = $project->getLocalSource()) { + if ($local_file = locale_translation_source_check_file($source)) { + $project->setLocalSource($local_file); } - // @todo What to do with when the file is not found (404)? To prevent - // re-checking within the TTL (1day, 1week) we can set a last_checked - // timestamp or cache the result. $checked = TRUE; } - else { - $failure = TRUE; + + // Check the status of remote translation files. + if ($options['use_remote'] && $project->remoteIsValid() && $remote_file = $project->getRemoteSource()) { + if ($result = locale_translation_http_check($remote_file->uri)) { + // Update the file object with the result data. In case of a redirect we + // store the resulting uri. + if (isset($result['last_modified'])) { + $remote_file->uri = isset($result['location']) ? $result['location'] : $remote_file->uri; + $remote_file->timestamp = $result['last_modified']; + $project->setRemoteSource($remote_file); + } + // @todo What to do with when the file is not found (404)? To prevent + // re-checking within the TTL (1day, 1week) we can set a last_checked + // timestamp or cache the result. + $checked = TRUE; + } + else { + $failure = TRUE; + } + } + + if ($checked) { + \Drupal::service('locale.project')->set($project); } - } - // Provide user feedback and record success or failure for reporting at the - // end of the batch. - if ($options['finish_feedback'] && $checked) { - $context['results']['files'][] = $source->name; - } - if ($failure && !$checked) { - $context['results']['failed_files'][] = $source->name; + // Provide user feedback and record success or failure for reporting at the + // end of the batch. + if ($options['finish_feedback'] && $checked) { + $context['results']['files'][] = $project->id(); + } + if ($failure && !$checked) { + $context['results']['failed_files'][] = $project->id(); + } + $context['message'] = t('Checked translation for %project.', array('%project' => $project->getName())); } - $context['message'] = t('Checked translation for %project.', array('%project' => $source->project)); } /** @@ -124,8 +134,8 @@ function locale_translation_batch_status_finished($success, $results) { * Downloads a remote gettext file into the translations directory. When * successfully the translation status is updated. * - * @param object $project - * Source object of the translatable project. + * @param string $project_id + * The id of the translatable project. * @param string $langcode * Language code. * @param array $context @@ -133,17 +143,20 @@ function locale_translation_batch_status_finished($success, $results) { * * @see locale_translation_batch_fetch_import() */ -function locale_translation_batch_fetch_download($project, $langcode, &$context) { - $sources = locale_translation_get_status(array($project), array($langcode)); - if (isset($sources[$project][$langcode])) { - $source = $sources[$project][$langcode]; - if (isset($source->type) && $source->type == LOCALE_TRANSLATION_REMOTE) { - if ($file = locale_translation_download_source($source->files[LOCALE_TRANSLATION_REMOTE], 'translations://')) { - $context['message'] = t('Downloaded translation for %project.', array('%project' => $source->project)); - locale_translation_status_save($source->name, $source->langcode, LOCALE_TRANSLATION_LOCAL, $file); +function locale_translation_batch_fetch_download($project_id, $langcode, &$context) { + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->setLangcode($langcode); + + if ($project->remoteIsAvailable() && $remote = $project->getRemoteSource()) { + if ($result = locale_translation_download_source($remote, 'translations://')) { + $context['message'] = t('Downloaded translation for %project.', array('%project' => $project->getName())); + $project->setLocalSource($result); + \Drupal::service('locale.project')->set($project); } else { - $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE]; + $context['results']['failed_files'][] = $remote; } } } @@ -155,8 +168,8 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) * Imports a gettext file from the translation directory. When successfully the * translation status is updated. * - * @param object $project - * Source object of the translatable project. + * @param string $project_id + * The id of the translatable project. * @param string $langcode * Language code. * @param array $options @@ -167,31 +180,32 @@ function locale_translation_batch_fetch_download($project, $langcode, &$context) * @see locale_translate_batch_import_files() * @see locale_translation_batch_fetch_download() */ -function locale_translation_batch_fetch_import($project, $langcode, $options, &$context) { - $sources = locale_translation_get_status(array($project), array($langcode)); - if (isset($sources[$project][$langcode])) { - $source = $sources[$project][$langcode]; - if (isset($source->type)) { - if ($source->type == LOCALE_TRANSLATION_REMOTE || $source->type == LOCALE_TRANSLATION_LOCAL) { - $file = $source->files[LOCALE_TRANSLATION_LOCAL]; - module_load_include('bulk.inc', 'locale'); - $options += array( - 'message' => t('Importing translation for %project.', array('%project' => $source->project)), - ); - // Import the translation file. For large files the batch operations is - // progressive and will be called repeatedly until finished. - locale_translate_batch_import($file, $options, $context); +function locale_translation_batch_fetch_import($project_id, $langcode, $options, &$context) { + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->setLangcode($langcode); + + if ($project->localIsLatest() && $local = $project->getLocalSource()) { + $local->langcode = $langcode; + module_load_include('bulk.inc', 'locale'); + $options += array( + 'message' => t('Importing translation for %project.', array('%project' => $project->getName())), + ); + // Import the translation file. For large files the batch operations is + // progressive and will be called repeatedly until finished. + locale_translate_batch_import($local, $options, $context); - // The import is finished. - if (isset($context['finished']) && $context['finished'] == 1) { - // The import is successful. - if (isset($context['results']['files'][$file->uri])) { - $context['message'] = t('Imported translation for %project.', array('%project' => $source->project)); + // The import is finished. + if (isset($context['finished']) && $context['finished'] == 1) { + // The import is successful. + if (isset($context['results']['files'][$local->uri])) { + $context['message'] = t('Imported translation for %project.', array('%project' => $project->getName())); - // Save the data of imported source into the {locale_file} table and - // update the current translation status. - locale_translation_status_save($project, $langcode, LOCALE_TRANSLATION_CURRENT, $source->files[LOCALE_TRANSLATION_LOCAL]); - } + // Update the current translation timestamp and translation history. + $project->setTimestamp($local->timestamp); + \Drupal::service('locale.project')->set($project); + locale_translation_update_file_history($local); } } } @@ -277,9 +291,8 @@ function locale_translation_http_check($uri) { * File object if download was successful. FALSE on failure. */ function locale_translation_download_source($source_file, $directory = 'temporary://') { - if ($uri = system_retrieve_file($source_file->uri, $directory)) { + if ($uri = system_retrieve_file($source_file->uri, $directory, FALSE, FILE_EXISTS_REPLACE)) { $file = clone($source_file); - $file->type = LOCALE_TRANSLATION_LOCAL; $file->uri = $uri; $file->directory = $directory; $file->timestamp = filemtime($uri); diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index c9bf165..6131c81 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -272,7 +272,7 @@ function locale_translate_batch_import_save($context) { // translation files are not tracked and are therefore not stored in this // table. if ($file->project && $file->version) { - $file->last_checked = REQUEST_TIME; + $file->lastChecked = REQUEST_TIME; locale_translation_update_file_history($file); } } diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc index e131d99..a8de91f 100644 --- a/core/modules/locale/locale.compare.inc +++ b/core/modules/locale/locale.compare.inc @@ -33,28 +33,21 @@ function locale_translation_flush_projects() { * array. * * @return array - * Array of project data: - * - "name": Project system name. - * - "project_type": Project type, e.g. 'module', 'theme'. - * - "core": Core release version, e.g. 8.x - * - "version": Project release version, e.g. 8.x-1.0 - * See http://drupalcode.org/project/drupalorg.git/blob/refs/heads/7.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l219 - * for how the version strings are created. - * - "server_pattern": Translation server po file pattern. - * - "status": Project status, 1 = enabled. + * Array of translatable projects \Drupal\locale\LocaleTranslatableProject */ function locale_translation_build_projects() { // This function depends on Update module. We degrade gracefully. if (!\Drupal::moduleHandler()->moduleExists('update')) { - return array(); + return \Drupal::service('locale.project')->getAll(); } + // Mark all existing projects as disabled. + \Drupal::service('locale.project')->disableAll(); + $existing_projects = \Drupal::service('locale.project')->getAll(); + // Get the project list based on .info.yml files. $projects = locale_translation_project_list(); - // Mark all previous projects as disabled and store new project data. - \Drupal::service('locale.project')->disableAll(); - $default_server = locale_translation_default_translation_server(); // If project is a dev release, or core, find the latest available release. @@ -87,8 +80,9 @@ function locale_translation_build_projects() { } // For every project store information. - $data += array( - 'name' => $name, + $project_data = array( + 'id' => $name, + 'name' => $data['info']['name'], 'version' => isset($data['info']['version']) ? $data['info']['version'] : '', 'core' => isset($data['info']['core']) ? $data['info']['core'] : \Drupal::CORE_COMPATIBILITY, // A project can provide the path and filename pattern to download the @@ -96,15 +90,27 @@ function locale_translation_build_projects() { 'server_pattern' => isset($data['info']['interface translation server pattern']) && $data['info']['interface translation server pattern'] ? $data['info']['interface translation server pattern'] : $default_server['pattern'], 'status' => !empty($data['project_status']) ? 1 : 0, ); - $project = (object) $data; - $projects[$name] = $project; - // Create or update the project record. - \Drupal::service('locale.project')->set($project->name, $data); + // Create a new translatable project or update an existing. + if (!isset($existing_projects[$name])) { + $project = new \Drupal\locale\LocaleTranslatableProject($project_data); + $project->initLangcodeMultiple(array_keys(locale_translatable_language_list())); + \Drupal::service('locale.project')->set($project); + $projects[$name] = $project; + } + else { + /** @var \Drupal\locale\LocaleTranslatableProject $project */ + $project = $existing_projects[$name]; + $project->update($project_data); + \Drupal::service('locale.project')->set($project); + $projects[$name] = $project; + } - // Invalidate the cache of translatable projects. - locale_translation_clear_cache_projects(); } + + // Invalidate the cache of translatable projects. + locale_translation_clear_cache_projects(); + return $projects; } @@ -324,11 +330,16 @@ function locale_translation_check_projects_local($projects = array(), $langcodes // For each project and each language we check if a local po file is // available. When found the source object is updated with the appropriate // type and timestamp of the po file. + /** @var \Drupal\locale\LocaleTranslatableProject $project */ foreach ($projects as $name => $project) { foreach ($langcodes as $langcode) { - $source = locale_translation_source_build($project, $langcode); - $file = locale_translation_source_check_file($source); - locale_translation_status_save($name, $langcode, LOCALE_TRANSLATION_LOCAL, $file); + $project->setLangcode($langcode); + $local = $project->getLocalSource(); + if ($local) { + $file = locale_translation_source_check_file($local); + $project->setLocalSource($file); + \Drupal::service('locale.project')->set($project); + } } } } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 197eded..67ec5a1 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -243,15 +243,18 @@ function locale_requirements($phase) { if ($languages) { // Determine the status of the translation updates per language. - $status = locale_translation_get_status(); - if ($status) { - foreach ($status as $project) { - foreach ($project as $langcode => $project_info) { - if (empty($project_info->type)) { - $untranslated[$langcode] = $languages[$langcode]->name; + $projects = \Drupal::service('locale.project')->getAll(); + if ($projects) { + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + foreach ($projects as $project) { + /** @var \Drupal\Core\Language\Language $language */ + foreach ($languages as $langcode => $language) { + $project->setLangcode($langcode); + if ($project->hasNoTranslation()) { + $untranslated[$langcode] = $language->getName(); } - elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) { - $available_updates[$langcode] = $languages[$langcode]->name; + elseif ($project->hasUpdates()) { + $available_updates[$langcode] = $language->getName(); } } } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 5740e4a..509f2a9 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -21,6 +21,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Component\Utility\Crypt; +use Drupal\locale\LocaleTranslatableProject; /** * Regular expression pattern used to localize JavaScript strings. @@ -121,21 +122,6 @@ const LOCALE_TRANSLATION_OVERWRITE_NONE = 'none'; /** - * Translation source is a remote file. - */ -const LOCALE_TRANSLATION_REMOTE = 'remote'; - -/** - * Translation source is a local file. - */ -const LOCALE_TRANSLATION_LOCAL = 'local'; - -/** - * Translation source is the current translation. - */ -const LOCALE_TRANSLATION_CURRENT = 'current'; - -/** * Implements hook_help(). */ function locale_help($route_name, RouteMatchInterface $route_match) { @@ -462,12 +448,12 @@ function locale_system_update(array $components) { // built-in support for translation imports in the installer. if (!drupal_installation_attempted() && locale_translatable_language_list() && \Drupal::config('locale.settings')->get('translation.import_enabled')) { module_load_include('compare.inc', 'locale'); + $projects = locale_translation_build_projects(); // Update the list of translatable projects and start the import batch. // Only when new projects are added the update batch will be triggered. Not // each enabled module will introduce a new project. E.g. sub modules. - $projects = array_keys(locale_translation_build_projects()); - if ($list = array_intersect($list, $projects)) { + if ($list = array_intersect($list, array_keys($projects))) { module_load_include('fetch.inc', 'locale'); // Get translation status of the projects, download and update // translations. @@ -513,12 +499,7 @@ function locale_system_remove($components) { locale_translate_delete_translation_files($list, array()); // Remove translatable projects. - // Followup issue http://drupal.org/node/1842362 to replace the - // {locale_project} table. Then change this to a function call. \Drupal::service('locale.project')->deleteMultiple($list); - - // Clear the translation status. - locale_translation_status_delete_projects($list); } } @@ -830,7 +811,7 @@ function locale_translation_get_file_history() { // Get file history from the database. $result = db_query('SELECT project, langcode, filename, version, uri, timestamp, last_checked FROM {locale_file}'); foreach ($result as $file) { - $file->type = $file->timestamp ? LOCALE_TRANSLATION_CURRENT : ''; + $file->type = $file->timestamp ? 'current' : ''; $history[$file->project][$file->langcode] = $file; } } @@ -855,11 +836,10 @@ function locale_translation_update_file_history($file) { ->fields(array( 'version' => $file->version, 'timestamp' => $file->timestamp, - 'last_checked' => $file->last_checked, + 'last_checked' => $file->lastChecked, )) ->execute(); // The file history has changed, flush the static cache now. - // @todo Can we make this more fine grained? drupal_static_reset('locale_translation_get_file_history'); return $status; } @@ -882,95 +862,32 @@ function locale_translation_file_history_delete($projects = array(), $langcodes $query->condition('langcode', $langcodes); } $query->execute(); + + // The file history has changed, flush the static cache now. + drupal_static_reset('locale_translation_get_file_history'); } /** * Gets the current translation status. * - * @todo What is 'translation status'? - */ -function locale_translation_get_status($projects = NULL, $langcodes = NULL) { - $result = array(); - $status = \Drupal::state()->get('locale.translation_status'); - module_load_include('translation.inc', 'locale'); - $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - - // Get the translation status of each project-language combination. If no - // status was stored, a new translation source is created. - foreach ($projects as $project) { - foreach ($langcodes as $langcode) { - if (isset($status[$project][$langcode])) { - $result[$project][$langcode] = $status[$project][$langcode]; - } - else { - $sources = locale_translation_build_sources(array($project), array($langcode)); - if (isset($sources[$project][$langcode])) { - $result[$project][$langcode] = $sources[$project][$langcode]; - } - } - } - } - return $result; -} - -/** - * Saves the status of translation sources in static cache. + * @param array|NULL $project_names + * Array of project machine names for which to get the translation status. + * Defaults to all projects. + * @param array|NULL $langcodes + * Array of language codes for which to get the translation status. Defaults + * to all languages. * - * @param string $project - * Machine readable project name. - * @param string $langcode - * Language code. - * @param string $type - * Type of data to be stored. - * @param array $data - * File object also containing timestamp when the translation is last updated. + * @return array + * Array of translatable project objects. */ -function locale_translation_status_save($project, $langcode, $type, $data) { - // Followup issue: http://drupal.org/node/1842362 - // Split status storage per module/language and expire individually. This will - // improve performance for large sites. - - // Load the translation status or build it if not already available. - module_load_include('translation.inc', 'locale'); - $status = locale_translation_get_status(); - if (empty($status)) { - $projects = locale_translation_get_projects(array($project)); - if (isset($projects[$project])) { - $status[$project][$langcode] = locale_translation_source_build($projects[$project], $langcode); - } - } - - // Merge the new status data with the existing status. - if (isset($status[$project][$langcode])) { - switch ($type) { - case LOCALE_TRANSLATION_REMOTE: - case LOCALE_TRANSLATION_LOCAL: - // Add the source data to the status array. - $status[$project][$langcode]->files[$type] = $data; - - // Check if this translation is the most recent one. Set timestamp and - // data type of the most recent translation source. - if (isset($data->timestamp) && $data->timestamp) { - if ($data->timestamp > $status[$project][$langcode]->timestamp) { - $status[$project][$langcode]->timestamp = $data->timestamp; - $status[$project][$langcode]->last_checked = REQUEST_TIME; - $status[$project][$langcode]->type = $type; - } - } - break; - - case LOCALE_TRANSLATION_CURRENT: - $data->last_checked = REQUEST_TIME; - $status[$project][$langcode]->timestamp = $data->timestamp; - $status[$project][$langcode]->last_checked = $data->last_checked; - $status[$project][$langcode]->type = $type; - locale_translation_update_file_history($data); - break; - } +// @todo deprecate ? +function locale_translation_get_status($project_names = NULL, $langcodes = NULL) { - \Drupal::state()->set('locale.translation_status', $status); - \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); + if ($project_names) { + return \Drupal::service('locale.project')->getMultiple($project_names); + } + else { + return \Drupal::service('locale.project')->getAll(); } } @@ -981,16 +898,14 @@ function locale_translation_status_save($project, $langcode, $type, $data) { * Language code(s) to be deleted from the cache. */ function locale_translation_status_delete_languages($langcodes) { - if ($status = locale_translation_get_status()) { - foreach ($status as $project => $languages) { - foreach ($languages as $langcode => $source) { - if (in_array($langcode, $langcodes)) { - unset($status[$project][$langcode]); - } - } + + $projects = \Drupal::service('locale.project')->getAll(); + foreach (array_keys($projects) as $name) { + foreach ($langcodes as $langcode) { + $projects[$name]->removeLangcode($langcode); } - \Drupal::state()->set('locale.translation_status', $status); } + \Drupal::service('locale.project')->setMultiple($projects); } /** @@ -999,22 +914,22 @@ function locale_translation_status_delete_languages($langcodes) { * @param array $projects * Project name(s) to be deleted from the cache. */ +// @todo Deprecate ? function locale_translation_status_delete_projects($projects) { - $status = locale_translation_get_status(); - foreach ($status as $project => $languages) { - if (in_array($project, $projects)) { - unset($status[$project]); - } - } - \Drupal::state()->set('locale.translation_status', $status); + \Drupal::service('locale.project')->deleteMultiple($projects); } /** * Clear the translation status cache. */ function locale_translation_clear_status() { - \Drupal::state()->delete('locale.translation_status'); + + $projects = \Drupal::service('locale.project')->getAll(); + foreach (array_keys($projects) as $name) { + $projects[$name]->clearTranslationStates(); + } + \Drupal::service('locale.project')->setMultiple($projects); \Drupal::state()->delete('locale.translation_last_checked'); } diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc index dc6043b..a6178ca 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -9,8 +9,9 @@ use Drupal\Core\Render\Element; use Drupal\locale\SourceString; use Drupal\locale\TranslationString; +use Drupal\Component\Utility\String; use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Drupal\locale\LocaleTranslatableProject; /** * Page callback: Checks for translation updates and displays the status. diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc index 2208b1d..e10a791 100644 --- a/core/modules/locale/locale.translation.inc +++ b/core/modules/locale/locale.translation.inc @@ -6,33 +6,6 @@ */ /** - * Comparison result of source files timestamps. - * - * Timestamp of source 1 is less than the timestamp of source 2. - * - * @see _locale_translation_source_compare() - */ -const LOCALE_TRANSLATION_SOURCE_COMPARE_LT = -1; - -/** - * Comparison result of source files timestamps. - * - * Timestamp of source 1 is equal to the timestamp of source 2. - * - * @see _locale_translation_source_compare() - */ -const LOCALE_TRANSLATION_SOURCE_COMPARE_EQ = 0; - -/** - * Comparison result of source files timestamps. - * - * Timestamp of source 1 is greater than the timestamp of source 2. - * - * @see _locale_translation_source_compare() - */ -const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1; - -/** * Get array of projects which are available for interface translation. * * This project data contains all projects which will be checked for available @@ -48,7 +21,7 @@ * Array of names of the projects to get. * * @return array - * Array of project data for translation update. + * Array of translatable project classes for translation update. * * @see locale_translation_build_projects() */ @@ -56,21 +29,19 @@ function locale_translation_get_projects($project_names = array()) { $projects = &drupal_static(__FUNCTION__, array()); if (empty($projects)) { - // Get project data from the database. + // Get project data from the key value store. $row_count = \Drupal::service('locale.project')->countProjects(); // http://drupal.org/node/1777106 is a follow-up issue to make the check for // possible out-of-date project information more robust. - if ($row_count == 0 && \Drupal::moduleHandler()->moduleExists('update')) { + if ($row_count == 0) { module_load_include('compare.inc', 'locale'); // At least the core project should be in the database, so we build the // data if none are found. locale_translation_build_projects(); } $projects = \Drupal::service('locale.project')->getAll(); - array_walk($projects, function(&$project) { - $project = (object) $project; - }); } + $projects = \Drupal::service('locale.project')->getAll(); // Return the requested project names or all projects. if ($project_names) { @@ -87,63 +58,6 @@ function locale_translation_clear_cache_projects() { } /** - * Loads cached translation sources containing current translation status. - * - * @param array $projects - * Array of project names. Defaults to all translatable projects. - * @param array $langcodes - * Array of language codes. Defaults to all translatable languages. - * - * @return array - * Array of source objects. Keyed with :. - * - * @see locale_translation_source_build() - */ -function locale_translation_load_sources($projects = NULL, $langcodes = NULL) { - $sources = array(); - $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - - // Load source data from locale_translation_status cache. - $status = locale_translation_get_status(); - - // Use only the selected projects and languages for update. - foreach ($projects as $project) { - foreach ($langcodes as $langcode) { - $sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL; - } - } - return $sources; -} - -/** - * Build translation sources. - * - * @param array $projects - * Array of project names. Defaults to all translatable projects. - * @param array $langcodes - * Array of language codes. Defaults to all translatable languages. - * - * @return array - * Array of source objects. Keyed by project name and language code. - * - * @see locale_translation_source_build() - */ -function locale_translation_build_sources($projects = array(), $langcodes = array()) { - $sources = array(); - $projects = locale_translation_get_projects($projects); - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - - foreach ($projects as $project) { - foreach ($langcodes as $langcode) { - $source = locale_translation_source_build($project, $langcode); - $sources[$source->name][$source->langcode] = $source; - } - } - return $sources; -} - -/** * Checks whether a po file exists in the local filesystem. * * It will search in the directory set in the translation source. Which defaults @@ -164,136 +78,21 @@ function locale_translation_build_sources($projects = array(), $langcodes = arra * - "uri": File name and path. * - "timestamp": Last updated time of the po file. * FALSE if the file is not found. - * - * @see locale_translation_source_build() */ -function locale_translation_source_check_file($source) { - if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { - $source_file = $source->files[LOCALE_TRANSLATION_LOCAL]; - $directory = $source_file->directory; - $filename = '/' . preg_quote($source_file->filename) . '$/'; +function locale_translation_source_check_file($source_file) { + $directory = $source_file->directory; + $filename = '/' . preg_quote($source_file->filename) . '$/'; - if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) { - $file = current($files); - $source_file->uri = $file->uri; - $source_file->timestamp = filemtime($file->uri); - return $source_file; - } + if ($files = file_scan_directory($directory, $filename, array('key' => 'name', 'recurse' => FALSE))) { + $file = current($files); + $source_file->uri = $file->uri; + $source_file->timestamp = filemtime($file->uri); + return $source_file; } return FALSE; } /** - * Builds abstract translation source. - * - * @param object $project - * Project object. - * @param string $langcode - * Language code. - * @param string $filename - * File name of translation file. May contain placeholders. - * - * @return object - * Source object: - * - "project": Project name. - * - "name": Project name (inherited from project). - * - "language": Language code. - * - "core": Core version (inherited from project). - * - "version": Project version (inherited from project). - * - "project_type": Project type (inherited from project). - * - "files": Array of file objects containing properties of local and remote - * translation files. - * Other processes can add the following properties: - * - "type": Most recent translation source found. LOCALE_TRANSLATION_REMOTE - * and LOCALE_TRANSLATION_LOCAL indicate available new translations, - * LOCALE_TRANSLATION_CURRENT indicate that the current translation is them - * most recent. "type" sorresponds with a key of the "files" array. - * - "timestamp": The creation time of the "type" translation (file). - * - "last_checked": The time when the "type" translation was last checked. - * The "files" array can hold file objects of type: - * LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE and - * LOCALE_TRANSLATION_CURRENT. Each contains following properties: - * - "type": The object type (LOCALE_TRANSLATION_LOCAL, - * LOCALE_TRANSLATION_REMOTE, etc. see above). - * - "project": Project name. - * - "langcode": Language code. - * - "version": Project version. - * - "uri": Local or remote file path. - * - "directory": Directory of the local po file. - * - "filename": File name. - * - "timestamp": Timestamp of the file. - * - "keep": TRUE to keep the downloaded file. - */ -function locale_translation_source_build($project, $langcode, $filename = NULL) { - // Followup issue: http://drupal.org/node/1842380 - // Convert $source object to a TranslatableProject class and use a typed class - // for $source-file. - - // Create a source object with data of the project object. - $source = clone $project; - $source->project = $project->name; - $source->langcode = $langcode; - $source->type = ''; - $source->timestamp = 0; - $source->last_checked = 0; - - $filename = $filename ? $filename : \Drupal::config('locale.settings')->get('translation.default_filename'); - - // If the server_pattern contains a remote file path we will check for a - // remote file. The local version of this file will only be checked if a - // translations directory has been defined. If the server_pattern is a local - // file path we will only check for a file in the local file system. - $files = array(); - if (_locale_translation_file_is_remote($source->server_pattern)) { - $files[LOCALE_TRANSLATION_REMOTE] = (object) array( - 'project' => $project->name, - 'langcode' => $langcode, - 'version' => $project->version, - 'type' => LOCALE_TRANSLATION_REMOTE, - 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), - 'uri' => locale_translation_build_server_pattern($source, $source->server_pattern), - ); - $files[LOCALE_TRANSLATION_LOCAL] = (object) array( - 'project' => $project->name, - 'langcode' => $langcode, - 'version' => $project->version, - 'type' => LOCALE_TRANSLATION_LOCAL, - 'filename' => locale_translation_build_server_pattern($source, $filename), - 'directory' => 'translations://', - ); - $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename; - } - else { - $files[LOCALE_TRANSLATION_LOCAL] = (object) array( - 'project' => $project->name, - 'langcode' => $langcode, - 'version' => $project->version, - 'type' => LOCALE_TRANSLATION_LOCAL, - 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), - 'directory' => locale_translation_build_server_pattern($source, drupal_dirname($source->server_pattern)), - ); - $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . '/' . $files[LOCALE_TRANSLATION_LOCAL]->filename; - } - $source->files = $files; - - // If this project+language is already translated, we add its status and - // update the current translation timestamp and last_updated time. If the - // project+language is not translated before, create a new record. - $history = locale_translation_get_file_history(); - if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) { - $source->files[LOCALE_TRANSLATION_CURRENT] = $history[$project->name][$langcode]; - $source->type = LOCALE_TRANSLATION_CURRENT; - $source->timestamp = $history[$project->name][$langcode]->timestamp; - $source->last_checked = $history[$project->name][$langcode]->last_checked; - } - else { - locale_translation_update_file_history($source); - } - - return $source; -} - -/** * Build path to translation source, out of a server path replacement pattern. * * @param object $project @@ -329,7 +128,7 @@ function locale_cron_fill_queue() { $last = REQUEST_TIME - $config->get('translation.update_interval_days') * 3600 * 24; $projects = \Drupal::service('locale.project')->getAll(); $projects = array_filter($projects, function($project) { - return $project['status'] == 1; + return $project->getStatus(); }); $files = db_select('locale_file', 'f') ->condition('f.project', array_keys($projects)) @@ -382,45 +181,6 @@ function _locale_translation_file_is_remote($uri) { } /** - * Compare two update sources, looking for the newer one. - * - * The timestamp property of the source objects are used to determine which is - * the newer one. - * - * @param object $source1 - * Source object of the first translation source. - * @param object $source2 - * Source object of available update. - * - * @return int - * - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1 - * is missing. - * - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ": $source1 == $source2 OR both - * $source1 and $source2 are missing. - * - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT": $source1 > $source2 OR $source2 - * is missing. - */ -function _locale_translation_source_compare($source1, $source2) { - if (isset($source1->timestamp) && isset($source2->timestamp)) { - if ($source1->timestamp == $source2->timestamp) { - return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; - } - else { - return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT; - } - } - elseif (isset($source1->timestamp) && !isset($source2->timestamp)) { - return LOCALE_TRANSLATION_SOURCE_COMPARE_GT; - } - elseif (!isset($source1->timestamp) && isset($source2->timestamp)) { - return LOCALE_TRANSLATION_SOURCE_COMPARE_LT; - } - else { - return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; - } -} - -/** * Returns default import options for translation update. * * @return array diff --git a/core/modules/locale/src/AbstractProjectSource.php b/core/modules/locale/src/AbstractProjectSource.php new file mode 100644 index 0000000..8d3acb5 --- /dev/null +++ b/core/modules/locale/src/AbstractProjectSource.php @@ -0,0 +1,61 @@ +latestSource = ''; + $this->latestSourceTimestamp = 0; + } + + /** + * Builds uri of a translation file out of a server path replacement pattern. + * + * @param string $template + * Uri template string containing placeholders. Available placeholders: + * - "%project": Project name. + * - "%version": Project version. + * - "%core": Project core version. + * - "%language": Language code. + * @param string $langcode + * Language code of the translation file. + * + * @return string + * File uri with replaced placeholders. + */ + public static function buildServerPattern($project, $langcode) { + $variables = array( + '%project' => $project->id, + '%version' => $project->projectVersion, + '%core' => $project->projectCore, + '%language' => $langcode, + ); + + return strtr($project->serverPattern, $variables); + } + + /** + * Returns whether the file URI is at a remote translation source. + * + * @param $uri + * File URI. May include a scheme. + * + * @return bool + * Returns TRUE if the file scheme indicates a remote file. FALSE if no + * scheme is supplied or files are stored at the local file system. + */ + public static function fileIsRemote($uri) { + + $scheme = file_uri_scheme($uri); + if ($scheme) { + return !drupal_realpath($scheme . '://'); + } + + return FALSE; + } +} \ No newline at end of file diff --git a/core/modules/locale/src/Controller/LocaleController.php b/core/modules/locale/src/Controller/LocaleController.php index eb25c4b..18dccdc 100644 --- a/core/modules/locale/src/Controller/LocaleController.php +++ b/core/modules/locale/src/Controller/LocaleController.php @@ -26,9 +26,9 @@ public function checkTranslation() { $this->moduleHandler()->loadInclude('locale', 'inc', 'locale.compare'); // Check translation status of all translatable project in all languages. - // First we clear the cached list of projects. Although not strictly - // necessary, this is helpful in case the project list is out of sync. - locale_translation_flush_projects(); + // First we rebuild list of projects. Although not strictly necessary, + // this is helpful in case the project list is out of sync. + locale_translation_build_projects(); locale_translation_check_projects(); // Execute a batch if required. A batch is only used when remote files diff --git a/core/modules/locale/src/Form/TranslationStatusForm.php b/core/modules/locale/src/Form/TranslationStatusForm.php index df43b62..725f063 100644 --- a/core/modules/locale/src/Form/TranslationStatusForm.php +++ b/core/modules/locale/src/Form/TranslationStatusForm.php @@ -4,7 +4,6 @@ * @file * Contains \Drupal\locale\Form\TranslationStatusForm. */ - namespace Drupal\locale\Form; use Drupal\Component\Utility\String; @@ -70,15 +69,16 @@ public function getFormID() { */ public function buildForm(array $form, FormStateInterface $form_state) { $languages = locale_translatable_language_list(); - $status = locale_translation_get_status(); + $projects = \Drupal::service('locale.project')->getAll(); $options = array(); $languages_update = array(); $languages_not_found = array(); $projects_update = array(); + // Prepare information about projects which have available translation // updates. - if ($languages && $status) { - $updates = $this->prepareUpdateData($status); + if ($languages && $projects) { + $updates = $this->prepareUpdateData($projects, $languages); // Build data options for the select table. foreach ($updates as $langcode => $update) { @@ -105,7 +105,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { if (!empty($update['not_found'])) { $languages_not_found[$langcode] = $langcode; } - elseif (!empty($update['updates'])) { + if (!empty($update['updates'])) { $languages_update[$langcode] = $langcode; } } @@ -138,7 +138,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '@add_language' => $this->url('language.admin_overview'), )); } - elseif ($status) { + elseif ($projects) { $empty = $this->t('All translations up to date.'); } else { @@ -183,41 +183,37 @@ public function buildForm(array $form, FormStateInterface $form_state) { /** * Prepare information about projects with available translation updates. * - * @param array $status - * Translation update status as an array keyed by Project ID and langcode. + * @param array $projects + * Translatable projects as an array keyed by project ID. + * @param array $languages + * Translatable languages as an array keyed by langcode. * * @return array * Translation update status as an array keyed by language code and * translation update status. */ - protected function prepareUpdateData(array $status) { + protected function prepareUpdateData(array $projects, array $languages) { $updates = array(); - // @todo Calling locale_translation_build_projects() is an expensive way to - // get a module name. In follow-up issue http://drupal.org/node/1842362 - // the project name will be stored to display use, like here. - $this->moduleHandler->loadInclude('locale', 'compare.inc'); - $project_data = locale_translation_build_projects(); - - foreach ($status as $project_id => $project) { - foreach ($project as $langcode => $project_info) { + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + foreach ($projects as $project_id => $project) { + /** @var \Drupal\Core\Language\Language $language */ + foreach ($languages as $langcode => $language) { + $project->setLangcode($langcode); // No translation file found for this project-language combination. - if (empty($project_info->type)) { + if ($project->hasNoTranslation()) { $updates[$langcode]['not_found'][] = array( - 'name' => $project_info->name == 'drupal' ? $this->t('Drupal core') : $project_data[$project_info->name]->info['name'], - 'version' => $project_info->version, - 'info' => $this->createInfoString($project_info), + 'name' => $project->getName(), + 'version' => $project->getVersion(), + 'info' => $this->createInfoString($project), ); } // Translation update found for this project-language combination. - elseif ($project_info->type == LOCALE_TRANSLATION_LOCAL || $project_info->type == LOCALE_TRANSLATION_REMOTE) { - $local = isset($project_info->files[LOCALE_TRANSLATION_LOCAL]) ? $project_info->files[LOCALE_TRANSLATION_LOCAL] : NULL; - $remote = isset($project_info->files[LOCALE_TRANSLATION_REMOTE]) ? $project_info->files[LOCALE_TRANSLATION_REMOTE] : NULL; - $recent = _locale_translation_source_compare($local, $remote) == LOCALE_TRANSLATION_SOURCE_COMPARE_LT ? $remote : $local; + elseif ($project->hasUpdates()) { $updates[$langcode]['updates'][] = array( - 'name' => $project_data[$project_info->name]->info['name'], - 'version' => $project_info->version, - 'timestamp' => $recent->timestamp, + 'name' => $project->getName(), + 'version' => $project->getVersion(), + 'timestamp' => $project->getLatestSourceTimestamp(), ); } } @@ -237,17 +233,19 @@ protected function prepareUpdateData(array $status) { * Translations for development versions are never fetched, so the debug info * for that is a fixed message. * - * @param array $project_info - * An array which is the project information of the source. + * @param \Drupal\locale\LocaleTranslatableProjectInterface $project + * The translatable project object. * * @return string * The string which contains debug information. */ - protected function createInfoString($project_info) { - $remote_path = isset($project_info->files['remote']->uri) ? $project_info->files['remote']->uri : FALSE; - $local_path = isset($project_info->files['local']->uri) ? $project_info->files['local']->uri : FALSE; + protected function createInfoString(\Drupal\locale\LocaleTranslatableProjectInterface $project) { + $remote_source = $project->getRemoteSource(); + $local_source = $project->getLocalSource(); + $remote_path = isset($remote_source->uri) ? $remote_source->uri : FALSE; + $local_path = isset($local_source->uri) ? $local_source->uri : FALSE; - if (strpos($project_info->version, 'dev') !== FALSE) { + if (strpos($project->getVersion(), 'dev') !== FALSE) { return $this->t('No translation files are provided for development releases.'); } if (locale_translation_use_remote_source() && $remote_path && $local_path) { diff --git a/core/modules/locale/src/LocalProjectSource.php b/core/modules/locale/src/LocalProjectSource.php new file mode 100644 index 0000000..05b8f26 --- /dev/null +++ b/core/modules/locale/src/LocalProjectSource.php @@ -0,0 +1,32 @@ +get('translation.default_filename'); + $source = new \stdClass(); + $server_pattern = $project->getServerPattern(); + if (self::fileIsRemote($server_pattern)) { + $source->filename = self::buildServerPattern($filename, $langcode); + $source->directory = 'translations://'; + $source->uri = $source->directory . $source->filename; + } + else { + $source->filename = self::buildServerPattern(basename($server_pattern), $langcode); + $source->directory = self::buildServerPattern(drupal_dirname($server_pattern), $langcode); + $source->uri = $source->directory . '/' . $source->filename; + } + + return $source; + } + +} \ No newline at end of file diff --git a/core/modules/locale/src/LocaleProjectStorage.php b/core/modules/locale/src/LocaleProjectStorage.php index 65c83fc..7cbf8cf 100644 --- a/core/modules/locale/src/LocaleProjectStorage.php +++ b/core/modules/locale/src/LocaleProjectStorage.php @@ -25,6 +25,7 @@ class LocaleProjectStorage implements LocaleProjectStorageInterface { * Static state cache. * * @var array + * Array of translatable project classes. */ protected $cache = array(); @@ -59,6 +60,7 @@ public function get($key, $default = NULL) { public function getMultiple(array $keys) { $values = array(); $load = array(); + foreach ($keys as $key) { // Check if we have a value in the cache. if (isset($this->cache[$key])) { @@ -76,8 +78,8 @@ public function getMultiple(array $keys) { // If we find a value, even one that is NULL, add it to the cache and // return it. if (isset($loaded_values[$key])) { - $values[$key] = $loaded_values[$key]; - $this->cache[$key] = $loaded_values[$key]; + $values[$key] = $this->buildProject($loaded_values[$key]); + $this->cache[$key] = $values[$key]; } else { $this->cache[$key] = NULL; @@ -91,16 +93,17 @@ public function getMultiple(array $keys) { /** * {@inheritdoc} */ - public function set($key, $value) { - $this->setMultiple(array($key => $value)); + public function set(\Drupal\locale\LocaleTranslatableProjectInterface $project) { + $this->setMultiple(array($project->id() => $project)); } /** * {@inheritdoc} */ public function setMultiple(array $data) { - foreach ($data as $key => $value) { - $this->cache[$key] = $value; + foreach ($data as $key => $project) { + $this->cache[$key] = $project; + $data[$key] = $project->toArray(); } $this->keyValueStore->setMultiple($data); } @@ -142,15 +145,15 @@ public function deleteAll() { * {@inheritdoc} */ public function disableAll() { - $projects = $this->keyValueStore->getAll(); + $projects = $this->getAll(); foreach (array_keys($projects) as $key) { - $projects[$key]['status'] = 0; + $projects[$key]->setStatus(FALSE); if (isset($cache[$key])) { $cache[$key] = $projects[$key]; } } - $this->keyValueStore->setMultiple($projects); + $this->setMultiple($projects); } /** @@ -165,9 +168,23 @@ public function countProjects() { */ public function getAll() { if (!static::$all) { - $this->cache = $this->keyValueStore->getAll(); + foreach ($this->keyValueStore->getAll() as $key => $value) { + $this->cache[$key] = $this->buildProject($value); + } static::$all = TRUE; } return $this->cache; } + + /** + * Builds translatable project object. + * + * @param array $data + * Project data. + * + * @return LocaleTranslatableProject + */ + protected function buildProject($data) { + return new \Drupal\locale\LocaleTranslatableProject($data); + } } diff --git a/core/modules/locale/src/LocaleProjectStorageInterface.php b/core/modules/locale/src/LocaleProjectStorageInterface.php index 876f05a..d75910d 100644 --- a/core/modules/locale/src/LocaleProjectStorageInterface.php +++ b/core/modules/locale/src/LocaleProjectStorageInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\locale\LocalProjectStorageInterface. + * Contains Drupal\locale\LocaleProjectStorageInterface. */ namespace Drupal\locale; @@ -20,7 +20,7 @@ * @param mixed $default * The default value to use if the key is not found. * - * @return mixed + * @return \Drupal\locale\LocaleTranslatableProjectInterface|NULL * The stored value, or the default value if no value exists. */ public function get($key, $default = NULL); @@ -39,12 +39,10 @@ public function getMultiple(array $keys); /** * Creates or updates the project record. * - * @param string $key - * The key of the data to store. - * @param mixed $value - * The data to store. + * @param \Drupal\locale\LocaleTranslatableProjectInterface $project + * The project to store. */ - public function set($key, $value); + public function set(\Drupal\locale\LocaleTranslatableProjectInterface $project); /** * Creates or updates multiple project records. diff --git a/core/modules/locale/src/LocaleProjectTranslationState.php b/core/modules/locale/src/LocaleProjectTranslationState.php new file mode 100644 index 0000000..71ba541 --- /dev/null +++ b/core/modules/locale/src/LocaleProjectTranslationState.php @@ -0,0 +1,205 @@ + $property) { + $state->{$property_name} = $property; + } + + return $state; + } + + /** + * Returns the timestamp of the latest source. + * @return int + */ + public function getLatestSourceTimestamp() { + return $this->latestSourceTimestamp; + } + + /** + * Returns true if the latest source is remote. + * @return bool + */ + public function remoteIsLatest() { + return $this->latestSource == 'remote'; + } + + /** + * @inheritdoc + */ + public function remoteIsValid() { + $this->remoteSource->isValid(); + } + + /** + * @inheritdoc + */ + public function remoteIsAvailable() { + $this->remoteSource->isAvailable(); + } + + /** + * Returns true if the latest source is local. + * @return bool + */ + public function localIsLatest() { + return $this->latestSource == 'local'; + } + + /** + * Clears translation states for each local and remote source. + * @return void + */ + public function clearTranslationStates() { + if ($this->latestSource == 'local' || $this->latestSource == 'remote') { + $this->localSource->clearTranslationState(); + $this->remoteSource->clearTranslationState(); + } + } + + /** + * Set a new local source from the data array. + * @param $data + * An array containing the data for the local source. + */ + public function setLocalSource($data) { + if (isset($data->timestamp)) { + $local_source = $this->localSource; + $local_source->filename = $data->filename; + $local_source->directory = $data->directory; + $local_source->uri = $data->uri; + $local_source->timestamp = $data->timestamp; + + // If the local translation is the most recent, we update the latestSource + // status. Otherwise only mark the current translation as checked. If remote + // and local have the same timestamp, the local source is chosen. + if ($data->timestamp > $this->latestSourceTimestamp || ($data->timestamp >= $this->latestSourceTimestamp && $this->latestSource == 'remote')) { + $this->latestSource = 'local'; + $this->latestSourceTimestamp = $data->timestamp; + } + else { + $this->lastChecked = REQUEST_TIME; + } + } + + $this->localSource->lastChecked = REQUEST_TIME; + } + + /** + * Set a new remote source from the data array. + * @param $data + * An array containing the data for the local source. + */ + public function setRemoteSource($data) { + if (isset($data->timestamp)) { + $remote_source = $this->remoteSource; + $remote_source->filename = $data->filename; + $remote_source->uri = $data->uri; + $remote_source->timestamp = $data->timestamp; + + // If the remote translation is the most recent, we update the + // latestSource status. Otherwise mark the current translation as checked. + if ($data->timestamp > $this->latestSourceTimestamp) { + $this->latestSource = 'remote'; + $this->latestSourceTimestamp = $data->timestamp; + } + else { + $this->lastChecked = REQUEST_TIME; + } + } + + $this->remoteSource->lastChecked = REQUEST_TIME; + } + + /** + * Sets the time when the translation state was last checked. + * @param $timestamp + * The timestamp when the state was last checked. + */ + public function setTimestamp($timestamp) { + $this->timestamp = $timestamp; + $this->lastChecked = REQUEST_TIME; + + $this->latestSource = 'current'; + $this->latestSourceTimestamp = $timestamp; + } + + /** + * Returns true if the project is translated. + * @return bool + */ + public function isTranslated() { + return $this->timestamp > 0; + } + + /** + * Returns true if the latest source has updates. + * @return bool + */ + public function hasUpdates() { + return $this->latestSource == 'local'|| $this->latestSource == 'remote'; + } + + /** + * Returns true if we don't have translations available. + * @return bool + */ + public function hasNoTranslation() { + return $this->latestSource == ''; + } + + /** + * Returns true if the translations are up to date. + * @return bool + */ + public function isUpToDate() { + return $this->latestSource == 'current'; + } +} \ No newline at end of file diff --git a/core/modules/locale/src/LocaleProjectTranslationStateInterface.php b/core/modules/locale/src/LocaleProjectTranslationStateInterface.php new file mode 100644 index 0000000..16bba74 --- /dev/null +++ b/core/modules/locale/src/LocaleProjectTranslationStateInterface.php @@ -0,0 +1,86 @@ +importData($data); + } + + /** + * @inheritdoc + */ + public function id() { + + return $this->id; + } + + /** + * @inheritdoc + */ + public function getName() { + + return $this->name; + } + + /** + * @inheritdoc + */ + public function update($data) { + + $old_version = $this->projectVersion; + $old_server_pattern = $this->serverPattern; + + unset($data['id']); + $this->importData($data); + + // Rebuild local and remote source and clear pending update states only + // if version or server pattern has changed. + if ((isset($data['version']) && $old_version != $this->projectVersion) + || (isset($data['server_pattern']) && $old_server_pattern != $this->serverPattern)) { + foreach ($this->stateByLanguage as $langcode => $state) { + $state->localSource = ProjectSource::create($this, $langcode); + $state->remoteSource = RemoteProjectSource::create($this, $langcode); + $state->clearTranslationStates(); + } + } + } + + /** + * @inheritdoc + */ + public function toArray() { + + $result = array(); + + foreach ($this->getMapping() as $key => $property) { + $result[$key] = $this->$property; + } + + return $result; + } + + /** + * @inheritdoc + */ + public function setLangcode($langcode) { + + $this->langcode = $langcode; + $this->initLangcode($langcode); + } + + /** + * @inheritdoc + */ + public function initLangcode($langcode) { + + $this->initLangcodeMultiple(array($langcode)); + } + + /** + * @inheritdoc + */ + public function initLangcodeMultiple(array $langcodes) { + + foreach ($langcodes as $langcode) { + if (empty($this->stateByLanguage[$langcode])) { + $this->initState($langcode); + } + } + } + + /** + * @inheritdoc + */ + public function removeLangcode($langcode) { + unset($this->stateByLanguage[$langcode]); + } + + /** + * @inheritdoc + */ + public function getStatus() { + return $this->status; + } + + /** + * @inheritdoc + */ + public function setStatus($status) { + $this->status = (bool) $status; + } + + /** + * @inheritdoc + */ + public function getLocalSource() { + $source = NULL; + + if (!empty($this->stateByLanguage[$this->langcode]->localSource)) { + $source = $this->stateByLanguage[$this->langcode]->localSource; + } + + return $source; + } + + /** + * @inheritdoc + */ + public function getRemoteSource() { + $source = NULL; + + if (!empty($this->stateByLanguage[$this->langcode]->remoteSource)) { + $source = $this->stateByLanguage[$this->langcode]->localSource; + } + + return $source; + } + + /** + * @inheritdoc + */ + public function getLatestSourceTimestamp() { + return $this->getCurrentState()->getLatestSourceTimestamp; + } + + /** + * @inheritdoc + */ + public function getVersion() { + + return $this->projectVersion; + } + + /** + * @inheritdoc + */ + public function setVersion($version) { + + $this->projectVersion = $version; + } + + /** + * @inheritdoc + */ + public function remoteIsLatest() { + return $this->getCurrentState()->remoteIsLatest(); + } + + /** + * @inheritdoc + */ + public function remoteIsValid() { + return $this->getCurrentState()->remoteIsValid(); + } + + /** + * @inheritdoc + */ + public function remoteIsAvailable() { + return $this->getCurrentState()->remoteIsAvailable(); + } + + /** + * @inheritdoc + */ + public function localIsLatest() { + return $this->getCurrentState()->localIsLatest(); + } + + /** + * @inheritdoc + */ + public function setLocalSource($data) { + $this->getCurrentState()->setLocalSource($data); + + } + + /** + * @inheritdoc + */ + public function setRemoteSource($data) { + $this->getCurrentState()->setRemoteSource($data); + } + + /** + * @inheritdoc + */ + public function setTimestamp($timestamp) { + $this->getCurrentState()->setTimestamp($timestamp); + } + + /** + * @inheritdoc + */ + public function isTranslated() { + return $this->getCurrentState()->isTranslated(); + } + + /** + * @inheritdoc + */ + public function hasUpdates() { + return $this->getCurrentState()->hasUpdates(); + } + + /** + * @inheritdoc + */ + public function hasNoTranslation() { + return $this->getCurrentState()->hasNoTranslation(); + } + + /** + * @inheritdoc + */ + public function isUpToDate() { + return $this->getCurrentState()->isUpToDate(); + } + + /** + * Returns the server pattern. + * @return string + */ + public function getServerPattern() { + return $this->serverPattern; + } + + /** + * Import data into the class from array data structure. + * + * @param array $data + * Array of project data. + */ + protected function importData($data) { + + foreach ($this->getMapping() as $key => $property) { + if (isset($data[$key])) { + $this->$property = $data[$key]; + } + } + + // Override the module name of drupal core. The default name is the name + // of the first extension (e.g. 'Action'). + if ($this->id == 'drupal') { + $this->name = 'Drupal core'; + } + } + + /* + * Initializes the translation state by loading from cache or building a new one. + * + * @param string $langcode + * Language code of the translation sources. + */ + protected function initState($langcode) { + + if (!$this->getCurrentState()) { + $data = array( + 'timestamp' => 0, + 'lastChecked' => 0, + 'latestSource' => '', + 'latestSourceTimestamp' => 0, + 'localSource' => LocalProjectSource::create($this, $langcode), + 'remoteSource' => RemoteProjectSource::create($this, $langcode), + ); + $this->setCurrentState(LocaleProjectTranslationState::create($data), $langcode); + } + } + + /** + * Returns mapping for array to properties. + * + * @return array + * Key value array of mapping values. Key: storage array key; value: class + * property name. + */ + protected function getMapping() { + + // A limited set of properties are mapped. Only those that are stored. + return array( + 'id' => 'id', + 'name' => 'name', + 'version' => 'projectVersion', + 'core' => 'projectCore', + 'status' => 'status', + 'server_pattern' => 'serverPattern', + 'state_by_language' => 'stateByLanguage', + ); + } + + /**r + * Returns the current LocalProjectState depending on the language. + * @return LocaleProjectTranslationState + */ + protected function getCurrentState() { + return isset($this->stateByLanguage[$this->langcode]) ? $this->stateByLanguage[$this->langcode] : null; + } + + /** + * @param LocaleProjectTranslationState $state + * The project translation state. + * @param string $langcode + * The language code. + */ + protected function setCurrentState($state, $langcode) { + $this->stateByLanguage[$langcode] = $state; + } +} diff --git a/core/modules/locale/src/LocaleTranslatableProjectInterface.php b/core/modules/locale/src/LocaleTranslatableProjectInterface.php new file mode 100644 index 0000000..83a5054 --- /dev/null +++ b/core/modules/locale/src/LocaleTranslatableProjectInterface.php @@ -0,0 +1,148 @@ +getServerPattern(); + + if (self::fileIsRemote($server_pattern)) { + $source->filename = self::buildServerPattern(basename($server_pattern), $langcode); + $source->uri = self::buildServerPattern($server_pattern, $langcode); + } + + return $source; + } + + /** + * Returns TRUE if is valid. + * Development release translations are not supported for remote sources. + * @return bool + */ + public function isValid() { + return empty($this->projectVersion) || strpos('dev', $this->projectVersion) === FALSE; + } + + /** + * Return TRUE if remote translation is available. + * @return bool + */ + public function isAvailable() { + return !$this->projectVersion || strpos('dev', $this->projectVersion) === FALSE; + } + +} \ No newline at end of file diff --git a/core/modules/locale/src/Tests/LocaleUpdateBase.php b/core/modules/locale/src/Tests/LocaleUpdateBase.php index 772ac2a..abdd791 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateBase.php +++ b/core/modules/locale/src/Tests/LocaleUpdateBase.php @@ -131,6 +131,7 @@ protected function makePoFile($path, $filename, $timestamp = NULL, $translations } file_prepare_directory($path, FILE_CREATE_DIRECTORY); + /** @var \Drupal\file\Entity\File $file */ $file = entity_create('file', array( 'uid' => 1, 'filename' => $filename, @@ -258,22 +259,22 @@ protected function setCurrentTranslations() { 'timestamp' => $this->timestampMedium, 'last_checked' => $this->timestampMedium, ); - $data[] = array( + $data['contrib_module_one'] = array( 'project' => 'contrib_module_one', 'filename' => 'contrib_module_one-8.x-1.1.de._po', 'version' => '8.x-1.1', ); - $data[] = array( + $data['contrib_module_two'] = array( 'project' => 'contrib_module_two', 'filename' => 'contrib_module_two-8.x-2.0-beta4.de._po', 'version' => '8.x-2.0-beta4', ); - $data[] = array( + $data['contrib_module_three'] = array( 'project' => 'contrib_module_three', 'filename' => 'contrib_module_three-8.x-1.0.de._po', 'version' => '8.x-1.0', ); - $data[] = array( + $data['custom_module_one'] = array( 'project' => 'custom_module_one', 'filename' => 'custom_module_one.de.po', 'version' => '', @@ -282,6 +283,23 @@ protected function setCurrentTranslations() { $file = array_merge($default, $file); db_insert('locale_file')->fields($file)->execute(); } + + // Build a mock translatable projects. This sets a the timestamp for the + // current translation. + foreach(array_keys($data) as $name) { + $project_data = array( + 'id' => $name, + 'state_by_language' => array($langcode => (object) array( + 'timestamp' => $this->timestampMedium, + 'lastChecked' => $this->timestampMedium, + 'latestSource' => 'current', + 'latestSourceTimestamp' => $this->timestampMedium, + )), + ); + $project = new \Drupal\locale\LocaleTranslatableProject($project_data); + $project->clearTranslationStates(); + \Drupal::service('locale.project')->set($project); + } } /** diff --git a/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php b/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php index ddd9eae..4ce2561 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php +++ b/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php @@ -51,9 +51,11 @@ public function testInterface() { // Drupal core is probably in 8.x, but tests may also be executed with // stable releases. As this is an uncontrolled factor in the test, we will // mark Drupal core as translated and continue with the prepared modules. - $status = locale_translation_get_status(); - $status['drupal']['de']->type = 'current'; - \Drupal::state()->set('locale.translation_status', $status); + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('drupal'); + $project->setLangcode('de'); + $project->setTimestamp(REQUEST_TIME); + \Drupal::service('locale.project')->set($project); // One language added, all translations up to date. $this->drupalGet('admin/reports/status'); @@ -63,9 +65,15 @@ public function testInterface() { $this->assertText(t('All translations up to date.'), 'Translations up to date'); // Set locale_test_translate module to have a local translation available. - $status = locale_translation_get_status(); - $status['locale_test_translate']['de']->type = 'local'; - \Drupal::state()->set('locale.translation_status', $status); + $project = \Drupal::service('locale.project')->get('locale_test_translate'); + $project->setLangcode('de'); + $project->setLocalSource((object) array( + 'timestamp' => REQUEST_TIME + 100, + 'filename' => 'TEST', + 'directory' => 'TEST', + 'uri' => 'TEST', + )); + \Drupal::service('locale.project')->set($project); // Check if updates are available for German. $this->drupalGet('admin/reports/status'); @@ -76,10 +84,11 @@ public function testInterface() { // Set locale_test_translate module to have a dev release and no // translation found. - $status = locale_translation_get_status(); - $status['locale_test_translate']['de']->version = '1.3-dev'; - $status['locale_test_translate']['de']->type = ''; - \Drupal::state()->set('locale.translation_status', $status); + $project = \Drupal::service('locale.project')->get('locale_test_translate'); + $project->setLangcode('de'); + $project->setVersion('1.3-dev'); + $project->clearTranslationStates(); + \Drupal::service('locale.project')->set($project); // Check if no updates were found. $this->drupalGet('admin/reports/status'); diff --git a/core/modules/locale/src/Tests/LocaleUpdateNotDevelopmentReleaseTest.php b/core/modules/locale/src/Tests/LocaleUpdateNotDevelopmentReleaseTest.php index 0763809..31496a1 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateNotDevelopmentReleaseTest.php +++ b/core/modules/locale/src/Tests/LocaleUpdateNotDevelopmentReleaseTest.php @@ -77,7 +77,7 @@ public function testLocaleUpdateNotDevelopmentRelease() { $available['last_fetch'] = REQUEST_TIME; \Drupal::keyValueExpirable('update_available_releases')->setWithExpire('drupal', $available, 10); $projects = locale_translation_build_projects(); - $this->verbose($projects['drupal']->info['version']); - $this->assertEqual($projects['drupal']->info['version'], '8.0.0-alpha110', 'The first release with the same major release number which is not a development release.'); + $this->verbose($projects['drupal']->getVersion()); + $this->assertEqual($projects['drupal']->getVersion(), '8.0.0-alpha110', 'The first release with the same major release number which is not a development release.'); } } diff --git a/core/modules/locale/src/Tests/LocaleUpdateTest.php b/core/modules/locale/src/Tests/LocaleUpdateTest.php index 88927f9..b53a902 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateTest.php +++ b/core/modules/locale/src/Tests/LocaleUpdateTest.php @@ -86,13 +86,24 @@ public function testUpdateCheckStatus() { // Get status of translation sources at local file system. $this->drupalGet('admin/reports/translations/check'); - $result = locale_translation_get_status(); - $this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_one found'); - $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestampOld, 'Translation timestamp found'); - $this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found'); - $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation timestamp found'); - $this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found'); - $this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found'); + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of contrib_module_one found'); + $this->assertEqual($project->getLatestSourceTimestamp(), $this->timestampOld, 'Translation timestamp found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of contrib_module_two found'); + $this->assertEqual($project->getLatestSourceTimestamp(), $this->timestampNew, 'Translation timestamp found'); + + $project = \Drupal::service('locale.project')->get('locale_test'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of locale_test found'); + + $project = \Drupal::service('locale.project')->get('custom_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of custom_module_one found'); // Set the test conditions. $edit = array( @@ -102,15 +113,33 @@ public function testUpdateCheckStatus() { // Get status of translation sources at both local and remote locations. $this->drupalGet('admin/reports/translations/check'); - $result = locale_translation_get_status(); - $this->assertEqual($result['contrib_module_one']['de']->type, LOCALE_TRANSLATION_REMOTE, 'Translation of contrib_module_one found'); - $this->assertEqual($result['contrib_module_one']['de']->timestamp, $this->timestampNew, 'Translation timestamp found'); - $this->assertEqual($result['contrib_module_two']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_two found'); - $this->assertEqual($result['contrib_module_two']['de']->timestamp, $this->timestampNew, 'Translation timestamp found'); - $this->assertEqual($result['contrib_module_three']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of contrib_module_three found'); - $this->assertEqual($result['contrib_module_three']['de']->timestamp, $this->timestampOld, 'Translation timestamp found'); - $this->assertEqual($result['locale_test']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of locale_test found'); - $this->assertEqual($result['custom_module_one']['de']->type, LOCALE_TRANSLATION_LOCAL, 'Translation of custom_module_one found'); + + // Reset the project cache to make sure we get fresh data. + \Drupal::service('locale.project')->resetCache(); + + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->remoteIsLatest(), 'Translation of contrib_module_one found'); + $this->assertEqual($project->getLatestSourceTimestamp(), $this->timestampNew, 'Translation timestamp found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of contrib_module_two found'); + $this->assertEqual($project->getLatestSourceTimestamp(), $this->timestampNew, 'Translation timestamp found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_three'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of contrib_module_three found'); + $this->assertEqual($project->getLatestSourceTimestamp(), $this->timestampOld, 'Translation timestamp found'); + + $project = \Drupal::service('locale.project')->get('locale_test'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of locale_test found'); + + $project = \Drupal::service('locale.project')->get('custom_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of custom_module_one found'); } /** @@ -148,10 +177,18 @@ public function testUpdateImportSourceRemote() { $this->drupalPostForm('admin/reports/translations', array(), t('Update translations')); // Check if the translation has been updated, using the status cache. - $status = locale_translation_get_status(); - $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found'); - $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found'); - $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found'); + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_one found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_two found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_three'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_three found'); // Check the new translation status. // The static cache needs to be flushed first to get the most recent data @@ -202,10 +239,18 @@ public function testUpdateImportSourceLocal() { $this->drupalPostForm('admin/reports/translations', array(), t('Update translations')); // Check if the translation has been updated, using the status cache. - $status = locale_translation_get_status(); - $this->assertEqual($status['contrib_module_one']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_one found'); - $this->assertEqual($status['contrib_module_two']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_two found'); - $this->assertEqual($status['contrib_module_three']['de']->type, LOCALE_TRANSLATION_CURRENT, 'Translation of contrib_module_three found'); + /** @var \Drupal\locale\LocaleTranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_one found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_two found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_three'); + $project->setLangcode('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_three found'); // Check the new translation status. // The static cache needs to be flushed first to get the most recent data