annotate core/modules/locale/locale.translation.inc @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * Common API for interface translation.
Chris@0 6 */
Chris@0 7
Chris@0 8 /**
Chris@0 9 * Comparison result of source files timestamps.
Chris@0 10 *
Chris@0 11 * Timestamp of source 1 is less than the timestamp of source 2.
Chris@0 12 *
Chris@0 13 * @see _locale_translation_source_compare()
Chris@0 14 */
Chris@0 15 const LOCALE_TRANSLATION_SOURCE_COMPARE_LT = -1;
Chris@0 16
Chris@0 17 /**
Chris@0 18 * Comparison result of source files timestamps.
Chris@0 19 *
Chris@0 20 * Timestamp of source 1 is equal to the timestamp of source 2.
Chris@0 21 *
Chris@0 22 * @see _locale_translation_source_compare()
Chris@0 23 */
Chris@0 24 const LOCALE_TRANSLATION_SOURCE_COMPARE_EQ = 0;
Chris@0 25
Chris@0 26 /**
Chris@0 27 * Comparison result of source files timestamps.
Chris@0 28 *
Chris@0 29 * Timestamp of source 1 is greater than the timestamp of source 2.
Chris@0 30 *
Chris@0 31 * @see _locale_translation_source_compare()
Chris@0 32 */
Chris@0 33 const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1;
Chris@0 34
Chris@0 35 /**
Chris@0 36 * Get array of projects which are available for interface translation.
Chris@0 37 *
Chris@0 38 * This project data contains all projects which will be checked for available
Chris@0 39 * interface translations.
Chris@0 40 *
Chris@0 41 * For full functionality this function depends on Update module.
Chris@0 42 * When Update module is enabled the project data will contain the most recent
Chris@0 43 * module status; both in enabled status as in version. When Update module is
Chris@0 44 * disabled this function will return the last known module state. The status
Chris@0 45 * will only be updated once Update module is enabled.
Chris@0 46 *
Chris@0 47 * @param array $project_names
Chris@0 48 * Array of names of the projects to get.
Chris@0 49 *
Chris@0 50 * @return array
Chris@0 51 * Array of project data for translation update.
Chris@0 52 *
Chris@0 53 * @see locale_translation_build_projects()
Chris@0 54 */
Chris@0 55 function locale_translation_get_projects(array $project_names = []) {
Chris@0 56 $projects = &drupal_static(__FUNCTION__, []);
Chris@0 57
Chris@0 58 if (empty($projects)) {
Chris@0 59 // Get project data from the database.
Chris@0 60 $row_count = \Drupal::service('locale.project')->countProjects();
Chris@0 61 // https://www.drupal.org/node/1777106 is a follow-up issue to make the
Chris@0 62 // check for possible out-of-date project information more robust.
Chris@0 63 if ($row_count == 0) {
Chris@0 64 module_load_include('compare.inc', 'locale');
Chris@0 65 // At least the core project should be in the database, so we build the
Chris@0 66 // data if none are found.
Chris@0 67 locale_translation_build_projects();
Chris@0 68 }
Chris@0 69 $projects = \Drupal::service('locale.project')->getAll();
Chris@0 70 array_walk($projects, function (&$project) {
Chris@0 71 $project = (object) $project;
Chris@0 72 });
Chris@0 73 }
Chris@0 74
Chris@0 75 // Return the requested project names or all projects.
Chris@0 76 if ($project_names) {
Chris@0 77 return array_intersect_key($projects, array_combine($project_names, $project_names));
Chris@0 78 }
Chris@0 79 return $projects;
Chris@0 80 }
Chris@0 81
Chris@0 82 /**
Chris@0 83 * Clears the projects cache.
Chris@0 84 */
Chris@0 85 function locale_translation_clear_cache_projects() {
Chris@0 86 drupal_static_reset('locale_translation_get_projects');
Chris@0 87 }
Chris@0 88
Chris@0 89 /**
Chris@0 90 * Loads cached translation sources containing current translation status.
Chris@0 91 *
Chris@0 92 * @param array $projects
Chris@0 93 * Array of project names. Defaults to all translatable projects.
Chris@0 94 * @param array $langcodes
Chris@0 95 * Array of language codes. Defaults to all translatable languages.
Chris@0 96 *
Chris@0 97 * @return array
Chris@0 98 * Array of source objects. Keyed with <project name>:<language code>.
Chris@0 99 *
Chris@0 100 * @see locale_translation_source_build()
Chris@0 101 */
Chris@0 102 function locale_translation_load_sources(array $projects = NULL, array $langcodes = NULL) {
Chris@0 103 $sources = [];
Chris@0 104 $projects = $projects ? $projects : array_keys(locale_translation_get_projects());
Chris@0 105 $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
Chris@0 106
Chris@0 107 // Load source data from locale_translation_status cache.
Chris@0 108 $status = locale_translation_get_status();
Chris@0 109
Chris@0 110 // Use only the selected projects and languages for update.
Chris@0 111 foreach ($projects as $project) {
Chris@0 112 foreach ($langcodes as $langcode) {
Chris@0 113 $sources[$project][$langcode] = isset($status[$project][$langcode]) ? $status[$project][$langcode] : NULL;
Chris@0 114 }
Chris@0 115 }
Chris@0 116 return $sources;
Chris@0 117 }
Chris@0 118
Chris@0 119 /**
Chris@0 120 * Build translation sources.
Chris@0 121 *
Chris@0 122 * @param array $projects
Chris@0 123 * Array of project names. Defaults to all translatable projects.
Chris@0 124 * @param array $langcodes
Chris@0 125 * Array of language codes. Defaults to all translatable languages.
Chris@0 126 *
Chris@0 127 * @return array
Chris@0 128 * Array of source objects. Keyed by project name and language code.
Chris@0 129 *
Chris@0 130 * @see locale_translation_source_build()
Chris@0 131 */
Chris@0 132 function locale_translation_build_sources(array $projects = [], array $langcodes = []) {
Chris@0 133 $sources = [];
Chris@0 134 $projects = locale_translation_get_projects($projects);
Chris@0 135 $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
Chris@0 136
Chris@0 137 foreach ($projects as $project) {
Chris@0 138 foreach ($langcodes as $langcode) {
Chris@0 139 $source = locale_translation_source_build($project, $langcode);
Chris@0 140 $sources[$source->name][$source->langcode] = $source;
Chris@0 141 }
Chris@0 142 }
Chris@0 143 return $sources;
Chris@0 144 }
Chris@0 145
Chris@0 146 /**
Chris@0 147 * Checks whether a po file exists in the local filesystem.
Chris@0 148 *
Chris@0 149 * It will search in the directory set in the translation source. Which defaults
Chris@0 150 * to the "translations://" stream wrapper path. The directory may contain any
Chris@0 151 * valid stream wrapper.
Chris@0 152 *
Chris@0 153 * The "local" files property of the source object contains the definition of a
Chris@0 154 * po file we are looking for. The file name defaults to
Chris@0 155 * %project-%version.%language.po. Per project this value can be overridden
Chris@0 156 * using the server_pattern directive in the module's .info.yml file or by using
Chris@0 157 * hook_locale_translation_projects_alter().
Chris@0 158 *
Chris@0 159 * @param object $source
Chris@0 160 * Translation source object.
Chris@0 161 *
Chris@0 162 * @return object
Chris@0 163 * Source file object of the po file, updated with:
Chris@0 164 * - "uri": File name and path.
Chris@0 165 * - "timestamp": Last updated time of the po file.
Chris@0 166 * FALSE if the file is not found.
Chris@0 167 *
Chris@0 168 * @see locale_translation_source_build()
Chris@0 169 */
Chris@0 170 function locale_translation_source_check_file($source) {
Chris@0 171 if (isset($source->files[LOCALE_TRANSLATION_LOCAL])) {
Chris@0 172 $source_file = $source->files[LOCALE_TRANSLATION_LOCAL];
Chris@0 173 $directory = $source_file->directory;
Chris@0 174 $filename = '/' . preg_quote($source_file->filename) . '$/';
Chris@0 175
Chris@0 176 if ($files = file_scan_directory($directory, $filename, ['key' => 'name', 'recurse' => FALSE])) {
Chris@0 177 $file = current($files);
Chris@0 178 $source_file->uri = $file->uri;
Chris@0 179 $source_file->timestamp = filemtime($file->uri);
Chris@0 180 return $source_file;
Chris@0 181 }
Chris@0 182 }
Chris@0 183 return FALSE;
Chris@0 184 }
Chris@0 185
Chris@0 186 /**
Chris@0 187 * Builds abstract translation source.
Chris@0 188 *
Chris@0 189 * @param object $project
Chris@0 190 * Project object.
Chris@0 191 * @param string $langcode
Chris@0 192 * Language code.
Chris@0 193 * @param string $filename
Chris@0 194 * (optional) File name of translation file. May contain placeholders.
Chris@0 195 * Defaults to the default translation filename from the settings.
Chris@0 196 *
Chris@0 197 * @return object
Chris@0 198 * Source object:
Chris@0 199 * - "project": Project name.
Chris@0 200 * - "name": Project name (inherited from project).
Chris@0 201 * - "language": Language code.
Chris@0 202 * - "core": Core version (inherited from project).
Chris@0 203 * - "version": Project version (inherited from project).
Chris@0 204 * - "project_type": Project type (inherited from project).
Chris@0 205 * - "files": Array of file objects containing properties of local and remote
Chris@0 206 * translation files.
Chris@0 207 * Other processes can add the following properties:
Chris@0 208 * - "type": Most recent translation source found. LOCALE_TRANSLATION_REMOTE
Chris@0 209 * and LOCALE_TRANSLATION_LOCAL indicate available new translations,
Chris@0 210 * LOCALE_TRANSLATION_CURRENT indicate that the current translation is them
Chris@0 211 * most recent. "type" corresponds with a key of the "files" array.
Chris@0 212 * - "timestamp": The creation time of the "type" translation (file).
Chris@0 213 * - "last_checked": The time when the "type" translation was last checked.
Chris@0 214 * The "files" array can hold file objects of type:
Chris@0 215 * LOCALE_TRANSLATION_LOCAL, LOCALE_TRANSLATION_REMOTE and
Chris@0 216 * LOCALE_TRANSLATION_CURRENT. Each contains following properties:
Chris@0 217 * - "type": The object type (LOCALE_TRANSLATION_LOCAL,
Chris@0 218 * LOCALE_TRANSLATION_REMOTE, etc. see above).
Chris@0 219 * - "project": Project name.
Chris@0 220 * - "langcode": Language code.
Chris@0 221 * - "version": Project version.
Chris@0 222 * - "uri": Local or remote file path.
Chris@0 223 * - "directory": Directory of the local po file.
Chris@0 224 * - "filename": File name.
Chris@0 225 * - "timestamp": Timestamp of the file.
Chris@0 226 * - "keep": TRUE to keep the downloaded file.
Chris@0 227 */
Chris@0 228 function locale_translation_source_build($project, $langcode, $filename = NULL) {
Chris@0 229 // Follow-up issue: https://www.drupal.org/node/1842380.
Chris@0 230 // Convert $source object to a TranslatableProject class and use a typed class
Chris@0 231 // for $source-file.
Chris@0 232
Chris@0 233 // Create a source object with data of the project object.
Chris@0 234 $source = clone $project;
Chris@0 235 $source->project = $project->name;
Chris@0 236 $source->langcode = $langcode;
Chris@0 237 $source->type = '';
Chris@0 238 $source->timestamp = 0;
Chris@0 239 $source->last_checked = 0;
Chris@0 240
Chris@0 241 $filename = $filename ? $filename : \Drupal::config('locale.settings')->get('translation.default_filename');
Chris@0 242
Chris@0 243 // If the server_pattern contains a remote file path we will check for a
Chris@0 244 // remote file. The local version of this file will only be checked if a
Chris@0 245 // translations directory has been defined. If the server_pattern is a local
Chris@0 246 // file path we will only check for a file in the local file system.
Chris@0 247 $files = [];
Chris@0 248 if (_locale_translation_file_is_remote($source->server_pattern)) {
Chris@0 249 $files[LOCALE_TRANSLATION_REMOTE] = (object) [
Chris@0 250 'project' => $project->name,
Chris@0 251 'langcode' => $langcode,
Chris@0 252 'version' => $project->version,
Chris@0 253 'type' => LOCALE_TRANSLATION_REMOTE,
Chris@0 254 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
Chris@0 255 'uri' => locale_translation_build_server_pattern($source, $source->server_pattern),
Chris@0 256 ];
Chris@0 257 $files[LOCALE_TRANSLATION_LOCAL] = (object) [
Chris@0 258 'project' => $project->name,
Chris@0 259 'langcode' => $langcode,
Chris@0 260 'version' => $project->version,
Chris@0 261 'type' => LOCALE_TRANSLATION_LOCAL,
Chris@0 262 'filename' => locale_translation_build_server_pattern($source, $filename),
Chris@0 263 'directory' => 'translations://',
Chris@0 264 ];
Chris@0 265 $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . $files[LOCALE_TRANSLATION_LOCAL]->filename;
Chris@0 266 }
Chris@0 267 else {
Chris@0 268 $files[LOCALE_TRANSLATION_LOCAL] = (object) [
Chris@0 269 'project' => $project->name,
Chris@0 270 'langcode' => $langcode,
Chris@0 271 'version' => $project->version,
Chris@0 272 'type' => LOCALE_TRANSLATION_LOCAL,
Chris@0 273 'filename' => locale_translation_build_server_pattern($source, basename($source->server_pattern)),
Chris@18 274 'directory' => locale_translation_build_server_pattern($source, \Drupal::service('file_system')->dirname($source->server_pattern)),
Chris@0 275 ];
Chris@0 276 $files[LOCALE_TRANSLATION_LOCAL]->uri = $files[LOCALE_TRANSLATION_LOCAL]->directory . '/' . $files[LOCALE_TRANSLATION_LOCAL]->filename;
Chris@0 277 }
Chris@0 278 $source->files = $files;
Chris@0 279
Chris@0 280 // If this project+language is already translated, we add its status and
Chris@0 281 // update the current translation timestamp and last_updated time. If the
Chris@0 282 // project+language is not translated before, create a new record.
Chris@0 283 $history = locale_translation_get_file_history();
Chris@0 284 if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) {
Chris@0 285 $source->files[LOCALE_TRANSLATION_CURRENT] = $history[$project->name][$langcode];
Chris@0 286 $source->type = LOCALE_TRANSLATION_CURRENT;
Chris@0 287 $source->timestamp = $history[$project->name][$langcode]->timestamp;
Chris@0 288 $source->last_checked = $history[$project->name][$langcode]->last_checked;
Chris@0 289 }
Chris@0 290 else {
Chris@0 291 locale_translation_update_file_history($source);
Chris@0 292 }
Chris@0 293
Chris@0 294 return $source;
Chris@0 295 }
Chris@0 296
Chris@0 297 /**
Chris@0 298 * Build path to translation source, out of a server path replacement pattern.
Chris@0 299 *
Chris@0 300 * @param object $project
Chris@0 301 * Project object containing data to be inserted in the template.
Chris@0 302 * @param string $template
Chris@0 303 * String containing placeholders. Available placeholders:
Chris@0 304 * - "%project": Project name.
Chris@0 305 * - "%version": Project version.
Chris@0 306 * - "%core": Project core version.
Chris@0 307 * - "%language": Language code.
Chris@0 308 *
Chris@0 309 * @return string
Chris@0 310 * String with replaced placeholders.
Chris@0 311 */
Chris@0 312 function locale_translation_build_server_pattern($project, $template) {
Chris@0 313 $variables = [
Chris@0 314 '%project' => $project->name,
Chris@0 315 '%version' => $project->version,
Chris@0 316 '%core' => $project->core,
Chris@0 317 '%language' => isset($project->langcode) ? $project->langcode : '%language',
Chris@0 318 ];
Chris@0 319 return strtr($template, $variables);
Chris@0 320 }
Chris@0 321
Chris@0 322 /**
Chris@0 323 * Populate a queue with project to check for translation updates.
Chris@0 324 */
Chris@0 325 function locale_cron_fill_queue() {
Chris@0 326 $updates = [];
Chris@0 327 $config = \Drupal::config('locale.settings');
Chris@0 328
Chris@0 329 // Determine which project+language should be updated.
Chris@0 330 $last = REQUEST_TIME - $config->get('translation.update_interval_days') * 3600 * 24;
Chris@0 331 $projects = \Drupal::service('locale.project')->getAll();
Chris@0 332 $projects = array_filter($projects, function ($project) {
Chris@0 333 return $project['status'] == 1;
Chris@0 334 });
Chris@18 335 $connection = \Drupal::database();
Chris@18 336 $files = $connection->select('locale_file', 'f')
Chris@0 337 ->condition('f.project', array_keys($projects), 'IN')
Chris@0 338 ->condition('f.last_checked', $last, '<')
Chris@0 339 ->fields('f', ['project', 'langcode'])
Chris@0 340 ->execute()->fetchAll();
Chris@0 341 foreach ($files as $file) {
Chris@0 342 $updates[$file->project][] = $file->langcode;
Chris@0 343
Chris@0 344 // Update the last_checked timestamp of the project+language that will
Chris@0 345 // be checked for updates.
Chris@18 346 $connection->update('locale_file')
Chris@0 347 ->fields(['last_checked' => REQUEST_TIME])
Chris@0 348 ->condition('project', $file->project)
Chris@0 349 ->condition('langcode', $file->langcode)
Chris@0 350 ->execute();
Chris@0 351 }
Chris@0 352
Chris@0 353 // For each project+language combination a number of tasks are added to
Chris@0 354 // the queue.
Chris@0 355 if ($updates) {
Chris@0 356 module_load_include('fetch.inc', 'locale');
Chris@0 357 $options = _locale_translation_default_update_options();
Chris@0 358 $queue = \Drupal::queue('locale_translation', TRUE);
Chris@0 359
Chris@0 360 foreach ($updates as $project => $languages) {
Chris@0 361 $batch = locale_translation_batch_update_build([$project], $languages, $options);
Chris@0 362 foreach ($batch['operations'] as $item) {
Chris@0 363 $queue->createItem($item);
Chris@0 364 }
Chris@0 365 }
Chris@0 366 }
Chris@0 367 }
Chris@0 368
Chris@0 369 /**
Chris@0 370 * Determine if a file is a remote file.
Chris@0 371 *
Chris@0 372 * @param string $uri
Chris@0 373 * The URI or URI pattern of the file.
Chris@0 374 *
Chris@0 375 * @return bool
Chris@0 376 * TRUE if the $uri is a remote file.
Chris@0 377 */
Chris@0 378 function _locale_translation_file_is_remote($uri) {
Chris@0 379 $scheme = file_uri_scheme($uri);
Chris@0 380 if ($scheme) {
Chris@14 381 return !\Drupal::service('file_system')->realpath($scheme . '://');
Chris@0 382 }
Chris@0 383 return FALSE;
Chris@0 384 }
Chris@0 385
Chris@0 386 /**
Chris@0 387 * Compare two update sources, looking for the newer one.
Chris@0 388 *
Chris@0 389 * The timestamp property of the source objects are used to determine which is
Chris@0 390 * the newer one.
Chris@0 391 *
Chris@0 392 * @param object $source1
Chris@0 393 * Source object of the first translation source.
Chris@0 394 * @param object $source2
Chris@0 395 * Source object of available update.
Chris@0 396 *
Chris@0 397 * @return int
Chris@0 398 * - "LOCALE_TRANSLATION_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
Chris@0 399 * is missing.
Chris@0 400 * - "LOCALE_TRANSLATION_SOURCE_COMPARE_EQ": $source1 == $source2 OR both
Chris@0 401 * $source1 and $source2 are missing.
Chris@0 402 * - "LOCALE_TRANSLATION_SOURCE_COMPARE_GT": $source1 > $source2 OR $source2
Chris@0 403 * is missing.
Chris@0 404 */
Chris@0 405 function _locale_translation_source_compare($source1, $source2) {
Chris@0 406 if (isset($source1->timestamp) && isset($source2->timestamp)) {
Chris@0 407 if ($source1->timestamp == $source2->timestamp) {
Chris@0 408 return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
Chris@0 409 }
Chris@0 410 else {
Chris@0 411 return $source1->timestamp > $source2->timestamp ? LOCALE_TRANSLATION_SOURCE_COMPARE_GT : LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
Chris@0 412 }
Chris@0 413 }
Chris@0 414 elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
Chris@0 415 return LOCALE_TRANSLATION_SOURCE_COMPARE_GT;
Chris@0 416 }
Chris@0 417 elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
Chris@0 418 return LOCALE_TRANSLATION_SOURCE_COMPARE_LT;
Chris@0 419 }
Chris@0 420 else {
Chris@0 421 return LOCALE_TRANSLATION_SOURCE_COMPARE_EQ;
Chris@0 422 }
Chris@0 423 }
Chris@0 424
Chris@0 425 /**
Chris@0 426 * Returns default import options for translation update.
Chris@0 427 *
Chris@0 428 * @return array
Chris@0 429 * Array of translation import options.
Chris@0 430 */
Chris@0 431 function _locale_translation_default_update_options() {
Chris@0 432 $config = \Drupal::config('locale.settings');
Chris@0 433 return [
Chris@0 434 'customized' => LOCALE_NOT_CUSTOMIZED,
Chris@0 435 'overwrite_options' => [
Chris@0 436 'not_customized' => $config->get('translation.overwrite_not_customized'),
Chris@0 437 'customized' => $config->get('translation.overwrite_customized'),
Chris@0 438 ],
Chris@0 439 'finish_feedback' => TRUE,
Chris@0 440 'use_remote' => locale_translation_use_remote_source(),
Chris@0 441 ];
Chris@0 442 }