Chris@0: countProjects(); Chris@0: // https://www.drupal.org/node/1777106 is a follow-up issue to make the Chris@0: // check for possible out-of-date project information more robust. Chris@0: if ($row_count == 0) { Chris@0: module_load_include('compare.inc', 'locale'); Chris@0: // At least the core project should be in the database, so we build the Chris@0: // data if none are found. Chris@0: locale_translation_build_projects(); Chris@0: } Chris@0: $projects = \Drupal::service('locale.project')->getAll(); Chris@0: array_walk($projects, function (&$project) { Chris@0: $project = (object) $project; Chris@0: }); Chris@0: } Chris@0: Chris@0: // Return the requested project names or all projects. Chris@0: if ($project_names) { Chris@0: return array_intersect_key($projects, array_combine($project_names, $project_names)); Chris@0: } Chris@0: return $projects; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clears the projects cache. Chris@0: */ Chris@0: function locale_translation_clear_cache_projects() { Chris@0: drupal_static_reset('locale_translation_get_projects'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads cached translation sources containing current translation status. Chris@0: * Chris@0: * @param array $projects Chris@0: * Array of project names. Defaults to all translatable projects. Chris@0: * @param array $langcodes Chris@0: * Array of language codes. Defaults to all translatable languages. Chris@0: * Chris@0: * @return array Chris@0: * Array of source objects. Keyed with :. Chris@0: * Chris@0: * @see locale_translation_source_build() Chris@0: */ Chris@0: function locale_translation_load_sources(array $projects = NULL, array $langcodes = NULL) { Chris@0: $sources = []; Chris@0: $projects = $projects ? $projects : array_keys(locale_translation_get_projects()); Chris@0: $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); Chris@0: Chris@0: // Load source data from locale_translation_status cache. Chris@0: $status = locale_translation_get_status(); Chris@0: Chris@0: // Use only the selected projects and languages for update. Chris@0: foreach ($projects as $project) { Chris@0: foreach ($langcodes as $langcode) { Chris@0: $sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL; Chris@0: } Chris@0: } Chris@0: return $sources; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Build translation sources. Chris@0: * Chris@0: * @param array $projects Chris@0: * Array of project names. Defaults to all translatable projects. Chris@0: * @param array $langcodes Chris@0: * Array of language codes. Defaults to all translatable languages. Chris@0: * Chris@0: * @return array Chris@0: * Array of source objects. Keyed by project name and language code. Chris@0: * Chris@0: * @see locale_translation_source_build() Chris@0: */ Chris@0: function locale_translation_build_sources(array $projects = [], array $langcodes = []) { Chris@0: $sources = []; Chris@0: $projects = locale_translation_get_projects($projects); Chris@0: $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); Chris@0: Chris@0: foreach ($projects as $project) { Chris@0: foreach ($langcodes as $langcode) { Chris@0: $source = locale_translation_source_build($project, $langcode); Chris@0: $sources[$source->name][$source->langcode] = $source; Chris@0: } Chris@0: } Chris@0: return $sources; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks whether a po file exists in the local filesystem. Chris@0: * Chris@0: * It will search in the directory set in the translation source. Which defaults Chris@0: * to the "translations://" stream wrapper path. The directory may contain any Chris@0: * valid stream wrapper. Chris@0: * Chris@0: * The "local" files property of the source object contains the definition of a Chris@0: * po file we are looking for. The file name defaults to Chris@0: * %project-%version.%language.po. Per project this value can be overridden Chris@0: * using the server_pattern directive in the module's .info.yml file or by using Chris@0: * hook_locale_translation_projects_alter(). Chris@0: * Chris@0: * @param object $source Chris@0: * Translation source object. Chris@0: * Chris@0: * @return object Chris@0: * Source file object of the po file, updated with: Chris@0: * - "uri": File name and path. Chris@0: * - "timestamp": Last updated time of the po file. Chris@0: * FALSE if the file is not found. Chris@0: * Chris@0: * @see locale_translation_source_build() Chris@0: */ Chris@0: function locale_translation_source_check_file($source) { Chris@0: if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) { Chris@0: $source_file = $source->files[LOCALE_TRANSLATION_LOCAL]; Chris@0: $directory = $source_file->directory; Chris@0: $filename = '/' . preg_quote($source_file->filename) . '$/'; Chris@0: Chris@0: if ($files = file_scan_directory($directory, $filename, ['key' => 'name', 'recurse' => FALSE])) { Chris@0: $file = current($files); Chris@0: $source_file->uri = $file->uri; Chris@0: $source_file->timestamp = filemtime($file->uri); Chris@0: return $source_file; Chris@0: } Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds abstract translation source. Chris@0: * Chris@0: * @param object $project Chris@0: * Project object. Chris@0: * @param string $langcode Chris@0: * Language code. Chris@0: * @param string $filename Chris@0: * (optional) File name of translation file. May contain placeholders. Chris@0: * Defaults to the default translation filename from the settings. Chris@0: * Chris@0: * @return object Chris@0: * Source object: Chris@0: * - "project": Project name. Chris@0: * - "name": Project name (inherited from project). Chris@0: * - "language": Language code. Chris@0: * - "core": Core version (inherited from project). Chris@0: * - "version": Project version (inherited from project). Chris@0: * - "project_type": Project type (inherited from project). Chris@0: * - "files": Array of file objects containing properties of local and remote Chris@0: * translation files. Chris@0: * Other processes can add the following properties: Chris@0: * - "type": Most recent translation source found. LOCALE_TRANSLATION_REMOTE Chris@0: * and LOCALE_TRANSLATION_LOCAL indicate available new translations, Chris@0: * LOCALE_TRANSLATION_CURRENT indicate that the current translation is them Chris@0: * most recent. "type" corresponds with a key of the "files" array. Chris@0: * - "timestamp": The creation time of the "type" translation (file). Chris@0: * - "last_checked": The time when the "type" translation was last checked. Chris@0: * The "files" array can hold file objects of type: Chris@0: * LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE and Chris@0: * LOCALE_TRANSLATION_CURRENT. Each contains following properties: Chris@0: * - "type": The object type (LOCALE_TRANSLATION_LOCAL, Chris@0: * LOCALE_TRANSLATION_REMOTE, etc. see above). Chris@0: * - "project": Project name. Chris@0: * - "langcode": Language code. Chris@0: * - "version": Project version. Chris@0: * - "uri": Local or remote file path. Chris@0: * - "directory": Directory of the local po file. Chris@0: * - "filename": File name. Chris@0: * - "timestamp": Timestamp of the file. Chris@0: * - "keep": TRUE to keep the downloaded file. Chris@0: */ Chris@0: function locale_translation_source_build($project, $langcode, $filename = NULL) { Chris@0: // Follow-up issue: https://www.drupal.org/node/1842380. Chris@0: // Convert $source object to a TranslatableProject class and use a typed class Chris@0: // for $source-file. Chris@0: Chris@0: // Create a source object with data of the project object. Chris@0: $source = clone $project; Chris@0: $source->project = $project->name; Chris@0: $source->langcode = $langcode; Chris@0: $source->type = ''; Chris@0: $source->timestamp = 0; Chris@0: $source->last_checked = 0; Chris@0: Chris@0: $filename = $filename ? $filename : \Drupal::config('locale.settings')->get('translation.default_filename'); Chris@0: Chris@0: // If the server_pattern contains a remote file path we will check for a Chris@0: // remote file. The local version of this file will only be checked if a Chris@0: // translations directory has been defined. If the server_pattern is a local Chris@0: // file path we will only check for a file in the local file system. Chris@0: $files = []; Chris@0: if (_locale_translation_file_is_remote($source->server_pattern)) { Chris@0: $files[LOCALE_TRANSLATION_REMOTE] = (object) [ Chris@0: 'project' => $project->name, Chris@0: 'langcode' => $langcode, Chris@0: 'version' => $project->version, Chris@0: 'type' => LOCALE_TRANSLATION_REMOTE, Chris@0: 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), Chris@0: 'uri' => locale_translation_build_server_pattern($source, $source->server_pattern), Chris@0: ]; Chris@0: $files[LOCALE_TRANSLATION_LOCAL] = (object) [ Chris@0: 'project' => $project->name, Chris@0: 'langcode' => $langcode, Chris@0: 'version' => $project->version, Chris@0: 'type' => LOCALE_TRANSLATION_LOCAL, Chris@0: 'filename' => locale_translation_build_server_pattern($source, $filename), Chris@0: 'directory' => 'translations://', Chris@0: ]; Chris@0: $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename; Chris@0: } Chris@0: else { Chris@0: $files[LOCALE_TRANSLATION_LOCAL] = (object) [ Chris@0: 'project' => $project->name, Chris@0: 'langcode' => $langcode, Chris@0: 'version' => $project->version, Chris@0: 'type' => LOCALE_TRANSLATION_LOCAL, Chris@0: 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)), Chris@18: 'directory' => locale_translation_build_server_pattern($source, \Drupal::service('file_system')->dirname($source->server_pattern)), Chris@0: ]; Chris@0: $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . '/' . $files[LOCALE_TRANSLATION_LOCAL]->filename; Chris@0: } Chris@0: $source->files = $files; Chris@0: Chris@0: // If this project+language is already translated, we add its status and Chris@0: // update the current translation timestamp and last_updated time. If the Chris@0: // project+language is not translated before, create a new record. Chris@0: $history = locale_translation_get_file_history(); Chris@0: if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) { Chris@0: $source->files[LOCALE_TRANSLATION_CURRENT] = $history[$project->name][$langcode]; Chris@0: $source->type = LOCALE_TRANSLATION_CURRENT; Chris@0: $source->timestamp = $history[$project->name][$langcode]->timestamp; Chris@0: $source->last_checked = $history[$project->name][$langcode]->last_checked; Chris@0: } Chris@0: else { Chris@0: locale_translation_update_file_history($source); Chris@0: } Chris@0: Chris@0: return $source; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Build path to translation source, out of a server path replacement pattern. Chris@0: * Chris@0: * @param object $project Chris@0: * Project object containing data to be inserted in the template. Chris@0: * @param string $template Chris@0: * String containing placeholders. Available placeholders: Chris@0: * - "%project": Project name. Chris@0: * - "%version": Project version. Chris@0: * - "%core": Project core version. Chris@0: * - "%language": Language code. Chris@0: * Chris@0: * @return string Chris@0: * String with replaced placeholders. Chris@0: */ Chris@0: function locale_translation_build_server_pattern($project, $template) { Chris@0: $variables = [ Chris@0: '%project' => $project->name, Chris@0: '%version' => $project->version, Chris@0: '%core' => $project->core, Chris@0: '%language' => isset($project->langcode) ? $project->langcode : '%language', Chris@0: ]; Chris@0: return strtr($template, $variables); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populate a queue with project to check for translation updates. Chris@0: */ Chris@0: function locale_cron_fill_queue() { Chris@0: $updates = []; Chris@0: $config = \Drupal::config('locale.settings'); Chris@0: Chris@0: // Determine which project+language should be updated. Chris@0: $last = REQUEST_TIME - $config->get('translation.update_interval_days') * 3600 * 24; Chris@0: $projects = \Drupal::service('locale.project')->getAll(); Chris@0: $projects = array_filter($projects, function ($project) { Chris@0: return $project['status'] == 1; Chris@0: }); Chris@18: $connection = \Drupal::database(); Chris@18: $files = $connection->select('locale_file', 'f') Chris@0: ->condition('f.project', array_keys($projects), 'IN') Chris@0: ->condition('f.last_checked', $last, '<') Chris@0: ->fields('f', ['project', 'langcode']) Chris@0: ->execute()->fetchAll(); Chris@0: foreach ($files as $file) { Chris@0: $updates[$file->project][] = $file->langcode; Chris@0: Chris@0: // Update the last_checked timestamp of the project+language that will Chris@0: // be checked for updates. Chris@18: $connection->update('locale_file') Chris@0: ->fields(['last_checked' => REQUEST_TIME]) Chris@0: ->condition('project', $file->project) Chris@0: ->condition('langcode', $file->langcode) Chris@0: ->execute(); Chris@0: } Chris@0: Chris@0: // For each project+language combination a number of tasks are added to Chris@0: // the queue. Chris@0: if ($updates) { Chris@0: module_load_include('fetch.inc', 'locale'); Chris@0: $options = _locale_translation_default_update_options(); Chris@0: $queue = \Drupal::queue('locale_translation', TRUE); Chris@0: Chris@0: foreach ($updates as $project => $languages) { Chris@0: $batch = locale_translation_batch_update_build([$project], $languages, $options); Chris@0: foreach ($batch['operations'] as $item) { Chris@0: $queue->createItem($item); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determine if a file is a remote file. Chris@0: * Chris@0: * @param string $uri Chris@0: * The URI or URI pattern of the file. Chris@0: * Chris@0: * @return bool Chris@0: * TRUE if the $uri is a remote file. Chris@0: */ Chris@0: function _locale_translation_file_is_remote($uri) { Chris@0: $scheme = file_uri_scheme($uri); Chris@0: if ($scheme) { Chris@14: return !\Drupal::service('file_system')->realpath($scheme . '://'); Chris@0: } Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Compare two update sources, looking for the newer one. Chris@0: * Chris@0: * The timestamp property of the source objects are used to determine which is Chris@0: * the newer one. Chris@0: * Chris@0: * @param object $source1 Chris@0: * Source object of the first translation source. Chris@0: * @param object $source2 Chris@0: * Source object of available update. Chris@0: * Chris@0: * @return int Chris@0: * - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1 Chris@0: * is missing. Chris@0: * - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ": $source1 == $source2 OR both Chris@0: * $source1 and $source2 are missing. Chris@0: * - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT": $source1 > $source2 OR $source2 Chris@0: * is missing. Chris@0: */ Chris@0: function _locale_translation_source_compare($source1, $source2) { Chris@0: if (isset($source1->timestamp) && isset($source2->timestamp)) { Chris@0: if ($source1->timestamp == $source2->timestamp) { Chris@0: return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; Chris@0: } Chris@0: else { Chris@0: return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT; Chris@0: } Chris@0: } Chris@0: elseif (isset($source1->timestamp) && !isset($source2->timestamp)) { Chris@0: return LOCALE_TRANSLATION_SOURCE_COMPARE_GT; Chris@0: } Chris@0: elseif (!isset($source1->timestamp) && isset($source2->timestamp)) { Chris@0: return LOCALE_TRANSLATION_SOURCE_COMPARE_LT; Chris@0: } Chris@0: else { Chris@0: return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns default import options for translation update. Chris@0: * Chris@0: * @return array Chris@0: * Array of translation import options. Chris@0: */ Chris@0: function _locale_translation_default_update_options() { Chris@0: $config = \Drupal::config('locale.settings'); Chris@0: return [ Chris@0: 'customized' => LOCALE_NOT_CUSTOMIZED, Chris@0: 'overwrite_options' => [ Chris@0: 'not_customized' => $config->get('translation.overwrite_not_customized'), Chris@0: 'customized' => $config->get('translation.overwrite_customized'), Chris@0: ], Chris@0: 'finish_feedback' => TRUE, Chris@0: 'use_remote' => locale_translation_use_remote_source(), Chris@0: ]; Chris@0: }