l10n_update.translation.inc 19 KB

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