l10n_update.module 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. <?php
  2. /**
  3. * @file
  4. * Download translations from remote localization server.
  5. */
  6. /**
  7. * Translation update mode: Use local files only.
  8. *
  9. * When checking for available translation updates, only local files will be
  10. * used. Any remote translation file will be ignored. Also custom modules and
  11. * themes which have set a "server pattern" to use a remote translation server
  12. * will be ignored.
  13. */
  14. define('L10N_UPDATE_USE_SOURCE_LOCAL', 2);
  15. /**
  16. * Translation update mode: Use both remote and local files.
  17. *
  18. * When checking for available translation updates, both local and remote files
  19. * will be checked.
  20. */
  21. define('L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL', 3);
  22. /**
  23. * Default location of gettext file on the translation server.
  24. *
  25. * @see l10n_update_default_translation_server().
  26. */
  27. define('L10N_UPDATE_DEFAULT_SERVER_PATTERN', 'https://ftp.drupal.org/files/translations/%core/%project/%project-%release.%language.po');
  28. /**
  29. * Default gettext file name on the translation server.
  30. */
  31. define('L10N_UPDATE_DEFAULT_FILE_NAME', '%project-%release.%language.po');
  32. /**
  33. * Default gettext file name on the translation server.
  34. */
  35. define('L10N_UPDATE_DEFAULT_TRANSLATION_PATH', 'sites/all/translations');
  36. /**
  37. * The number of seconds that the translations status entry is valid.
  38. */
  39. define('L10N_UPDATE_STATUS_TTL', 600);
  40. /**
  41. * UI option for override of existing translations.
  42. *
  43. * Only override non-customized translations.
  44. */
  45. define('L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED', 2);
  46. /**
  47. * Translation source is a remote file.
  48. */
  49. define('L10N_UPDATE_REMOTE', 'remote');
  50. /**
  51. * Translation source is a local file.
  52. */
  53. define('L10N_UPDATE_LOCAL', 'local');
  54. /**
  55. * Translation source is the current translation.
  56. */
  57. define('L10N_UPDATE_CURRENT', 'current');
  58. /**
  59. * The delimiter used to split plural strings.
  60. *
  61. * This is the ETX (End of text) character and is used as a minimal means to
  62. * separate singular and plural variants in source and translation text. It
  63. * was found to be the most compatible delimiter for the supported databases.
  64. */
  65. define('L10N_UPDATE_PLURAL_DELIMITER', "\03");
  66. /**
  67. * Flag for locally not customized interface translation.
  68. *
  69. * Such translations are imported from .po files downloaded from
  70. * localize.drupal.org for example.
  71. */
  72. define('L10N_UPDATE_NOT_CUSTOMIZED', 0);
  73. /**
  74. * Flag for locally customized interface translation.
  75. *
  76. * Strings are customized when translated or edited using the build in
  77. * string translation form. Strings can also be marked as customized when a po
  78. * file is imported.
  79. */
  80. define('L10N_UPDATE_STRING_CUSTOM', 1);
  81. /**
  82. * Flag for locally customized interface translation.
  83. *
  84. * Such translations are edited from their imported originals on the user
  85. * interface or are imported as customized.
  86. */
  87. define('L10N_UPDATE_CUSTOMIZED', 1);
  88. /**
  89. * Implements hook_help().
  90. */
  91. function l10n_update_help($path, $arg) {
  92. $output = '';
  93. switch ($path) {
  94. case 'admin/config/regional/translate/update':
  95. $output = '<p>' . t('Status of interface translations for each of the enabled languages.') . '</p>';
  96. $output .= '<p>' . t('If there are available updates you can click on "Update translation" for them to be downloaded and imported now or you can edit the configuration for them to be updated automatically on the <a href="@update-settings">Update settings page</a>', array('@update-settings' => url('admin/config/regional/language/update'))) . '</p>';
  97. break;
  98. case 'admin/config/regional/language/update':
  99. $output = '<p>' . t('These are the settings for the translation update system. To update your translations now, check out the <a href="@update-admin">Translation update administration page</a>.', array('@update-admin' => url('admin/config/regional/translate/update'))) . '</p>';
  100. break;
  101. }
  102. return $output;
  103. }
  104. /**
  105. * Implements hook_menu().
  106. */
  107. function l10n_update_menu() {
  108. $items['admin/config/regional/translate/update'] = array(
  109. 'title' => 'Update',
  110. 'description' => 'Available updates',
  111. 'page callback' => 'drupal_get_form',
  112. 'page arguments' => array('l10n_update_status_form'),
  113. 'access arguments' => array('translate interface'),
  114. 'file' => 'l10n_update.admin.inc',
  115. 'weight' => 20,
  116. 'type' => MENU_LOCAL_TASK,
  117. );
  118. $items['admin/config/regional/translate/check'] = array(
  119. 'title' => 'Update',
  120. 'description' => 'Available updates',
  121. 'page callback' => 'l10n_update_manual_status',
  122. 'access arguments' => array('translate interface'),
  123. 'file' => 'l10n_update.admin.inc',
  124. 'weight' => 20,
  125. 'type' => MENU_CALLBACK,
  126. );
  127. $items['admin/config/regional/language/update'] = array(
  128. 'title' => 'Translation updates',
  129. 'description' => 'Automatic update configuration',
  130. 'page callback' => 'drupal_get_form',
  131. 'page arguments' => array('l10n_update_admin_settings_form'),
  132. 'access arguments' => array('translate interface'),
  133. 'file' => 'l10n_update.admin.inc',
  134. 'weight' => 20,
  135. 'type' => MENU_LOCAL_TASK,
  136. );
  137. return $items;
  138. }
  139. /**
  140. * Implements hook_theme().
  141. */
  142. function l10n_update_theme() {
  143. return array(
  144. 'l10n_update_last_check' => array(
  145. 'variables' => array('last' => NULL),
  146. 'file' => 'l10n_update.admin.inc',
  147. 'template' => 'l10n_update-translation-last-check',
  148. ),
  149. 'l10n_update_update_info' => array(
  150. 'variables' => array('updates' => array(), 'not_found' => array()),
  151. 'file' => 'l10n_update.admin.inc',
  152. 'template' => 'l10n_update-translation-update-info',
  153. ),
  154. );
  155. }
  156. /**
  157. * Implements hook_menu_alter().
  158. */
  159. function l10n_update_menu_alter(&$menu) {
  160. // Redirect l10n_client AJAX callback path for strings.
  161. if (module_exists('l10n_client')) {
  162. $menu['l10n_client/save']['page callback'] = 'l10n_update_client_save_string';
  163. }
  164. }
  165. /**
  166. * Implements hook_cron().
  167. *
  168. * Checks interface translations. Downloads and imports updates when available.
  169. */
  170. function l10n_update_cron() {
  171. // Update translations only when an update frequency was set by the admin
  172. // and a translatable language was set.
  173. // Update tasks are added to the queue here but processed by Drupal's cron
  174. // using the cron worker defined in l10n_update_queue_info().
  175. if ($frequency = variable_get('l10n_update_check_frequency', '0') && l10n_update_translatable_language_list()) {
  176. module_load_include('translation.inc', 'l10n_update');
  177. l10n_update_cron_fill_queue();
  178. }
  179. }
  180. /**
  181. * Implements hook_cron_queue_info().
  182. */
  183. function l10n_update_cron_queue_info() {
  184. $queues['l10n_update'] = array(
  185. 'worker callback' => 'l10n_update_worker',
  186. 'time' => 30,
  187. );
  188. return $queues;
  189. }
  190. /**
  191. * Callback: Executes interface translation queue tasks.
  192. *
  193. * The translation update functions executed here are batch operations which
  194. * are also used in translation update batches. The batch functions may need to
  195. * be executed multiple times to complete their task, typically this is the
  196. * translation import function. When a batch function is not finished, a new
  197. * queue task is created and added to the end of the queue. The batch context
  198. * data is needed to continue the batch task is stored in the queue with the
  199. * queue data.
  200. *
  201. * @param array $data
  202. * Queue data array containing:
  203. * - Function name.
  204. * - Array of function arguments. Optionally contains the batch context data.
  205. *
  206. * @see l10n_update_queue_info()
  207. */
  208. function l10n_update_worker(array $data) {
  209. module_load_include('batch.inc', 'l10n_update');
  210. list($function, $args) = $data;
  211. // We execute batch operation functions here to check, download and import the
  212. // translation files. Batch functions use a context variable as last argument
  213. // which is passed by reference. When a batch operation is called for the
  214. // first time a default batch context is created. When called iterative
  215. // (usually the batch import function) the batch context is passed through via
  216. // the queue and is part of the $data.
  217. $last = count($args) - 1;
  218. if (!is_array($args[$last]) || !isset($args[$last]['finished'])) {
  219. $batch_context = array(
  220. 'sandbox' => array(),
  221. 'results' => array(),
  222. 'finished' => 1,
  223. 'message' => '',
  224. );
  225. }
  226. else {
  227. $batch_context = $args[$last];
  228. unset($args[$last]);
  229. }
  230. $args = array_merge($args, array(&$batch_context));
  231. // Call the batch operation function.
  232. call_user_func_array($function, $args);
  233. // If the batch operation is not finished we create a new queue task to
  234. // continue the task. This is typically the translation import task.
  235. if ($batch_context['finished'] < 1) {
  236. unset($batch_context['strings']);
  237. $queue = DrupalQueue::get('l10n_update', TRUE);
  238. $queue->createItem(array($function, $args));
  239. }
  240. }
  241. /**
  242. * Implements hook_stream_wrappers().
  243. */
  244. function l10n_update_stream_wrappers() {
  245. // Load the stream wrapper class if not automatically loaded. This happens
  246. // before update.php is executed.
  247. if (!class_exists('TranslationsStreamWrapper')) {
  248. require_once 'includes/locale/TranslationsStreamWrapper.php';
  249. }
  250. $wrappers['translations'] = array(
  251. 'name' => t('Translation files'),
  252. 'class' => 'TranslationsStreamWrapper',
  253. 'description' => t('Translation files.'),
  254. 'type' => STREAM_WRAPPERS_LOCAL_HIDDEN,
  255. );
  256. return $wrappers;
  257. }
  258. /**
  259. * Implements hook_form_alter().
  260. */
  261. function l10n_update_form_alter(&$form, $form_state, $form_id) {
  262. switch ($form_id) {
  263. case 'locale_translate_edit_form':
  264. case 'i18n_string_locale_translate_edit_form':
  265. $form['#submit'][] = 'l10n_update_locale_translate_edit_form_submit';
  266. break;
  267. case 'locale_languages_predefined_form':
  268. case 'locale_languages_custom_form':
  269. $form['#submit'][] = 'l10n_update_languages_changed_submit';
  270. break;
  271. case 'locale_languages_delete_form':
  272. // A language is being deleted.
  273. $form['#submit'][] = 'l10n_update_languages_delete_submit';
  274. break;
  275. }
  276. }
  277. /**
  278. * Implements hook_modules_enabled().
  279. *
  280. * Refresh project translation status and get translations if required.
  281. */
  282. function l10n_update_modules_enabled($modules) {
  283. $components['module'] = $modules;
  284. l10n_update_system_update($components);
  285. }
  286. /**
  287. * Implements hook_modules_disabled().
  288. *
  289. * Set disabled modules to be ignored when updating translations.
  290. */
  291. function l10n_update_modules_disabled($modules) {
  292. if (!variable_get('l10n_update_check_disabled', FALSE)) {
  293. db_update('l10n_update_project')
  294. ->fields(array(
  295. 'status' => 0,
  296. ))
  297. ->condition('name', $modules)
  298. ->execute();
  299. }
  300. }
  301. /**
  302. * Implements hook_modules_uninstalled().
  303. *
  304. * Remove data of uninstalled modules from {l10n_update_file} table and
  305. * rebuild the projects cache.
  306. */
  307. function l10n_update_modules_uninstalled($modules) {
  308. $components['module'] = $modules;
  309. l10n_update_system_remove($components);
  310. }
  311. /**
  312. * Implements hook_themes_enabled().
  313. *
  314. * Refresh project translation status and get translations if required.
  315. */
  316. function l10n_update_themes_enabled($themes) {
  317. $components['theme'] = $themes;
  318. l10n_update_system_update($components);
  319. }
  320. /**
  321. * Implements hook_l10n_update_languages_alter().
  322. *
  323. * Removes disabled languages from the language list.
  324. */
  325. function l10n_update_l10n_update_languages_alter(array &$langcodes) {
  326. $disabled = array_filter(variable_get('l10n_update_disabled_languages', array()));
  327. $langcodes = array_diff_key($langcodes, $disabled);
  328. }
  329. /**
  330. * Additional submit handler for language forms.
  331. *
  332. * We need to refresh status when a new language is enabled / disabled.
  333. */
  334. function l10n_update_languages_changed_submit($form, $form_state) {
  335. if (variable_get('l10n_update_import_enabled', TRUE)) {
  336. if (empty($form_state['values']['predefined_langcode']) || $form_state['values']['predefined_langcode'] == 'custom') {
  337. $langcode = $form_state['values']['langcode'];
  338. }
  339. else {
  340. $langcode = $form_state['values']['predefined_langcode'];
  341. }
  342. // Download and import translations for the newly added language.
  343. module_load_include('fetch.inc', 'l10n_update');
  344. $options = _l10n_update_default_update_options();
  345. $batch = l10n_update_batch_update_build(array(), array($langcode), $options);
  346. batch_set($batch);
  347. }
  348. }
  349. /**
  350. * Additional submit handler for language deletion form.
  351. *
  352. * When a language is deleted, the file history of this language is cleared.
  353. */
  354. function l10n_update_languages_delete_submit($form, $form_state) {
  355. $langcode = $form_state['values']['langcode'];
  356. l10n_update_file_history_delete(array(), $langcode);
  357. }
  358. /**
  359. * Additional submit handler for locale and i18n_string translation edit form.
  360. *
  361. * Mark locally edited translations as customized.
  362. *
  363. * @see l10n_update_form_alter()
  364. */
  365. function l10n_update_locale_translate_edit_form_submit($form, &$form_state) {
  366. $lid = $form_state['values']['lid'];
  367. $languages = $form_state['values']['translations'];
  368. $languages = array_intersect_key($languages, l10n_update_translatable_language_list());
  369. foreach ($languages as $langcode => $value) {
  370. if (!empty($value) && $value != $form_state['complete form']['translations'][$langcode]['#default_value']) {
  371. // An update has been made, mark the string as customized.
  372. db_update('locales_target')
  373. ->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
  374. ->condition('lid', $lid)
  375. ->condition('language', $langcode)
  376. ->execute();
  377. }
  378. }
  379. }
  380. /**
  381. * Menu callback. Saves a string translation coming as POST data.
  382. */
  383. function l10n_update_client_save_string() {
  384. global $user, $language;
  385. if (l10n_client_access()) {
  386. if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) {
  387. // Ensure we have this source string before we attempt to save it.
  388. // @todo: add actual context support.
  389. $lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context AND textgroup = :textgroup", array(
  390. ':source' => $_POST['source'],
  391. ':context' => '',
  392. ':textgroup' => $_POST['textgroup'],
  393. ))->fetchField();
  394. if (!empty($lid)) {
  395. module_load_include('translation.inc', 'l10n_update');
  396. $report = array(
  397. 'skips' => 0,
  398. 'additions' => 0,
  399. 'updates' => 0,
  400. 'deletes' => 0,
  401. );
  402. // @todo: add actual context support.
  403. _l10n_update_locale_import_one_string_db($report, $language->language, '', $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE, L10N_UPDATE_STRING_CUSTOM);
  404. cache_clear_all('locale:', 'cache', TRUE);
  405. _locale_invalidate_js($language->language);
  406. if (!empty($report['skips'])) {
  407. $message = theme('l10n_client_message', array('message' => t('Not saved locally due to invalid HTML content.')));
  408. }
  409. elseif (!empty($report['additions']) || !empty($report['updates'])) {
  410. $message = theme('l10n_client_message', array('message' => t('Translation saved locally.'), 'level' => WATCHDOG_INFO));
  411. }
  412. elseif (!empty($report['deletes'])) {
  413. $message = theme('l10n_client_message', array('message' => t('Translation successfully removed locally.'), 'level' => WATCHDOG_INFO));
  414. }
  415. else {
  416. $message = theme('l10n_client_message', array('message' => t('Unknown error while saving translation locally.')));
  417. }
  418. // Submit to remote server if enabled.
  419. if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) {
  420. if (!empty($user->data['l10n_client_key'])) {
  421. $remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->data['l10n_client_key'], l10n_client_user_token($user));
  422. $message .= theme('l10n_client_message', array('message' => $remote_result[1], 'level' => $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR));
  423. }
  424. else {
  425. $server_url = variable_get('l10n_client_server', 'https://localize.drupal.org');
  426. $user_edit_url = url('user/' . $user->uid . '/edit', array('absolute' => TRUE));
  427. $message .= theme('l10n_client_message', array(
  428. 'message' => t('You could share your work with !l10n_server if you set your API key at !user_link.', array(
  429. '!l10n_server' => l($server_url, $server_url),
  430. '!user_link' => l($user_edit_url, 'user/' . $user->uid . '/edit'),
  431. )),
  432. 'level' => WATCHDOG_WARNING,
  433. ));
  434. }
  435. }
  436. }
  437. else {
  438. $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.')));
  439. }
  440. }
  441. else {
  442. $message = theme('l10n_client_message', array('message' => t('Not saved due to missing form values.')));
  443. }
  444. }
  445. else {
  446. $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.')));
  447. }
  448. drupal_json_output($message);
  449. exit;
  450. }
  451. /**
  452. * Imports translations when new modules or themes are installed.
  453. *
  454. * This function will start a batch to import translations for the added
  455. * components.
  456. *
  457. * @param array $components
  458. * An array of arrays of component (theme and/or module) names to import
  459. * translations for, indexed by type.
  460. */
  461. function l10n_update_system_update(array $components) {
  462. $components += array('module' => array(), 'theme' => array());
  463. $list = array_merge($components['module'], $components['theme']);
  464. // Skip running the translation imports if in the installer,
  465. // because it would break out of the installer flow. We have
  466. // built-in support for translation imports in the installer.
  467. if (!drupal_installation_attempted() && l10n_update_translatable_language_list() && variable_get('l10n_update_import_enabled', TRUE)) {
  468. module_load_include('compare.inc', 'l10n_update');
  469. // Update the list of translatable projects and start the import batch.
  470. // Only when new projects are added the update batch will be triggered. Not
  471. // each enabled module will introduce a new project. E.g. sub modules.
  472. $projects = array_keys(l10n_update_build_projects());
  473. if ($list = array_intersect($list, $projects)) {
  474. module_load_include('fetch.inc', 'l10n_update');
  475. // Get translation status of the projects, download and update
  476. // translations.
  477. $options = _l10n_update_default_update_options();
  478. $batch = l10n_update_batch_update_build($list, array(), $options);
  479. batch_set($batch);
  480. }
  481. }
  482. }
  483. /**
  484. * Delete translation history of modules and themes.
  485. *
  486. * Only the translation history is removed, not the source strings or
  487. * translations. This is not possible because strings are shared between
  488. * modules and we have no record of which string is used by which module.
  489. *
  490. * @param array $components
  491. * An array of arrays of component (theme and/or module) names to import
  492. * translations for, indexed by type.
  493. */
  494. function l10n_update_system_remove(array $components) {
  495. $components += array('module' => array(), 'theme' => array());
  496. $list = array_merge($components['module'], $components['theme']);
  497. if ($language_list = l10n_update_translatable_language_list()) {
  498. module_load_include('compare.inc', 'l10n_update');
  499. module_load_include('bulk.inc', 'l10n_update');
  500. // Only when projects are removed, the translation files and records will be
  501. // deleted. Not each disabled module will remove a project. E.g. sub
  502. // modules.
  503. $projects = array_keys(l10n_update_get_projects());
  504. if ($list = array_intersect($list, $projects)) {
  505. l10n_update_file_history_delete($list);
  506. // Remove translation files.
  507. l10n_update_delete_translation_files($list, array());
  508. // Remove translatable projects.
  509. // Followup issue https://www.drupal.org/node/1842362 to replace the
  510. // {l10n_update_project} table. Then change this to a function call.
  511. db_delete('l10n_update_project')
  512. ->condition('name', $list)
  513. ->execute();
  514. // Clear the translation status.
  515. l10n_update_status_delete_projects($list);
  516. }
  517. }
  518. }
  519. /**
  520. * Gets current translation status from the {l10n_update_file} table.
  521. *
  522. * @return array
  523. * Array of translation file objects.
  524. */
  525. function l10n_update_get_file_history() {
  526. $history = &drupal_static(__FUNCTION__, array());
  527. if (empty($history)) {
  528. // Get file history from the database.
  529. $result = db_query('SELECT project, language, filename, version, uri, timestamp, last_checked FROM {l10n_update_file}');
  530. foreach ($result as $file) {
  531. $file->langcode = $file->language;
  532. $file->type = $file->timestamp ? L10N_UPDATE_CURRENT : '';
  533. $history[$file->project][$file->langcode] = $file;
  534. }
  535. }
  536. return $history;
  537. }
  538. /**
  539. * Updates the {locale_file} table.
  540. *
  541. * @param object $file
  542. * Object representing the file just imported.
  543. *
  544. * @return int
  545. * FALSE on failure. Otherwise SAVED_NEW or SAVED_UPDATED.
  546. *
  547. * @see drupal_write_record()
  548. */
  549. function l10n_update_update_file_history($file) {
  550. // Update or write new record.
  551. if (db_query("SELECT project FROM {l10n_update_file} WHERE project = :project AND language = :langcode", array(':project' => $file->project, ':langcode' => $file->langcode))->fetchField()) {
  552. $update = array('project', 'language');
  553. }
  554. else {
  555. $update = array();
  556. }
  557. $file->language = $file->langcode;
  558. $result = drupal_write_record('l10n_update_file', $file, $update);
  559. // The file history has changed, flush the static cache now.
  560. // @todo Can we make this more fine grained?
  561. drupal_static_reset('l10n_update_get_file_history');
  562. return $result;
  563. }
  564. /**
  565. * Deletes the history of downloaded translations.
  566. *
  567. * @param array $projects
  568. * Project name(s) to be deleted from the file history. If both project(s) and
  569. * language code(s) are specified the conditions will be ANDed.
  570. * @param array $langcodes
  571. * Language code(s) to be deleted from the file history.
  572. */
  573. function l10n_update_file_history_delete($projects = array(), $langcodes = array()) {
  574. $query = db_delete('l10n_update_file');
  575. if (!empty($projects)) {
  576. $query->condition('project', $projects);
  577. }
  578. if (!empty($langcodes)) {
  579. $query->condition('language', $langcodes);
  580. }
  581. $query->execute();
  582. }
  583. /**
  584. * Gets the current translation status.
  585. *
  586. * @todo What is 'translation status'?
  587. */
  588. function l10n_update_get_status($projects = NULL, $langcodes = NULL) {
  589. module_load_include('translation.inc', 'l10n_update');
  590. $result = array();
  591. $cache = cache_get('l10n_update_status');
  592. $status = $cache ? $cache->data : array();
  593. $projects = $projects ? $projects : array_keys(l10n_update_get_projects());
  594. $langcodes = $langcodes ? $langcodes : array_keys(l10n_update_translatable_language_list());
  595. // Get the translation status of each project-language combination. If no
  596. // status was stored, a new translation source is created.
  597. foreach ($projects as $project) {
  598. foreach ($langcodes as $langcode) {
  599. if (isset($status[$project][$langcode])) {
  600. $result[$project][$langcode] = $status[$project][$langcode];
  601. }
  602. else {
  603. $sources = l10n_update_build_sources(array($project), array($langcode));
  604. if (isset($sources[$project][$langcode])) {
  605. $result[$project][$langcode] = $sources[$project][$langcode];
  606. }
  607. }
  608. }
  609. }
  610. return $result;
  611. }
  612. /**
  613. * Saves the status of translation sources in static cache.
  614. *
  615. * @param string $project
  616. * Machine readable project name.
  617. * @param string $langcode
  618. * Language code.
  619. * @param string $type
  620. * Type of data to be stored.
  621. * @param object $data
  622. * File object also containing timestamp when the translation is last updated.
  623. */
  624. function l10n_update_status_save($project, $langcode, $type, $data) {
  625. // Followup issue: https://www.drupal.org/node/1842362
  626. // Split status storage per module/language and expire individually. This will
  627. // improve performance for large sites.
  628. // Load the translation status or build it if not already available.
  629. module_load_include('translation.inc', 'l10n_update');
  630. $status = l10n_update_get_status();
  631. if (empty($status)) {
  632. $projects = l10n_update_get_projects(array($project));
  633. if (isset($projects[$project])) {
  634. $status[$project][$langcode] = l10n_update_source_build($projects[$project], $langcode);
  635. }
  636. }
  637. // Merge the new status data with the existing status.
  638. if (isset($status[$project][$langcode])) {
  639. switch ($type) {
  640. case L10N_UPDATE_REMOTE:
  641. case L10N_UPDATE_LOCAL:
  642. // Add the source data to the status array.
  643. $status[$project][$langcode]->files[$type] = $data;
  644. // Check if this translation is the most recent one. Set timestamp and
  645. // data type of the most recent translation source.
  646. if (isset($data->timestamp) && $data->timestamp) {
  647. $version_changed = isset($status[$project][$langcode]->files[L10N_UPDATE_CURRENT]->version) &&
  648. $status[$project][$langcode]->files[L10N_UPDATE_CURRENT]->version != $data->version;
  649. if (($data->timestamp > $status[$project][$langcode]->timestamp) || $version_changed) {
  650. $status[$project][$langcode]->timestamp = $data->timestamp;
  651. $status[$project][$langcode]->last_checked = REQUEST_TIME;
  652. $status[$project][$langcode]->version = $data->version;
  653. $status[$project][$langcode]->type = $type;
  654. }
  655. }
  656. break;
  657. case L10N_UPDATE_CURRENT:
  658. $data->last_checked = REQUEST_TIME;
  659. $status[$project][$langcode]->timestamp = $data->timestamp;
  660. $status[$project][$langcode]->last_checked = $data->last_checked;
  661. $status[$project][$langcode]->version = $data->version;
  662. $status[$project][$langcode]->type = $type;
  663. $status[$project][$langcode]->files[$type] = $data;
  664. l10n_update_update_file_history($data);
  665. break;
  666. }
  667. cache_set('l10n_update_status', $status);
  668. variable_set('l10n_update_last_check', REQUEST_TIME);
  669. }
  670. }
  671. /**
  672. * Delete language entries from the status cache.
  673. *
  674. * @param array $langcodes
  675. * Language code(s) to be deleted from the cache.
  676. */
  677. function l10n_update_status_delete_languages(array $langcodes) {
  678. if ($status = l10n_update_get_status()) {
  679. foreach ($status as $project => $languages) {
  680. foreach ($languages as $langcode => $source) {
  681. if (in_array($langcode, $langcodes)) {
  682. unset($status[$project][$langcode]);
  683. }
  684. }
  685. }
  686. cache_set('l10n_update_status', $status);
  687. }
  688. }
  689. /**
  690. * Delete project entries from the status cache.
  691. *
  692. * @param array $projects
  693. * Project name(s) to be deleted from the cache.
  694. */
  695. function l10n_update_status_delete_projects(array $projects) {
  696. $status = l10n_update_get_status();
  697. foreach ($status as $project => $languages) {
  698. if (in_array($project, $projects)) {
  699. unset($status[$project]);
  700. }
  701. }
  702. cache_set('l10n_update_status', $status);
  703. }
  704. /**
  705. * Returns list of translatable languages.
  706. *
  707. * @return array
  708. * Array of enabled languages keyed by language name. English is omitted.
  709. */
  710. function l10n_update_translatable_language_list() {
  711. $languages = locale_language_list('name');
  712. unset($languages['en']);
  713. drupal_alter('l10n_update_languages', $languages);
  714. return $languages;
  715. }
  716. /**
  717. * Clear the translation status cache.
  718. */
  719. function l10n_update_clear_status() {
  720. cache_clear_all('l10n_update_status', 'cache');
  721. }
  722. /**
  723. * Checks whether remote translation sources are used.
  724. *
  725. * @return bool
  726. * Returns TRUE if remote translations sources should be taken into account
  727. * when checking or importing translation files, FALSE otherwise.
  728. */
  729. function l10n_update_use_remote_source() {
  730. return variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL) == L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL;
  731. }
  732. /**
  733. * Creates a .htaccess file in the translations directory if it is missing.
  734. *
  735. * @param string $directory
  736. * The translations directory to create the file in. Defaults to the directory
  737. * of the 'translations://' wrapper.
  738. */
  739. function l10n_update_ensure_htaccess($directory = '') {
  740. $directory = empty($directory) ? 'translations://' : $directory;
  741. file_create_htaccess($directory, FALSE);
  742. }