l10n_update.translation.inc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  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 _l10n_update_source_compare()
  12. */
  13. define('L10N_UPDATE_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 _l10n_update_source_compare()
  20. */
  21. define('L10N_UPDATE_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 _l10n_update_source_compare()
  28. */
  29. define('L10N_UPDATE_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 l10n_update_build_projects()
  49. */
  50. function l10n_update_get_projects($project_names = array()) {
  51. $projects = &drupal_static(__FUNCTION__, array());
  52. if (empty($projects)) {
  53. // Get project data from the database.
  54. $result = db_query('SELECT name, project_type, core, version, l10n_path as server_pattern, status FROM {l10n_update_project}');
  55. // https://www.drupal.org/node/1777106 is a follow-up issue to make the check for
  56. // possible out-of-date project information more robust.
  57. if ($result->rowCount() == 0) {
  58. module_load_include('compare.inc', 'l10n_update');
  59. // At least the core project should be in the database, so we build the
  60. // data if none are found.
  61. l10n_update_build_projects();
  62. $result = db_query('SELECT name, project_type, core, version, l10n_path as server_pattern, status FROM {l10n_update_project}');
  63. }
  64. foreach ($result as $project) {
  65. $projects[$project->name] = $project;
  66. }
  67. }
  68. // Return the requested project names or all projects.
  69. if ($project_names) {
  70. return array_intersect_key($projects, drupal_map_assoc($project_names));
  71. }
  72. return $projects;
  73. }
  74. /**
  75. * Clears the projects cache.
  76. */
  77. function l10n_update_clear_cache_projects() {
  78. drupal_static('l10n_update_get_projects', array());
  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 l10n_update_source_build()
  92. */
  93. function l10n_update_load_sources(array $projects = NULL, array $langcodes = NULL) {
  94. $sources = array();
  95. $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
  96. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  97. // Load source data from l10n_update_status cache.
  98. $status = l10n_update_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 l10n_update_source_build()
  119. */
  120. function l10n_update_build_sources($projects = array(), $langcodes = array()) {
  121. $sources = array();
  122. $projects = l10n_update_get_projects($projects);
  123. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  124. foreach ($projects as $project) {
  125. foreach ($langcodes as $langcode) {
  126. $source = l10n_update_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-%release.%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_l10n_update_projects_alter().
  144. *
  145. * @param object $source
  146. * Translation source object.
  147. *
  148. * @return object|bool
  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 l10n_update_source_build()
  155. */
  156. function l10n_update_source_check_file($source) {
  157. if (isset($source->files[L10N_UPDATE_LOCAL])) {
  158. $source_file = $source->files[L10N_UPDATE_LOCAL];
  159. $directory = $source_file->directory;
  160. $filename = '/^' . preg_quote($source_file->filename) . '$/';
  161. if ($files = file_scan_directory($directory, $filename, array('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. * File name of translation file. May contain placeholders.
  179. *
  180. * @return object
  181. * Source object:
  182. * - "project": Project name.
  183. * - "name": Project name (inherited from project).
  184. * - "language": Language code.
  185. * - "core": Core version (inherited from project).
  186. * - "version": Project version (inherited from project).
  187. * - "project_type": Project type (inherited from project).
  188. * - "files": Array of file objects containing properties of local and remote
  189. * translation files.
  190. * Other processes can add the following properties:
  191. * - "type": Most recent translation source found. L10N_UPDATE_REMOTE and
  192. * L10N_UPDATE_LOCAL indicate available new translations,
  193. * L10N_UPDATE_CURRENT indicate that the current translation is them
  194. * most recent. "type" sorresponds with a key of the "files" array.
  195. * - "timestamp": The creation time of the "type" translation (file).
  196. * - "last_checked": The time when the "type" translation was last checked.
  197. * The "files" array can hold file objects of type:
  198. * L10N_UPDATE_LOCAL, L10N_UPDATE_REMOTE and
  199. * L10N_UPDATE_CURRENT. Each contains following properties:
  200. * - "type": The object type (L10N_UPDATE_LOCAL,
  201. * L10N_UPDATE_REMOTE, etc. see above).
  202. * - "project": Project name.
  203. * - "langcode": Language code.
  204. * - "version": Project version.
  205. * - "uri": Local or remote file path.
  206. * - "directory": Directory of the local po file.
  207. * - "filename": File name.
  208. * - "timestamp": Timestamp of the file.
  209. * - "keep": TRUE to keep the downloaded file.
  210. */
  211. function l10n_update_source_build($project, $langcode, $filename = NULL) {
  212. // Create a source object with data of the project object.
  213. $source = clone $project;
  214. $source->project = $project->name;
  215. $source->langcode = $langcode;
  216. $source->type = '';
  217. $source->timestamp = 0;
  218. $source->last_checked = 0;
  219. $filename = $filename ? $filename : variable_get('l10n_update_default_filename', L10N_UPDATE_DEFAULT_FILE_NAME);
  220. // If the server_pattern contains a remote file path we will check for a
  221. // remote file. The local version of this file will only be checked if a
  222. // translations directory has been defined. If the server_pattern is a local
  223. // file path we will only check for a file in the local file system.
  224. $files = array();
  225. if (_l10n_update_file_is_remote($source->server_pattern)) {
  226. $files[L10N_UPDATE_REMOTE] = (object) array(
  227. 'project' => $project->name,
  228. 'langcode' => $langcode,
  229. 'version' => $project->version,
  230. 'type' => L10N_UPDATE_REMOTE,
  231. 'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
  232. 'uri' => l10n_update_build_server_pattern($source, $source->server_pattern),
  233. );
  234. $files[L10N_UPDATE_LOCAL] = (object) array(
  235. 'project' => $project->name,
  236. 'langcode' => $langcode,
  237. 'version' => $project->version,
  238. 'type' => L10N_UPDATE_LOCAL,
  239. 'filename' => l10n_update_build_server_pattern($source, $filename),
  240. 'directory' => 'translations://',
  241. );
  242. $files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . $files[L10N_UPDATE_LOCAL]->filename;
  243. }
  244. else {
  245. $files[L10N_UPDATE_LOCAL] = (object) array(
  246. 'project' => $project->name,
  247. 'langcode' => $langcode,
  248. 'version' => $project->version,
  249. 'type' => L10N_UPDATE_LOCAL,
  250. 'filename' => l10n_update_build_server_pattern($source, basename($source->server_pattern)),
  251. 'directory' => l10n_update_build_server_pattern($source, drupal_dirname($source->server_pattern)),
  252. );
  253. $files[L10N_UPDATE_LOCAL]->uri = $files[L10N_UPDATE_LOCAL]->directory . '/' . $files[L10N_UPDATE_LOCAL]->filename;
  254. }
  255. $source->files = $files;
  256. // If this project+language is already translated, we add its status and
  257. // update the current translation timestamp and last_updated time. If the
  258. // project+language is not translated before, create a new record.
  259. $history = l10n_update_get_file_history();
  260. if (isset($history[$project->name][$langcode]) && $history[$project->name][$langcode]->timestamp) {
  261. $source->files[L10N_UPDATE_CURRENT] = $history[$project->name][$langcode];
  262. $source->type = L10N_UPDATE_CURRENT;
  263. $source->timestamp = $history[$project->name][$langcode]->timestamp;
  264. $source->last_checked = $history[$project->name][$langcode]->last_checked;
  265. }
  266. else {
  267. l10n_update_update_file_history($source);
  268. }
  269. return $source;
  270. }
  271. /**
  272. * Build path to translation source, out of a server path replacement pattern.
  273. *
  274. * @param object $project
  275. * Project object containing data to be inserted in the template.
  276. * @param string $template
  277. * String containing placeholders. Available placeholders:
  278. * - "%project": Project name.
  279. * - "%release": Project version.
  280. * - "%core": Project core version.
  281. * - "%language": Language code.
  282. *
  283. * @return string
  284. * String with replaced placeholders.
  285. */
  286. function l10n_update_build_server_pattern($project, $template) {
  287. $variables = array(
  288. '%project' => $project->name,
  289. '%release' => $project->version,
  290. '%core' => $project->core,
  291. '%language' => isset($project->langcode) ? $project->langcode : '%language',
  292. );
  293. return strtr($template, $variables);
  294. }
  295. /**
  296. * Populate a queue with project to check for translation updates.
  297. */
  298. function l10n_update_cron_fill_queue() {
  299. $updates = array();
  300. // Determine which project+language should be updated.
  301. $last = REQUEST_TIME - variable_get('l10n_update_check_frequency', '0') * 3600 * 24;
  302. $query = db_select('l10n_update_file', 'f');
  303. $query->join('l10n_update_project', 'p', 'p.name = f.project');
  304. $query->condition('f.last_checked', $last, '<');
  305. $query->fields('f', array('project', 'language'));
  306. // Only currently installed / enabled components should be checked for.
  307. $query->condition('p.status', 1);
  308. $files = $query->execute()->fetchAll();
  309. foreach ($files as $file) {
  310. $updates[$file->project][] = $file->language;
  311. // Update the last_checked timestamp of the project+language that will
  312. // be checked for updates.
  313. db_update('l10n_update_file')
  314. ->fields(array('last_checked' => REQUEST_TIME))
  315. ->condition('project', $file->project)
  316. ->condition('language', $file->language)
  317. ->execute();
  318. }
  319. // For each project+language combination a number of tasks are added to
  320. // the queue.
  321. if ($updates) {
  322. module_load_include('fetch.inc', 'l10n_update');
  323. $options = _l10n_update_default_update_options();
  324. $queue = DrupalQueue::get('l10n_update', TRUE);
  325. foreach ($updates as $project => $languages) {
  326. $batch = l10n_update_batch_update_build(array($project), $languages, $options);
  327. foreach ($batch['operations'] as $item) {
  328. $queue->createItem($item);
  329. }
  330. }
  331. }
  332. }
  333. /**
  334. * Determine if a file is a remote file.
  335. *
  336. * @param string $uri
  337. * The URI or URI pattern of the file.
  338. *
  339. * @return bool
  340. * TRUE if the $uri is a remote file.
  341. */
  342. function _l10n_update_file_is_remote($uri) {
  343. $scheme = file_uri_scheme($uri);
  344. if ($scheme) {
  345. if ($scheme == 'http' || $scheme == 'https') {
  346. return TRUE;
  347. }
  348. return !drupal_realpath($scheme . '://');
  349. }
  350. return FALSE;
  351. }
  352. /**
  353. * Compare two update sources, looking for the newer one.
  354. *
  355. * The timestamp property of the source objects are used to determine which is
  356. * the newer one.
  357. *
  358. * @param object $source1
  359. * Source object of the first translation source.
  360. * @param object $source2
  361. * Source object of available update.
  362. *
  363. * @return int
  364. * - "L10N_UPDATE_SOURCE_COMPARE_LT": $source1 < $source2 OR $source1
  365. * is missing.
  366. * - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 == $source2 OR both
  367. * $source1 and $source2 are missing.
  368. * - "L10N_UPDATE_SOURCE_COMPARE_EQ": $source1 > $source2 OR $source2
  369. * is missing.
  370. */
  371. function _l10n_update_source_compare($source1, $source2) {
  372. if (isset($source1->timestamp) && isset($source2->timestamp)) {
  373. if ($source1->timestamp == $source2->timestamp) {
  374. return L10N_UPDATE_SOURCE_COMPARE_EQ;
  375. }
  376. else {
  377. return $source1->timestamp > $source2->timestamp ? L10N_UPDATE_SOURCE_COMPARE_GT : L10N_UPDATE_SOURCE_COMPARE_LT;
  378. }
  379. }
  380. elseif (isset($source1->timestamp) && !isset($source2->timestamp)) {
  381. return L10N_UPDATE_SOURCE_COMPARE_GT;
  382. }
  383. elseif (!isset($source1->timestamp) && isset($source2->timestamp)) {
  384. return L10N_UPDATE_SOURCE_COMPARE_LT;
  385. }
  386. else {
  387. return L10N_UPDATE_SOURCE_COMPARE_EQ;
  388. }
  389. }
  390. /**
  391. * Returns default import options for translation update.
  392. *
  393. * @return array
  394. * Array of translation import options.
  395. */
  396. function _l10n_update_default_update_options() {
  397. $options = array(
  398. 'customized' => L10N_UPDATE_NOT_CUSTOMIZED,
  399. 'finish_feedback' => TRUE,
  400. 'use_remote' => l10n_update_use_remote_source(),
  401. );
  402. switch (variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP)) {
  403. case LOCALE_IMPORT_OVERWRITE:
  404. $options['overwrite_options'] = array(
  405. 'customized' => TRUE,
  406. 'not_customized' => TRUE,
  407. );
  408. break;
  409. case L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED:
  410. $options['overwrite_options'] = array(
  411. 'customized' => FALSE,
  412. 'not_customized' => TRUE,
  413. );
  414. break;
  415. case LOCALE_IMPORT_KEEP:
  416. $options['overwrite_options'] = array(
  417. 'customized' => FALSE,
  418. 'not_customized' => FALSE,
  419. );
  420. break;
  421. }
  422. return $options;
  423. }
  424. /**
  425. * Import one string into the database.
  426. *
  427. * @param array $report
  428. * Report array summarizing the number of changes done in the form:
  429. * array(inserts, updates, deletes).
  430. * @param string $langcode
  431. * Language code to import string into.
  432. * @param string $context
  433. * The context of this string.
  434. * @param string $source
  435. * Source string.
  436. * @param string $translation
  437. * Translation to language specified in $langcode.
  438. * @param string $textgroup
  439. * Name of textgroup to store translation in.
  440. * @param string $location
  441. * Location value to save with source string.
  442. * @param int $mode
  443. * Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
  444. * @param int $status
  445. * Status of translation if created: L10N_UPDATE_STRING_DEFAULT or
  446. * L10N_UPDATE_STRING_CUSTOM.
  447. * @param int $plid
  448. * Optional plural ID to use.
  449. * @param int $plural
  450. * Optional plural value to use.
  451. *
  452. * @return string
  453. * The string ID of the existing string modified or the new string added.
  454. */
  455. function _l10n_update_locale_import_one_string_db(array &$report, $langcode, $context, $source, $translation, $textgroup, $location, $mode, $status = L10N_UPDATE_NOT_CUSTOMIZED, $plid = 0, $plural = 0) {
  456. $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(
  457. ':source' => $source,
  458. ':context' => $context,
  459. ':textgroup' => $textgroup,
  460. ))->fetchField();
  461. if (!empty($translation)) {
  462. // Skip this string unless it passes a check for dangerous code.
  463. // Text groups other than default still can contain HTML tags
  464. // (i.e. translatable blocks).
  465. if ($textgroup == "default" && !locale_string_is_safe($translation)) {
  466. $report['skips']++;
  467. $lid = 0;
  468. watchdog('locale', 'Disallowed HTML detected. String not imported: %string', array('%string' => $translation), WATCHDOG_WARNING);
  469. }
  470. elseif ($lid) {
  471. // We have this source string saved already.
  472. db_update('locales_source')
  473. ->fields(array(
  474. 'location' => $location,
  475. ))
  476. ->condition('lid', $lid)
  477. ->execute();
  478. $exists = db_query("SELECT lid, l10n_status FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchObject();
  479. if (!$exists) {
  480. // No translation in this language.
  481. db_insert('locales_target')
  482. ->fields(array(
  483. 'lid' => $lid,
  484. 'language' => $langcode,
  485. 'translation' => $translation,
  486. 'plid' => $plid,
  487. 'plural' => $plural,
  488. ))
  489. ->execute();
  490. $report['additions']++;
  491. }
  492. elseif (($exists->l10n_status == L10N_UPDATE_NOT_CUSTOMIZED && $mode == L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED) || $mode == LOCALE_IMPORT_OVERWRITE) {
  493. // Translation exists, only overwrite if instructed.
  494. db_update('locales_target')
  495. ->fields(array(
  496. 'translation' => $translation,
  497. 'plid' => $plid,
  498. 'plural' => $plural,
  499. ))
  500. ->condition('language', $langcode)
  501. ->condition('lid', $lid)
  502. ->execute();
  503. $report['updates']++;
  504. }
  505. }
  506. else {
  507. // No such source string in the database yet.
  508. $lid = db_insert('locales_source')
  509. ->fields(array(
  510. 'location' => $location,
  511. 'source' => $source,
  512. 'context' => (string) $context,
  513. 'textgroup' => $textgroup,
  514. ))
  515. ->execute();
  516. db_insert('locales_target')
  517. ->fields(array(
  518. 'lid' => $lid,
  519. 'language' => $langcode,
  520. 'translation' => $translation,
  521. 'plid' => $plid,
  522. 'plural' => $plural,
  523. 'l10n_status' => $status,
  524. ))
  525. ->execute();
  526. $report['additions']++;
  527. }
  528. }
  529. elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
  530. // Empty translation, remove existing if instructed.
  531. db_delete('locales_target')
  532. ->condition('language', $langcode)
  533. ->condition('lid', $lid)
  534. ->condition('plid', $plid)
  535. ->condition('plural', $plural)
  536. ->execute();
  537. $report['deletes']++;
  538. }
  539. return $lid;
  540. }