l10n_update.admin.inc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <?php
  2. /**
  3. * @file
  4. * Admin settings and update page.
  5. */
  6. /**
  7. * Page callback: Checks for translation updates and displays the status.
  8. *
  9. * Manually checks the translation status without the use of cron.
  10. */
  11. function l10n_update_manual_status() {
  12. module_load_include('compare.inc', 'l10n_update');
  13. // Check the translation status of all translatable projects in all languages.
  14. // First we clear the cached list of projects. Although not strictly
  15. // necessary, this is helpful in case the project list is out of sync.
  16. l10n_update_flush_projects();
  17. l10n_update_check_projects();
  18. // Execute a batch if required. A batch is only used when remote files
  19. // are checked.
  20. if (batch_get()) {
  21. batch_process('admin/config/regional/translate/update');
  22. }
  23. drupal_goto('admin/config/regional/translate/update');
  24. }
  25. /**
  26. * Page callback: Translation status page.
  27. */
  28. function l10n_update_status_form() {
  29. module_load_include('compare.inc', 'l10n_update');
  30. $updates = $options = array();
  31. $languages_update = $languages_not_found = array();
  32. $projects_update = array();
  33. // @todo Calling l10n_update_build_projects() is an expensive way to
  34. // get a module name. In follow-up issue https://www.drupal.org/node/1842362
  35. // the project name will be stored to display use, like here.
  36. $project_data = l10n_update_build_projects();
  37. $languages = l10n_update_translatable_language_list();
  38. $status = l10n_update_get_status();
  39. // Prepare information about projects which have available translation
  40. // updates.
  41. if ($languages && $status) {
  42. foreach ($status as $project) {
  43. foreach ($project as $langcode => $project_info) {
  44. if (isset($project_data[$project_info->name])) {
  45. // No translation file found for this project-language combination.
  46. if (empty($project_info->type)) {
  47. $updates[$langcode]['not_found'][] = array(
  48. 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $project_data[$project_info->name]->info['name'],
  49. 'version' => $project_info->version,
  50. 'info' => _l10n_update_status_debug_info($project_info),
  51. );
  52. $languages_not_found[$langcode] = $langcode;
  53. }
  54. // Translation update found for this project-language combination.
  55. elseif ($project_info->type == L10N_UPDATE_LOCAL || $project_info->type == L10N_UPDATE_REMOTE) {
  56. $local = isset($project_info->files[L10N_UPDATE_LOCAL]) ? $project_info->files[L10N_UPDATE_LOCAL] : NULL;
  57. $remote = isset($project_info->files[L10N_UPDATE_REMOTE]) ? $project_info->files[L10N_UPDATE_REMOTE] : NULL;
  58. $recent = _l10n_update_source_compare($local, $remote) == L10N_UPDATE_SOURCE_COMPARE_LT ? $remote : $local;
  59. $name = isset($project_data[$project_info->name]->info['name']) ? $project_data[$project_info->name]->info['name'] : '';
  60. $updates[$langcode]['updates'][] = array(
  61. 'name' => $project_info->name == 'drupal' ? t('Drupal core') : $name,
  62. 'version' => $project_info->version,
  63. 'timestamp' => $recent->timestamp,
  64. );
  65. $languages_update[$langcode] = $langcode;
  66. $projects_update[$project_info->name] = $project_info->name;
  67. }
  68. }
  69. }
  70. }
  71. $languages_not_found = array_diff($languages_not_found, $languages_update);
  72. // Build data options for the select table.
  73. foreach ($updates as $langcode => $update) {
  74. $title = check_plain($languages[$langcode]);
  75. $l10n_update_update_info = array('#theme' => 'l10n_update_update_info');
  76. foreach (array('updates', 'not_found') as $update_status) {
  77. if (isset($update[$update_status])) {
  78. $l10n_update_update_info['#' . $update_status] = $update[$update_status];
  79. }
  80. }
  81. $options[$langcode] = array(
  82. 'title' => array(
  83. 'class' => array('label'),
  84. 'data' => array(
  85. '#title' => $title,
  86. '#markup' => $title,
  87. ),
  88. ),
  89. 'status' => array(
  90. 'class' => array(
  91. 'description',
  92. 'expand',
  93. 'priority-low',
  94. ),
  95. 'data' => drupal_render($l10n_update_update_info),
  96. ),
  97. );
  98. }
  99. // Sort the table data on language name.
  100. uasort($options, function ($a, $b) {
  101. return strcasecmp($a['title']['data']['#title'], $b['title']['data']['#title']);
  102. });
  103. }
  104. $last_checked = variable_get('l10n_update_last_check');
  105. $form['last_checked'] = array(
  106. '#theme' => 'l10n_update_last_check',
  107. '#last' => $last_checked,
  108. );
  109. $header = array(
  110. 'title' => array(
  111. 'data' => t('Language'),
  112. 'class' => array('title'),
  113. ),
  114. 'status' => array(
  115. 'data' => t('Status'),
  116. 'class' => array('status', 'priority-low'),
  117. ),
  118. );
  119. if (!$languages) {
  120. $empty = t('No translatable languages available. <a href="@add_language">Add a language</a> first.', array('@add_language' => url('admin/config/regional/language')));
  121. }
  122. elseif (empty($options)) {
  123. $empty = t('All translations up to date.');
  124. }
  125. else {
  126. $empty = t('No translation status available. <a href="@check">Check manually</a>.', array('@check' => url('admin/config/regional/translate/check')));
  127. }
  128. // The projects which require an update. Used by the _submit callback.
  129. $form['projects_update'] = array(
  130. '#type' => 'value',
  131. '#value' => $projects_update,
  132. );
  133. $form['langcodes'] = array(
  134. '#type' => 'tableselect',
  135. '#header' => $header,
  136. '#options' => $options,
  137. '#default_value' => $languages_update,
  138. '#empty' => $empty,
  139. '#js_select' => TRUE,
  140. '#multiple' => TRUE,
  141. '#required' => TRUE,
  142. '#not_found' => $languages_not_found,
  143. '#after_build' => array('l10n_update_language_table'),
  144. '#attributes' => array(),
  145. );
  146. $form['#attached'] = array(
  147. 'js' => array(
  148. drupal_get_path('module', 'l10n_update') . '/js/l10n_update.admin.js',
  149. ),
  150. 'css' => array(
  151. drupal_get_path('module', 'l10n_update') . '/css/l10n_update.admin.css',
  152. ),
  153. );
  154. if ($languages_update) {
  155. $form['actions'] = array(
  156. '#type' => 'actions',
  157. 'submit' => array(
  158. '#type' => 'submit',
  159. '#value' => t('Update translations'),
  160. ),
  161. '#attributes' => array(),
  162. );
  163. }
  164. return $form;
  165. }
  166. /**
  167. * Form validation handler for locale_translation_status_form().
  168. */
  169. function l10n_update_status_form_validate($form, &$form_state) {
  170. // Check if a language has been selected. 'tableselect' doesn't.
  171. if (!array_filter($form_state['values']['langcodes'])) {
  172. form_set_error('', t('Select a language to update.'));
  173. }
  174. }
  175. /**
  176. * Form submission handler for locale_translation_status_form().
  177. */
  178. function l10n_update_status_form_submit($form, $form_state) {
  179. module_load_include('fetch.inc', 'l10n_update');
  180. $langcodes = array_filter($form_state['values']['langcodes']);
  181. $projects = array_filter($form_state['values']['projects_update']);
  182. // Set the translation import options. This determines if existing
  183. // translations will be overwritten by imported strings.
  184. $options = _l10n_update_default_update_options();
  185. // If the status was updated recently we can immediately start fetching the
  186. // translation updates. If the status is expired we clear it an run a batch to
  187. // update the status and then fetch the translation updates.
  188. $last_checked = variable_get('l10n_update_last_check');
  189. if ($last_checked < REQUEST_TIME - L10N_UPDATE_STATUS_TTL) {
  190. l10n_update_clear_status();
  191. $batch = l10n_update_batch_update_build(array(), $langcodes, $options);
  192. batch_set($batch);
  193. }
  194. else {
  195. $batch = l10n_update_batch_fetch_build($projects, $langcodes, $options);
  196. batch_set($batch);
  197. }
  198. }
  199. /**
  200. * Page callback: Settings form.
  201. */
  202. function l10n_update_admin_settings_form($form, &$form_state) {
  203. $form['l10n_update_check_frequency'] = array(
  204. '#type' => 'radios',
  205. '#title' => t('Check for updates'),
  206. '#default_value' => variable_get('l10n_update_check_frequency', '0'),
  207. '#options' => array(
  208. '0' => t('Never (manually)'),
  209. '7' => t('Weekly'),
  210. '30' => t('Monthly'),
  211. ),
  212. '#description' => t('Select how frequently you want to check for new interface translations for your currently installed modules and themes. <a href="@url">Check updates now</a>.', array('@url' => url('admin/config/regional/translate/check'))),
  213. );
  214. $form['l10n_update_check_disabled'] = array(
  215. '#type' => 'checkbox',
  216. '#title' => t('Check for updates of disabled modules and themes'),
  217. '#default_value' => variable_get('l10n_update_check_disabled', FALSE),
  218. );
  219. $form['l10n_update_check_mode'] = array(
  220. '#type' => 'radios',
  221. '#title' => t('Translation source'),
  222. '#default_value' => variable_get('l10n_update_check_mode', L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL),
  223. '#options' => array(
  224. L10N_UPDATE_USE_SOURCE_REMOTE_AND_LOCAL => t('Drupal translation server and local files'),
  225. L10N_UPDATE_USE_SOURCE_LOCAL => t('Local files only'),
  226. ),
  227. '#description' => t('The source of translation files for automatic interface translation.'),
  228. );
  229. $form['l10n_update_download_store'] = array(
  230. '#title' => t('Translations directory'),
  231. '#type' => 'textfield',
  232. '#default_value' => variable_get('l10n_update_download_store', L10N_UPDATE_DEFAULT_TRANSLATION_PATH),
  233. '#required' => TRUE,
  234. '#description' => t('A path relative to the Drupal installation directory where translation files will be stored, e.g. sites/all/translations. Saved translation files can be reused by other installations.'),
  235. );
  236. $form['l10n_update_import_mode'] = array(
  237. '#type' => 'radios',
  238. '#title' => t('Import behaviour'),
  239. '#default_value' => variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP),
  240. '#options' => array(
  241. LOCALE_IMPORT_KEEP => t("Don't overwrite existing translations."),
  242. L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => t('Only overwrite imported translations, customized translations are kept.'),
  243. LOCALE_IMPORT_OVERWRITE => t('Overwrite existing translations.'),
  244. ),
  245. '#description' => t('How to treat existing translations when automatically updating the interface translations.'),
  246. );
  247. $form['disable_update'] = array(
  248. '#type' => 'fieldset',
  249. '#title' => t('Disable update'),
  250. '#collapible' => FALSE,
  251. '#collapsed' => FALSE,
  252. );
  253. $form['disable_update']['disabled_projects'] = array(
  254. '#type' => 'textarea',
  255. '#title' => t('Projects'),
  256. '#default_value' => implode(PHP_EOL, variable_get('l10n_update_disabled_projects', array())),
  257. '#description' => t("These modules, themes or profiles will not receive interface translation updates. Specify them by there machine name, enter one name per line. The '*' character is a wildcard. Use for example feature_* for every feature."),
  258. );
  259. $languages = locale_language_list('name');
  260. unset($languages['en']);
  261. $form['disable_update']['l10n_update_disabled_languages'] = array(
  262. '#type' => 'checkboxes',
  263. '#title' => t('Languages'),
  264. '#options' => $languages,
  265. '#default_value' => variable_get('l10n_update_disabled_languages', array()),
  266. '#description' => t('The selected languages will not receive interface translation updates.'),
  267. );
  268. $form = system_settings_form($form);
  269. $form['#submit'][] = 'l10n_update_admin_settings_form_submit';
  270. return $form;
  271. }
  272. /**
  273. * Validation handler for translation update settings.
  274. */
  275. function l10n_update_admin_settings_form_validate($form, &$form_state) {
  276. // Check for existing translations directory and create one if required.
  277. // When using local sources, only check if the directory exists.
  278. $directory = $form_state['values']['l10n_update_download_store'];
  279. $directory = rtrim($directory, '/\\');
  280. if ($form_state['values']['l10n_update_check_mode'] == L10N_UPDATE_USE_SOURCE_LOCAL
  281. && is_dir($directory)) {
  282. return;
  283. }
  284. if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
  285. form_set_error('l10n_update_download_store', t('The directory %directory does not exist or is not writable.', array('%directory' => $directory)));
  286. watchdog('file system', 'The directory %directory does not exist or is not writable.', array('%directory' => $directory), WATCHDOG_ERROR);
  287. }
  288. }
  289. /**
  290. * Submit handler for translation update settings.
  291. */
  292. function l10n_update_admin_settings_form_submit($form, &$form_state) {
  293. // Invalidate the cached translation status when the configuration setting of
  294. // 'l10n_update_check_mode' or 'check_disabled' change.
  295. if ($form['l10n_update_check_mode']['#default_value'] != $form_state['values']['l10n_update_check_mode'] ||
  296. $form['l10n_update_check_disabled']['#default_value'] != $form_state['values']['l10n_update_check_disabled']) {
  297. l10n_update_clear_status();
  298. }
  299. // Convert the disabled projects input into an array value. The input value is
  300. // removed from the form_state values to prevent it being turned into a
  301. // variable.
  302. $input = $form_state['values']['disabled_projects'];
  303. unset($form_state['values']['disabled_projects']);
  304. $input = strtr($input, array("\r" => '', ' ' => ''));
  305. $values = array_filter(explode("\n", $input));
  306. variable_set('l10n_update_disabled_projects', $values);
  307. // Add .htaccess file to the translations directory.
  308. l10n_update_ensure_htaccess();
  309. }
  310. /**
  311. * Get array of import options.
  312. *
  313. * The import options of the Locale module are used but the UI text is altered
  314. * to suit the Localization update cases.
  315. *
  316. * @return array
  317. * Keyed array of import options.
  318. */
  319. function _l10n_update_admin_import_options() {
  320. return array(
  321. LOCALE_IMPORT_OVERWRITE => t('Translation updates replace existing ones, new ones are added'),
  322. L10N_UPDATE_OVERWRITE_NON_CUSTOMIZED => t('Edited translations are kept, only previously imported ones are overwritten and new translations are added'),
  323. LOCALE_IMPORT_KEEP => t('All existing translations are kept, only new translations are added.'),
  324. );
  325. }
  326. /**
  327. * Provides debug info for projects in case translation files are not found.
  328. *
  329. * Translations files are being fetched either from Drupal translation server
  330. * and local files or only from the local filesystem depending on the
  331. * "Translation source" setting at admin/config/regional/language/update.
  332. * This method will produce debug information including the respective path(s)
  333. * based on this setting.
  334. *
  335. * Translations for development versions are never fetched, so the debug info
  336. * for that is a fixed message.
  337. *
  338. * @param object $source
  339. * An object which is the project information of the source.
  340. *
  341. * @return string
  342. * The string which contains debug information.
  343. */
  344. function _l10n_update_status_debug_info($source) {
  345. $remote_path = isset($source->files['remote']->uri) ? $source->files['remote']->uri : '';
  346. $local_path = isset($source->files['local']->uri) ? $source->files['local']->uri : '';
  347. if (strpos($source->version, 'dev') !== FALSE) {
  348. return t('No translation files are provided for development releases.');
  349. }
  350. if (l10n_update_use_remote_source() && $remote_path && $local_path) {
  351. return t('File not found at %remote_path nor at %local_path', array(
  352. '%remote_path' => $remote_path,
  353. '%local_path' => $local_path,
  354. ));
  355. }
  356. elseif ($local_path) {
  357. return t('File not found at %local_path', array('%local_path' => $local_path));
  358. }
  359. return t('Translation file location could not be determined.');
  360. }
  361. /**
  362. * Form element callback: After build changes to the language update table.
  363. *
  364. * Adds labels to the languages and removes checkboxes from languages from which
  365. * translation files could not be found.
  366. */
  367. function l10n_update_language_table($form_element) {
  368. // Remove checkboxes of languages without updates.
  369. if ($form_element['#not_found']) {
  370. foreach ($form_element['#not_found'] as $langcode) {
  371. $form_element[$langcode] = array();
  372. }
  373. }
  374. return $form_element;
  375. }
  376. /**
  377. * Returns HTML for translation edit form.
  378. *
  379. * @param array $variables
  380. * An associative array containing:
  381. * - form: The form that contains the language information.
  382. *
  383. * @return string
  384. * HTML output.
  385. *
  386. * @see l10n_update_edit_form()
  387. *
  388. * @ingroup themeable
  389. */
  390. function theme_l10n_update_edit_form_strings(array $variables) {
  391. $output = '';
  392. $form = $variables['form'];
  393. $header = array(
  394. t('Source string'),
  395. t('Translation for @language', array('@language' => $form['#language'])),
  396. );
  397. $rows = array();
  398. foreach (element_children($form) as $lid) {
  399. $string = $form[$lid];
  400. if ($string['plural']['#value']) {
  401. $source = drupal_render($string['original_singular']) . '<br />' . drupal_render($string['original_plural']);
  402. }
  403. else {
  404. $source = drupal_render($string['original']);
  405. }
  406. $source .= empty($string['context']) ? '' : '<br /><small>' . t('In Context') . ':&nbsp;' . $string['context']['#value'] . '</small>';
  407. $rows[] = array(
  408. array('data' => $source),
  409. array('data' => $string['translations']),
  410. );
  411. }
  412. $table = array(
  413. '#theme' => 'table',
  414. '#header' => $header,
  415. '#rows' => $rows,
  416. '#empty' => t('No strings available.'),
  417. '#attributes' => array('class' => array('locale-translate-edit-table')),
  418. );
  419. $output .= drupal_render($table);
  420. $pager = array('#theme' => 'pager');
  421. $output .= drupal_render($pager);
  422. return $output;
  423. }
  424. /**
  425. * Prepares variables for translation status information templates.
  426. *
  427. * Translation status information is displayed per language.
  428. *
  429. * Default template: l10n_update-translation-update-info.tpl.php.
  430. *
  431. * @param array $variables
  432. * An associative array containing:
  433. * - updates: The projects which have updates.
  434. * - not_found: The projects which updates are not found.
  435. *
  436. * @see l10n_update_status_form()
  437. */
  438. function template_preprocess_l10n_update_update_info(array &$variables) {
  439. $details = array();
  440. $modules = array();
  441. // Default values.
  442. $variables['modules'] = array();
  443. $variables['module_list'] = '';
  444. $details['available_updates_list'] = array();
  445. // Build output for available updates.
  446. if (isset($variables['updates'])) {
  447. $releases = array();
  448. if ($variables['updates']) {
  449. foreach ($variables['updates'] as $update) {
  450. $modules[] = $update['name'];
  451. $releases[] = t('@module (@date)', array(
  452. '@module' => $update['name'],
  453. '@date' => format_date($update['timestamp'], 'html_date'),
  454. ));
  455. }
  456. $variables['modules'] = $modules;
  457. $variables['module_list'] = t('Updates for: @modules', array('@modules' => implode(', ', $modules)));
  458. }
  459. $details['available_updates_list'] = array(
  460. '#theme' => 'item_list',
  461. '#items' => $releases,
  462. );
  463. }
  464. // Build output for updates not found.
  465. if (isset($variables['not_found'])) {
  466. $releases = array();
  467. $variables['missing_updates_status'] = format_plural(count($variables['not_found']), 'Missing translations for one project', 'Missing translations for @count projects');
  468. if ($variables['not_found']) {
  469. foreach ($variables['not_found'] as $update) {
  470. $version = $update['version'] ? $update['version'] : t('no version');
  471. $releases[] = t('@module (@version).', array('@module' => $update['name'], '@version' => $version)) . ' ' . $update['info'];
  472. }
  473. }
  474. $details['missing_updates_list'] = array(
  475. '#theme' => 'item_list',
  476. '#items' => $releases,
  477. );
  478. // Prefix the missing updates list if there is an available updates lists
  479. // before it.
  480. if (!empty($details['missing_updates_list']['#items'])) {
  481. $details['missing_updates_list']['#prefix'] = t('Missing translations for:');
  482. }
  483. }
  484. $variables['details'] = $details;
  485. }
  486. /**
  487. * Prepares variables for most recent translation update templates.
  488. *
  489. * Displays the last time we checked for locale update data. In addition to
  490. * properly formatting the given timestamp, this function also provides a "Check
  491. * manually" link that refreshes the available update and redirects back to the
  492. * same page.
  493. *
  494. * Default template: l10n_update-translation-last-check.tpl.php.
  495. *
  496. * @param array $variables
  497. * An associative array containing:
  498. * - last: The timestamp when the site last checked for available updates.
  499. *
  500. * @see l10n_update_status_form()
  501. */
  502. function template_preprocess_l10n_update_last_check(array &$variables) {
  503. $last = $variables['last'];
  504. $variables['last_checked'] = $last ? t('Last checked: !time ago', array('!time' => format_interval(REQUEST_TIME - $last))) : t('Last checked: never');
  505. $variables['link'] = l(t('Check manually'), 'admin/config/regional/translate/check');
  506. }