diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 3c8d121..0bf1f9e 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -1663,7 +1663,9 @@ function _install_prepare_import($langcodes, $server_pattern) { 'server_pattern' => $server_pattern, 'status' => 1, ); - \Drupal::service('locale.project')->set($data['name'], $data); + $project = new \Drupal\locale\TranslatableProject($data); + $project->setLangcode($langcode); + \Drupal::service('locale.project')->set($project); module_load_include('compare.inc', 'locale'); locale_translation_check_projects_local(array('drupal'), array($langcode)); } diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc index 67ce5d8..bc27ce6 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\TranslatableProject; /** * 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,57 @@ * @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); + /** @var \Drupal\locale\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->getStateByLanguage($langcode); + // Check the status of local translation files. + if ($project->getLocalSource()->isAvailable()) { + locale_translation_source_check_file($project->getLocalSource()); + $checked = TRUE; } - $checked = TRUE; - } - // 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 remote translation files. + if ($options['use_remote'] && $project->getRemoteSource()->isAvailable() && $remote_source = $project->getRemoteSource()) { + if ($result = locale_translation_http_check($remote_source->getUri())) { + // Update the file object with the result data. In case of a redirect we + // store the resulting uri. + if (isset($result['last_modified'])) { + if (isset($result['location'])) { + $remote_source->setUri($result['location']); + } + $remote_source->setTimestamp($result['last_modified']); + } + $checked = TRUE; + } + else { + $failure = TRUE; } - // @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 +129,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 +138,21 @@ 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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->getStateByLanguage($langcode); + + $remote_source = $project->getRemoteSource(); + $local_source = $project->getLocalSource(); + if ($remote_source && $remote_source->isAvailable() && $project->remoteIsLatest()) { + if (locale_translation_download_source($remote_source, $local_source, 'translations://')) { + $context['message'] = t('Downloaded translation for %project.', array('%project' => $project->getName())); + \Drupal::service('locale.project')->set($project); } else { - $context['results']['failed_files'][] = $source->files[LOCALE_TRANSLATION_REMOTE]; + $context['results']['failed_files'][] = $remote_source; } } } @@ -155,8 +164,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 +176,34 @@ 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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get($project_id); + if ($project) { + $project->getStateByLanguage($langcode); - // 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)); + /** @var \Drupal\locale\ProjectSourceInterface $local_source */ + if ($project->localIsLatest() && $local_source = $project->getLocalSource()) { + 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. + $file = (object) $local_source->toArray(); + locale_translate_batch_import($file, $options, $context); - // 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]); - } + // The import is finished. + if (isset($context['finished']) && $context['finished'] == 1) { + // The import is successful. + if (isset($context['results']['files'][$local_source->getUri()])) { + $context['message'] = t('Imported translation for %project.', array('%project' => $project->getName())); + + // Update the translation timestamps and translation history. + $local_source->setTimestamp($file->timestamp); + $project->setTimestamp($file->timestamp); + \Drupal::service('locale.project')->set($project); + locale_translation_update_file_history($file); } } } @@ -211,7 +223,7 @@ function locale_translation_batch_fetch_finished($success, $results) { if ($success) { \Drupal::state()->set('locale.translation_last_checked', REQUEST_TIME); } - return locale_translate_batch_finished($success, $results); + locale_translate_batch_finished($success, $results); } /** @@ -262,29 +274,24 @@ function locale_translation_http_check($uri) { /** * Downloads a translation file from a remote server. * - * @param object $source_file - * Source file object with at least: - * - "uri": uri to download the file from. - * - "project": Project name. - * - "langcode": Translation language. - * - "version": Project version. - * - "filename": File name. + * @param \Drupal\locale\ProjectSourceInterface $source + * Remote translation source object. + * @param \Drupal\locale\ProjectSourceInterface $destination + * Local translation source object. * @param string $directory * Directory where the downloaded file will be saved. Defaults to the * temporary file path. * - * @return object - * File object if download was successful. FALSE on failure. + * @return bool + * TRUE 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)) { - $file = clone($source_file); - $file->type = LOCALE_TRANSLATION_LOCAL; - $file->uri = $uri; - $file->directory = $directory; - $file->timestamp = filemtime($uri); - return $file; +function locale_translation_download_source(\Drupal\locale\ProjectSourceInterface $source, \Drupal\locale\ProjectSourceInterface $destination, $directory = 'temporary://') { + if ($uri = system_retrieve_file($source->getUri(), $directory, FALSE, FILE_EXISTS_REPLACE)) { + $destination->setUri($uri); + $destination->setDirectory = $directory; + $destination->setTimestamp(filemtime($uri)); + return TRUE; } - \Drupal::logger('locale')->error('Unable to download translation file @uri.', array('@uri' => $source_file->uri)); + \Drupal::logger('locale')->error('Unable to download translation file @uri.', array('@uri' => $source->getUri())); return FALSE; } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 83cd425..af0b48e 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..f3580a6 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\TranslatableProject */ 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\TranslatableProject($project_data); + $project->initLangcodeMultiple(array_keys(locale_translatable_language_list())); + \Drupal::service('locale.project')->set($project); + $projects[$name] = $project; + } + else { + /** @var \Drupal\locale\TranslatableProject $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,15 @@ 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\TranslatableProject $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->getStateByLanguage($langcode); + $local = $project->getLocalSource(); + if ($local) { + locale_translation_source_check_file($local); + \Drupal::service('locale.project')->set($project); + } } } } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index b9804d6..28ea319 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -244,15 +244,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\TranslatableProjectInterface $project */ + foreach ($projects as $project) { + /** @var \Drupal\Core\Language\Language $language */ + foreach ($languages as $langcode => $language) { + $project->getStateByLanguage($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 40934de..b98d237 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -22,6 +22,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Component\Utility\Crypt; +use Drupal\locale\TranslatableProject; /** * Regular expression pattern used to localize JavaScript strings. @@ -122,21 +123,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) { @@ -393,12 +379,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. @@ -444,12 +430,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); } } @@ -760,7 +741,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; } } @@ -785,11 +766,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; } @@ -812,95 +792,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(); } } @@ -911,16 +828,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); } /** @@ -929,22 +844,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 3ff2a15..8e366f3 100644 --- a/core/modules/locale/locale.pages.inc +++ b/core/modules/locale/locale.pages.inc @@ -10,8 +10,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\TranslatableProject; /** * Page callback: Checks for translation updates and displays the status. @@ -116,7 +117,7 @@ function template_preprocess_locale_translation_update_info(&$variables) { } // Build output for updates not found. - if (isset($variables['not_found'])) { + if (isset($variables['not_found']) && $variables['not_found']) { $releases = array(); $variables['missing_updates_status'] = format_plural(count($variables['not_found']), 'Missing translations for one project', 'Missing translations for @count projects'); if ($variables['not_found']) { diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc index 2208b1d..7652148 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 objects 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,238 +58,37 @@ 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 * to the "translations://" stream wrapper path. The directory may contain any * valid stream wrapper. * - * The "local" files property of the source object contains the definition of a - * po file we are looking for. The file name defaults to - * %project-%version.%language.po. Per project this value can be overridden - * using the server_pattern directive in the module's .info.yml file or by using - * hook_locale_translation_projects_alter(). + * The source object contains the definition of a po file we are looking for. + * The file name defaults to %project-%version.%language.po. Per project this + * value can be overridden using the server_pattern directive in the module's + * .info.yml file or by using hook_locale_translation_projects_alter(). * - * @param object $source + * @param \Drupal\locale\ProjectSourceInterface $source * Translation source object. * - * @return object - * Source file object of the po file, updated with: - * - "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() + * @return bool + * TRUE if the file was found, FALSE otherwise. */ -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(\Drupal\locale\ProjectSourceInterface $source) { + $directory = $source->getDirectory(); + $filename = '/' . preg_quote($source->getFilename()) . '$/'; - 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->setUri($file->uri); + $source->setTimestamp(filemtime($file->uri)); + return TRUE; } 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 - * Project object containing data to be inserted in the template. - * @param string $template - * String containing placeholders. Available placeholders: - * - "%project": Project name. - * - "%version": Project version. - * - "%core": Project core version. - * - "%language": Language code. - * - * @return string - * String with replaced placeholders. - */ -function locale_translation_build_server_pattern($project, $template) { - $variables = array( - '%project' => $project->name, - '%version' => $project->version, - '%core' => $project->core, - '%language' => isset($project->langcode) ? $project->langcode : '%language', - ); - return strtr($template, $variables); -} - -/** * Populate a queue with project to check for translation updates. */ function locale_cron_fill_queue() { @@ -329,7 +99,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)) @@ -365,62 +135,6 @@ function locale_cron_fill_queue() { } /** - * Determine if a file is a remote file. - * - * @param string $uri - * The URI or URI pattern of the file. - * - * @return bool - * TRUE if the $uri is a remote file. - */ -function _locale_translation_file_is_remote($uri) { - $scheme = file_uri_scheme($uri); - if ($scheme) { - return !drupal_realpath($scheme . '://'); - } - return FALSE; -} - -/** - * 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/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 2b20c47..42c80a3 100644 --- a/core/modules/locale/src/Form/TranslationStatusForm.php +++ b/core/modules/locale/src/Form/TranslationStatusForm.php @@ -63,6 +63,10 @@ public function getFormID() { return 'locale_translation_status_form'; } + function set_timestamp($source, $timestamp) { + $source->setTimestamp($timestamp); + } + /** * Form builder for displaying the current translation status. * @@ -70,15 +74,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) { @@ -138,7 +143,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 +188,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\TranslatableProjectInterface $project */ + foreach ($projects as $project_id => $project) { + /** @var \Drupal\Core\Language\Language $language */ + foreach ($languages as $langcode => $language) { + $project->getStateByLanguage($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_info->name == 'drupal' ? $this->t('Drupal core') : $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 +238,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\TranslatableProjectInterface $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\TranslatableProjectInterface $project) { + $remote_source = $project->getRemoteSource(); + $local_source = $project->getLocalSource(); + $remote_path = $remote_source->getUri(); + $local_path = $local_source->getUri(); - 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..fb4f420 --- /dev/null +++ b/core/modules/locale/src/LocalProjectSource.php @@ -0,0 +1,43 @@ +project = $project; + + $filename = \Drupal::config('locale.settings')->get('translation.default_filename'); + $server_pattern = $project->getServerPattern(); + + if ($this->fileIsRemote($server_pattern)) { + $this->filename = $this->buildServerPattern($filename, $this->project, $langcode); + $this->directory = 'translations://'; + $this->uri = $this->directory . $this->filename; + } + else { + $this->filename = $this->buildServerPattern(basename($server_pattern), $this->project, $langcode); + $this->directory = $this->buildServerPattern(drupal_dirname($server_pattern), $this->project, $langcode); + $this->uri = $this->directory . '/' . $this->filename; + } + } + + /** + * @inheritdoc + */ + public function setTimestamp($timestamp) { + parent::setTimestamp($timestamp); + $this->project->setLocalTimestamp($timestamp); + } + +} \ No newline at end of file diff --git a/core/modules/locale/src/LocaleProjectStorage.php b/core/modules/locale/src/LocaleProjectStorage.php index 65c83fc..ebe5588 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] = unserialize($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\TranslatableProjectInterface $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] = serialize($project); } $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,12 @@ public function countProjects() { */ public function getAll() { if (!static::$all) { - $this->cache = $this->keyValueStore->getAll(); + foreach ($this->keyValueStore->getAll() as $key => $value) { + $this->cache[$key] = unserialize($value); + } static::$all = TRUE; } return $this->cache; } + } diff --git a/core/modules/locale/src/LocaleProjectStorageInterface.php b/core/modules/locale/src/LocaleProjectStorageInterface.php index 876f05a..792e495 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\TranslatableProjectInterface|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\TranslatableProjectInterface $project + * The project to store. */ - public function set($key, $value); + public function set(\Drupal\locale\TranslatableProjectInterface $project); /** * Creates or updates multiple project records. diff --git a/core/modules/locale/src/ProjectSourceBase.php b/core/modules/locale/src/ProjectSourceBase.php new file mode 100644 index 0000000..971d00e --- /dev/null +++ b/core/modules/locale/src/ProjectSourceBase.php @@ -0,0 +1,230 @@ +filename; + } + + /** + * @inheritdoc + */ + public function getDirectory() { + return $this->directory; + } + + /** + * @inheritdoc + */ + public function getUri() { + return $this->uri; + } + + /** + * @inheritdoc + */ + public function getTimestamp() { + return $this->timestamp; + } + + /** + * @inheritdoc + */ + public function getLastChecked() { + return $this->lastChecked; + } + + /** + * @inheritdoc + */ + public function setFilename($name) { + return $this->filename = $name; + } + + /** + * @inheritdoc + */ + public function setDirectory($directory) { + return $this->directory = $directory; + } + + /** + * @inheritdoc + */ + public function setUri($uri) { + $this->uri = $uri; + } + + /** + * @inheritdoc + */ + public function setTimestamp($timestamp) { + $this->timestamp = $timestamp; + $this->lastChecked = REQUEST_TIME; + } + + /** + * @inheritdoc + */ + public function setProject($project) { + $this->project = $project; + } + + /** + * @inheritdoc + */ + public function toArray() { + return array( + 'uri' => $this->uri, + 'filename' => $this->filename, + 'timestamp' => $this->timestamp, + 'lastChecked' => $this->lastChecked, + 'langcode' => $this->project->getLangcode(), + 'project' => $this->project->id(), + 'version' => $this->project->getVersion(), + ); + } + + /** + * @inheritdoc + */ + public function isAvailable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function serialize() { + $source = clone $this; + // Don't serialize the parent project object. + unset($source->project); + return serialize(get_object_vars($source)); + + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) { + $data = unserialize($serialized); + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + // TranslatableProject::unserialize takes care of restoring $this->project. + } + + /** + * 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 \Drupal\locale\TranslatableProjectInterface $project + * Translatable project. + * @param string $langcode + * Language code of the translation file. + * + * @return string + * File uri with replaced placeholders. + */ + protected function buildServerPattern($template, \Drupal\locale\TranslatableProjectInterface $project, $langcode) { + $variables = array( + '%project' => $project->id(), + '%version' => $project->getVersion(), + '%core' => $project->getCoreVersion(), + '%language' => $langcode, + ); + + return strtr($template, $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. + */ + protected 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/ProjectSourceInterface.php b/core/modules/locale/src/ProjectSourceInterface.php new file mode 100644 index 0000000..26b0cb9 --- /dev/null +++ b/core/modules/locale/src/ProjectSourceInterface.php @@ -0,0 +1,92 @@ +project = $project; + + $server_pattern = $project->getServerPattern(); + + if ($this->fileIsRemote($server_pattern)) { + $this->filename = self::buildServerPattern(basename($server_pattern), $this->project, $langcode); + $this->uri = self::buildServerPattern($server_pattern, $this->project, $langcode); + } + } + + /** + * @inheritdoc + */ + public function setTimestamp($timestamp) { + parent::setTimestamp($timestamp); + $this->project->setRemoteTimestamp($timestamp); + } + + /** + * @inheritdoc + */ + public function isAvailable() { + // Development release translations are not supported for remote sources. + if (strpos('dev', $this->project->getVersion()) !== FALSE) { + return FALSE; + } + + return TRUE; + } + +} \ 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 5e9b81e..8551c95 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\TranslatableProject($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 c790cd1..bc45be0 100644 --- a/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php +++ b/core/modules/locale/src/Tests/LocaleUpdateInterfaceTest.php @@ -7,6 +7,8 @@ namespace Drupal\locale\Tests; +use Drupal\locale\LocalProjectSource; + /** * Tests for the user interface of project interface translations. * @@ -49,9 +51,14 @@ public function testInterface() { $this->addLanguage('de'); // Override Drupal core translation status as 'up-to-date'. - $status = locale_translation_get_status(); - $status['drupal']['de']->type = 'current'; - \Drupal::state()->set('locale.translation_status', $status); + /** @var \Drupal\locale\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('drupal'); + $project->getStateByLanguage('de')->setTimestamp(REQUEST_TIME); + \Drupal::service('locale.project')->set($project); + + // Reset the project cache to make sure we get fresh data. + \Drupal::service('locale.project')->resetCache(); +//debug(\Drupal::service('locale.project')->getAll()); // One language added, all translations up to date. $this->drupalGet('admin/reports/status'); @@ -61,9 +68,9 @@ 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->getStateByLanguage('de')->getLocalSource()->setTimestamp(REQUEST_TIME + 100); + \Drupal::service('locale.project')->set($project); // Check if updates are available for German. $this->drupalGet('admin/reports/status'); @@ -74,10 +81,10 @@ 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')->getStateByLanguage('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'); @@ -89,11 +96,11 @@ public function testInterface() { $this->assertText(t('No translation files are provided for development releases.'), 'Release info'); // Override Drupal core translation status as 'no translations found'. - $status = locale_translation_get_status(); - $status['drupal']['de']->type = ''; - $status['drupal']['de']->timestamp = 0; - $status['drupal']['de']->version = '8.1.1'; - \Drupal::state()->set('locale.translation_status', $status); + $project = \Drupal::service('locale.project')->get('drupal')->getStateByLanguage('de'); + $project->setVersion('8.1.1'); + $project->clearTranslationStates(); + \Drupal::service('locale.project')->set($project); +//debug($project); // Check if Drupal core is not translated. $this->drupalGet('admin/reports/translations'); @@ -101,11 +108,10 @@ public function testInterface() { $this->assertText(t('@module (@version).', array('@module' => t('Drupal core'), '@version' => '8.1.1')), 'Release details'); // Override Drupal core translation status as 'translations available'. - $status = locale_translation_get_status(); - $status['drupal']['de']->type = 'local'; - $status['drupal']['de']->files['local']->timestamp = REQUEST_TIME; - $status['drupal']['de']->files['local']->info['version'] = '8.1.1'; - \Drupal::state()->set('locale.translation_status', $status); + $project = \Drupal::service('locale.project')->get('drupal')->getStateByLanguage('de'); + $project->getLocalSource()->setTimestamp(REQUEST_TIME); + \Drupal::service('locale.project')->set($project); +//debug($project); // Check if translations are available for Drupal core. $this->drupalGet('admin/reports/translations'); 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..f1f53df 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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->getStateByLanguage('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->getStateByLanguage('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->getStateByLanguage('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of locale_test found'); + + $project = \Drupal::service('locale.project')->get('custom_module_one'); + $project->getStateByLanguage('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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->getStateByLanguage('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->getStateByLanguage('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->getStateByLanguage('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->getStateByLanguage('de'); + $this->assertTrue($project->localIsLatest(), 'Translation of locale_test found'); + + $project = \Drupal::service('locale.project')->get('custom_module_one'); + $project->getStateByLanguage('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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->getStateByLanguage('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_one found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->getStateByLanguage('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_two found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_three'); + $project->getStateByLanguage('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\TranslatableProjectInterface $project */ + $project = \Drupal::service('locale.project')->get('contrib_module_one'); + $project->getStateByLanguage('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_one found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_two'); + $project->getStateByLanguage('de'); + $this->assertTrue($project->isUpToDate(), 'Translation of contrib_module_two found'); + + $project = \Drupal::service('locale.project')->get('contrib_module_three'); + $project->getStateByLanguage('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 diff --git a/core/modules/locale/src/TranslatableProject.php b/core/modules/locale/src/TranslatableProject.php new file mode 100644 index 0000000..2bd21e4 --- /dev/null +++ b/core/modules/locale/src/TranslatableProject.php @@ -0,0 +1,470 @@ +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 when + // version or server pattern have changed. + if ((isset($data['version']) && $old_version != $this->projectVersion) + || (isset($data['server_pattern']) && $old_server_pattern != $this->serverPattern)) { + foreach (array_keys($this->stateByLanguage) as $langcode) { + $this->stateByLanguage[$langcode]->localSource = new LocalProjectSource($this, $langcode); + $this->stateByLanguage[$langcode]->remoteSource = new RemoteProjectSource($this, $langcode); + } + $this->clearTranslationStates(); + } + } + + /** + * {@inheritdoc} + */ + public function serialize() { + $project = clone $this; + // Don't serialize the parent project object. + unset($project->langcode); + return serialize(get_object_vars($project)); + + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) { + $data = unserialize($serialized); + foreach ($data as $key => $value) { + $this->{$key} = $value; + } + foreach (array_keys($this->stateByLanguage) as $langcode) { + $this->stateByLanguage[$langcode]->localSource->setProject($this); + $this->stateByLanguage[$langcode]->remoteSource->setProject($this); + } + } + + /** + * @inheritdoc + */ + public function getStateByLanguage($langcode) { + $this->langcode = $langcode; + if (!isset($this->stateByLanguage[$langcode])) { + $this->initLangcode($langcode); + } + + return $this; + } + + /** + * @inheritdoc + */ + public function initLangcode($langcode) { + $this->initLangcodeMultiple(array($langcode)); + } + + /** + * @inheritdoc + */ + public function initLangcodeMultiple(array $langcodes) { + foreach ($langcodes as $langcode) { + if (!isset($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 clearTranslationStates() { + foreach (array_keys($this->stateByLanguage) as $langcode) { + if ($this->stateByLanguage[$langcode]->latestSource == $this::LOCAL_SOURCE + || $this->stateByLanguage[$langcode]->latestSource == $this::REMOTE_SOURCE) { + $this->stateByLanguage[$langcode]->latestSource = ''; + $this->stateByLanguage[$langcode]->latestSourceTimestamp = 0; + } + } + } + + /** + * @inheritdoc + */ + public function getLocalSource() { + if (empty($this->getState()->localSource)) { + new LocalProjectSource($this, $this->langcode); + } + return $this->getState()->localSource; + } + + /** + * @inheritdoc + */ + public function getRemoteSource() { + if (empty($this->getState()->remoteSource)) { + new RemoteProjectSource($this, $this->langcode); + } + return $this->getState()->remoteSource; + } + + /** + * @inheritdoc + */ + public function getLatestSourceTimestamp() { + return $this->getState()->latestSourceTimestamp; + } + + /** + * @inheritdoc + */ + public function getVersion() { + return $this->projectVersion; + } + + /** + * @inheritdoc + */ + public function setVersion($version) { + $this->projectVersion = $version; + } + + /** + * @inheritdoc + */ + public function getCoreVersion() { + return $this->projectCore; + } + + /** + * @inheritdoc + */ + public function getLangcode() { + return $this->langcode; + } + + /** + * @inheritdoc + */ + public function remoteIsLatest() { + return $this->getState()->latestSource == $this::REMOTE_SOURCE; + } + + /** + * @inheritdoc + */ + public function remoteIsValid() { + // Development release translations are not supported for remote sources. + if ($this->projectVersion && strpos('dev', $this->projectVersion) !== FALSE) { + return FALSE; + } + + return TRUE; + } + + /** + * @inheritdoc + */ + public function remoteIsAvailable() { + return $this->remoteIsValid() && $this->getState()->latestSource == $this::REMOTE_SOURCE; + } + + /** + * @inheritdoc + */ + public function localIsLatest() { + return $this->getState()->latestSource == $this::LOCAL_SOURCE; + } + + /** + * @inheritdoc + */ + public function setLocalTimestamp($timestamp) { + + if ($timestamp) { + $this->checkLangcode(); + + // If the local translation is the most recent, we update the + // latestSource status. Otherwise mark the current translation as + // checked. + if ($timestamp > $this->getState()->latestSourceTimestamp + || ($timestamp >= $this->getState()->latestSourceTimestamp && $this->getState()->latestSource == $this::REMOTE_SOURCE)) { + $this->stateByLanguage[$this->langcode]->latestSource = $this::LOCAL_SOURCE; + $this->stateByLanguage[$this->langcode]->latestSourceTimestamp = $timestamp; + } + else { + $this->stateByLanguage[$this->langcode]->lastChecked = REQUEST_TIME; + } + } + } + + + /** + * @inheritdoc + */ + public function setRemoteTimestamp($timestamp) { + + if ($timestamp) { + $this->checkLangcode(); + + // If the remote translation is the most recent, we update the + // latestSource status. Otherwise mark the current translation as + // checked. + if ($timestamp > $this->getState()->latestSourceTimestamp) { + $this->stateByLanguage[$this->langcode]->latestSource = $this::REMOTE_SOURCE; + $this->stateByLanguage[$this->langcode]->latestSourceTimestamp = $timestamp; + } + else { + $this->stateByLanguage[$this->langcode]->lastChecked = REQUEST_TIME; + } + } +} + + /** + * @inheritdoc + */ + public function setTimestamp($timestamp) { + if ($timestamp) { + $this->checkLangcode(); + + // The current translation has been updated. Mark the current translation + // as checked and store the timestamp. + $this->stateByLanguage[$this->langcode]->timestamp = $timestamp; + $this->stateByLanguage[$this->langcode]->lastChecked = REQUEST_TIME; + $this->stateByLanguage[$this->langcode]->latestSource = $this::CURRENT_SOURCE; + $this->stateByLanguage[$this->langcode]->latestSourceTimestamp = $timestamp; + } + } + + /** + * @inheritdoc + */ + public function isTranslated() { + return $this->getState()->timestamp > 0; + } + + /** + * @inheritdoc + */ + public function hasUpdates() { + return $this->getState()->latestSource == $this::LOCAL_SOURCE || $this->getState()->latestSource == $this::REMOTE_SOURCE; + } + + /** + * @inheritdoc + */ + public function hasNoTranslation() { + return $this->getState()->latestSource == ''; + } + + /** + * @inheritdoc + */ + public function isUpToDate() { + return $this->getState()->latestSource == $this::CURRENT_SOURCE; + } + + /** + * 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->getState()) { + $state = new \stdClass(); + $state->timestamp = 0; + $state->lastChecked = 0; + $state->latestSource = ''; + $state->latestSourceTimestamp = 0; + $state->localSource = new LocalProjectSource($this, $langcode); + $state->remoteSource = new RemoteProjectSource($this, $langcode); + + $this->stateByLanguage[$langcode] = $state; +// } + } + + /** + * 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', + ); + } + + /** + * @todo + * + * @throws \Exception when the required language code was not set. + */ + protected function checkLangcode() { + if (empty($this->langcode)) { + throw new \Exception('Required language code was not set.'); + } + } + + /** + * Returns the current translations state of the currently set language. + * + * @return object + */ + protected function getState() { + $this->checkLangcode(); + return $this->stateByLanguage[$this->langcode]; + } + + /** + * Set the current translations state. + * + * @param \Drupal\locale\TranslatableProjectInterface $state + * The project translation state. + * + * @param string $langcode + * The language code. + */ + protected function setState($state, $langcode) { + $this->stateByLanguage[$langcode] = $state; + } +} diff --git a/core/modules/locale/src/TranslatableProjectInterface.php b/core/modules/locale/src/TranslatableProjectInterface.php new file mode 100644 index 0000000..9c34163 --- /dev/null +++ b/core/modules/locale/src/TranslatableProjectInterface.php @@ -0,0 +1,260 @@ +