locale.translation.inc 15 KB

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