'Save string', 'page callback' => 'l10n_client_save_string', 'access callback' => 'l10n_client_access', 'type' => MENU_CALLBACK, ); // Helper pages to group all translated/untranslated strings. $items['locale'] = array( 'title' => 'Translate strings', 'page callback' => 'l10n_client_translate_page', 'access callback' => 'l10n_client_access', ); $items['locale/untranslated'] = array( 'title' => 'Untranslated', 'page arguments' => array('untranslated'), 'access callback' => 'l10n_client_access', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['locale/translated'] = array( 'title' => 'Translated', 'page arguments' => array('translated'), 'access callback' => 'l10n_client_access', 'type' => MENU_LOCAL_TASK, 'weight' => 10, ); // Direct copy of the Configure tab from locale module to // make space for the "Localization sharing" tab below. $items['admin/settings/language/configure/language'] = array( 'title' => 'Language negotiation', 'page callback' => 'locale_inc_callback', 'page arguments' => array('drupal_get_form', 'locale_languages_configure_form'), 'access arguments' => array('administer languages'), 'weight' => -10, 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/language/configure/l10n_client'] = array( 'title' => 'Localization sharing', 'page callback' => 'drupal_get_form', 'page arguments' => array('l10n_client_settings_form'), 'access arguments' => array('administer languages'), 'weight' => 5, 'type' => MENU_LOCAL_TASK, ); return $items; } /** * Implementation of hook_perm(). */ function l10n_client_perm() { return array('use on-page translation', 'submit translations to localization server'); } /** * Implement hook_theme(). */ function l10n_client_theme($existing, $type, $theme, $path) { return array( 'l10n_client_message' => array( 'arguments' => array('message' => '', 'level' => WATCHDOG_ERROR), ), ); } /** * Implementation of hook_init(). */ function l10n_client_init() { global $conf, $language; if (l10n_client_access()) { // Turn off the short string cache *in this request*, so we will // have an accurate picture of strings used to assemble the page. $conf['locale_cache_strings'] = 0; // Reset locale cache. If any hook_init() implementation was invoked before // this point, that would normally result in all strings loaded into memory. // That would go against our goal of displaying only strings used on the page // and would hang browsers. Drops any string used for the page before this point. locale(NULL, NULL, TRUE); drupal_add_css(drupal_get_path('module', 'l10n_client') .'/l10n_client.css', 'module'); // Add jquery cookie plugin -- this should actually belong in // jstools (but hasn't been updated for HEAD) drupal_add_js(drupal_get_path('module', 'l10n_client') .'/jquery.hotkeys.js', 'module'); drupal_add_js(drupal_get_path('module', 'l10n_client') .'/jquery.cookie.js', 'module'); drupal_add_js(drupal_get_path('module', 'l10n_client') .'/l10n_client.js', 'module'); // We use textareas to be able to edit long text, which need resizing. drupal_add_js('misc/textarea.js', 'module'); } } /** * Detects whether a user can access l10n_client. */ function l10n_client_access($account = NULL) { if (!isset($account)) { global $user; $account = $user; } return user_access('use on-page translation', $account) && empty($account->l10n_client_disabled); } /** * Menu callback. Translation pages. * * These pages just list strings so they can be added to the string list for * translation below the page. This can be considered a hack, since we could * just implement the same UI on the page, and do away with these artifical * listings, but the current UI works, so we just reuse it this way. * * This includes custom textgroup support that can be used manually or * by other modules. * * @param $display_translated * Boolean indicating whether translated or untranslated strings are displayed. * @param $textgroup * Internal name of textgroup to use. * @param $allow_translation * Boolean indicating whether translation of strings via the l10n_client UI is allowed. */ function l10n_client_translate_page($display_translated = FALSE, $textgroup = 'default', $allow_translation = TRUE) { global $language; $header = $table = array(); $output = ''; // Build query to look for strings. $sql = "SELECT s.source, t.translation, t.language FROM {locales_source} s "; if ($display_translated) { $header = array(t('Source string'), t('Translation')); $sql .= "INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.language = '%s' AND t.translation != '' "; } else { $header = array(t('Source string')); $sql .= "LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE (t.translation IS NULL OR t.translation = '') "; } if (!empty($textgroup)) { $sql .= "AND s.textgroup ='" . db_escape_string($textgroup) . "' "; } $sql .= 'ORDER BY s.source'; // For the 'default' textgroup and English language we don't allow translation. $allow_translation = (($textgroup == 'default') && ($language->language == 'en')) ? FALSE : $allow_translation; $result = pager_query($sql, L10N_CLIENT_STRINGS, 0, NULL, $language->language); while ($data = db_fetch_object($result)) { if ($display_translated) { $table[] = array(check_plain($data->source), check_plain($data->translation)); if ($allow_translation) { l10_client_add_string_to_page($data->source, $data->translation, $textgroup); } } else { $table[] = array(check_plain($data->source)); if ($allow_translation) { l10_client_add_string_to_page($data->source, TRUE, $textgroup); } } } if (!empty($table)) { $output .= ($pager = theme('pager', NULL, L10N_CLIENT_STRINGS)); $output .= theme('table', $header, $table); $output .= $pager; } else { $output .= t('No strings found to translate.'); } return $output; } /** * Implementation of hook_footer(). * * Output a form to the page and a list of strings used to build * the page in JSON form. */ function l10n_client_footer() { global $conf, $language; // Check permission and get all strings used on the page. if (l10n_client_access() && ($page_strings = _l10n_client_page_strings())) { // If we have strings for the page language, restructure the data. $l10n_strings = array(); foreach ($page_strings as $textgroup => $group_strings) { foreach ($group_strings as $string => $translation) { $l10n_strings[] = array($string, $translation, $textgroup); } } array_multisort($l10n_strings); // Include string selector on page. $string_list = _l10n_client_string_list($l10n_strings); // Include editing form on page. $l10n_form = drupal_get_form('l10n_client_form', $l10n_strings); // Include search form on page. $l10n_search = drupal_get_form('l10n_client_search_form'); // Generate HTML wrapper with strings data. $l10n_dom = _l10n_client_dom_strings($l10n_strings); // UI Labels $string_label = '

'. t('Page Text') .'

'; $source_label = '

'. t('Source') .'

'; $translation_label = '

'. t('Translation to %language', array('%language' => $language->native)) .'

'; $toggle_label = t('Translate Text'); $output = " $l10n_dom "; return $output; } } /** * Adds a string to the list onto the l10n_client UI on this page. * * @param $source * Source string or NULL if geting the list of strings specified. * @param $translation * Translation string. TRUE if untranslated. * @param $textgroup * Text group the string belongs to */ function l10_client_add_string_to_page($source = NULL, $translation = NULL, $textgroup = 'default') { static $strings = array(); if (isset($source)) { $strings[$textgroup][$source] = $translation; } else { return $strings; } } /** * Get the strings to translate for this page. * * These will be: * - The ones added through l10n_client_add_string_to_page() by this or other modules. * - The strings stored by the locale function (not for for this module's own pages). */ function _l10n_client_page_strings() { global $language; // Get the page strings stored by this or other modules. $strings = l10_client_add_string_to_page(); // If this is not the module's translation page, merge all strings used on the page. if (arg(0) != 'locale' && is_array($locale = locale()) && isset($locale[$language->language])) { $strings += array('default' => array()); $strings['default'] = array_merge($strings['default'], $locale[$language->language]); // Also select and add other strings for this path. Other users may have run // into these strings for the same page. This might be useful in some cases // but will not work reliably in all cases, since strings might have been // found on completely different paths first, or on a slightly different // path. $result = db_query("SELECT s.source, t.translation, s.textgroup FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.location = '%s'", $language->language, request_uri()); while ($data = db_fetch_object($result)) { if (!array_key_exists($data->source, $strings[$data->textgroup])) { $strings[$data->textgroup][$data->source] = (empty($data->translation) ? TRUE : $data->translation); } } } return $strings; } /** * Helper function for the string list DOM tree */ function _l10n_client_dom_strings($strings) { $output = ''; foreach ($strings as $values) { $source = $values[0] === TRUE ? '' : htmlspecialchars($values[0], ENT_NOQUOTES, 'UTF-8'); $target = $values[1] === TRUE ? '' : htmlspecialchars($values[1], ENT_NOQUOTES, 'UTF-8'); $textgroup = $values[2]; $output .= "
$source$target$textgroup
"; } return "
$output
"; } /** * String selection has been moved to a jquery-based list. * Todo: make this a themeable function. */ function _l10n_client_string_list($strings) { // Build a list of short string excerpts for a selectable list. foreach ($strings as $values) { // Add a class to help identify translated strings if ($values[1] === TRUE) { $str_class = 'untranslated'; } else { $str_class = 'translated'; } // TRUE means we don't have translation, so we use the original string, // so we always have the string displayed on the page in the dropdown. $original = $values[1] === TRUE ? $values[0] : $values[1]; // Remove HTML tags for display. $string = strip_tags($original); if (empty($string)) { // Edge case where the whole string was HTML tags. For the // user to be able to select anything, we need to show part // of the HTML tags. Truncate first, so we do not truncate in // the middle of an already escaped HTML tag, thus possibly // breaking the page. $string = htmlspecialchars(truncate_utf8($original, 78, TRUE, TRUE), ENT_NOQUOTES, 'UTF-8'); } else { // Truncate and add ellipsis if too long. $string = truncate_utf8($string, 78, TRUE, TRUE); } $select_list[] = "
  • $string
  • "; } $output = implode("\n", $select_list); return ""; } /** * String editing form. Source & selection moved to UI components outside the form. * Backed with jquery magic on the client. * * @todo * This form has nothing to do with different plural versions yet. */ function l10n_client_form($form_id, $strings) { global $language; // Selector and editing form. $form = array(); $form['#action'] = url('l10n_client/save'); $form['target'] = array( '#type' => 'textarea', '#resizable' => false, '#rows' => 6, '#attributes' => array('class' => 'translation-target'), ); $form['save'] = array( '#value' => t('Save translation'), '#type' => 'submit', ); $form['textgroup'] = array( '#type' => 'hidden', '#value' => 'default', '#attributes' => array('class' => 'source-textgroup'), ); $form['copy'] = array( '#type' => 'button', '#id' => 'l10n-client-edit-copy', '#attributes' => array('class' => 'form-submit edit-copy'), '#value' => t('Copy source'), ); $form['clear'] = array( '#type' => 'button', '#id' => 'l10n-client-edit-clear', '#attributes' => array('class' => 'form-submit edit-clear'), '#value' => t('Clear'), ); return $form; } /** * Search form for string list */ function l10n_client_search_form() { global $language; // Selector and editing form. $form = array(); $form['search'] = array( '#type' => 'textfield', '#attributes' => array('class' => 'string-search'), ); $form['clear-button'] = array( '#type' => 'button', '#id' => 'l10n-client-search-filter-clear', '#attributes' => array('class' => 'form-submit'), '#value' => t('X'), ); return $form; } /** * Menu callback. Saves a string translation coming as POST data. */ function l10n_client_save_string() { global $user, $language; if (l10n_client_access()) { if (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['textgroup']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) { // Ensure we have this source string before we attempt to save it. $lid = db_result(db_query("SELECT lid FROM {locales_source} WHERE source = '%s' AND textgroup = '%s'", $_POST['source'], $_POST['textgroup'])); if (!empty($lid)) { include_once 'includes/locale.inc'; $report = array('skips' => 0, 'additions' => 0, 'updates' => 0, 'deletes' => 0); _locale_import_one_string_db($report, $language->language, $_POST['source'], $_POST['target'], $_POST['textgroup'], NULL, LOCALE_IMPORT_OVERWRITE); cache_clear_all('locale:', 'cache', TRUE); _locale_invalidate_js($language->language); if (!empty($report['skips'])) { $message = theme('l10n_client_message', t('Not saved locally due to invalid HTML content.')); } elseif (!empty($report['additions']) || !empty($report['updates'])) { $message = theme('l10n_client_message', t('Translation saved locally.'), WATCHDOG_INFO); } elseif (!empty($report['deletes'])) { $message = theme('l10n_client_message', t('Translation successfuly removed locally.'), WATCHDOG_INFO); } else { $message = theme('l10n_client_message', t('Unknown error while saving translation locally.')); } // Submit to remote server if enabled. if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server') && ($_POST['textgroup'] == 'default')) { if (!empty($user->l10n_client_key)) { $remote_result = l10n_client_submit_translation($language->language, $_POST['source'], $_POST['target'], $user->l10n_client_key, l10n_client_user_token($user)); $message .= theme('l10n_client_message', $remote_result[1], $remote_result[0] ? WATCHDOG_INFO : WATCHDOG_ERROR); } else { $server_url = variable_get('l10n_client_server', 'http://localize.drupal.org'); $user_edit_url = url('user/'. $user->uid .'/edit', array('absolute' => TRUE)); $message .= theme('l10n_client_message', t('You could share your work with !l10n_server if you set your API key at !user_link.', array('!l10n_server' => l($server_url, $server_url), '!user_link' => l($user_edit_url, 'user/'. $user->uid .'/edit'))), WATCHDOG_WARNING); } } } else { $message = theme('l10n_client_message', t('Not saved due to source string missing.')); } } else { $message = theme('l10n_client_message', t('Not saved due to missing form values.')); } } else { $message = theme('l10n_client_message', t('Not saved due to insufficient permissions.')); } drupal_json(array('message' => $message)); exit; } /** * Theme function to wrap l10n_client messages in proper markup. */ function theme_l10n_client_message($message, $level) { switch ($level) { case WATCHDOG_INFO: return '
    '. $message .'
    '; break; case WATCHDOG_WARNING: return '
    '. $message .'
    '; break; case WATCHDOG_ERROR: return '
    '. $message .'
    '; break; } } // ----------------------------------------------------------------------------- /** * Settings form for l10n_client. * * Enable users to set up a central server to share translations with. */ function l10n_client_settings_form() { $form = array(); $form['l10n_client_use_server'] = array( '#title' => t('Enable sharing translations with server'), '#type' => 'checkbox', '#default_value' => variable_get('l10n_client_use_server', FALSE), ); $form['l10n_client_server'] = array( '#title' => t('Address of localization server to use'), '#type' => 'textfield', '#description' => t('Each translation submission will also be submitted to this server. We suggest you enter http://localize.drupal.org to share with the greater Drupal community. Make sure you set up an API-key in the user account settings for each user that will participate in the translations.', array('@localize' => 'http://localize.drupal.org')), '#default_value' => variable_get('l10n_client_server', 'http://localize.drupal.org'), ); return system_settings_form($form); } /** * Validation to make sure the provided server can handle our submissions. * * Make sure it supports the exact version of the API we will try to use. */ function l10n_client_settings_form_validate($form, &$form_state) { if ($form_state['values']['l10n_client_use_server']) { if (!empty($form_state['values']['l10n_client_server'])) { // Try to invoke the remote string submission with a test request. $response = xmlrpc($form_state['values']['l10n_client_server'] .'/xmlrpc.php', 'l10n.server.test', '2.0'); if ($response && !empty($response['name']) && !empty($response['version'])) { if (empty($response['supported']) || !$response['supported']) { form_set_error('l10n_client_server', t('The given server could not handle the v2.0 remote submission API.')); } else { drupal_set_message(t('Verified that the specified server can handle remote string submissions. Supported languages: %languages.', array('%languages' => $response['languages']))); } } else { form_set_error('l10n_client_server', t('Invalid localization server address specified. Make sure you specified the right server address.')); } } else { form_set_error('l10n_client_server', t('You should provide a server address, such as http://localize.drupal.org')); } } } /** * Implementation of hook_user(). * * Set up API key for localization server. */ function l10n_client_user($type, &$edit, &$account, $category = NULL) { if ($type == 'form' && $category == 'account') { $items = $form = array(); if (variable_get('l10n_client_use_server', FALSE) && user_access('submit translations to localization server', $account)) { // Build link to retrieve user key. $server_link = variable_get('l10n_client_server', 'http://localize.drupal.org') .'?q=translate/remote/userkey/'. l10n_client_user_token($account); $items['l10n_client_key'] = array( '#type' => 'textfield', '#title' => t('Your Localization Server API key'), '#default_value' => !empty($account->l10n_client_key) ? $account->l10n_client_key : '', '#description' => t('This is a unique key that will allow you to send translations to the remote server. To get your API key go to !server-link.', array('!server-link' => l($server_link, $server_link))), ); } if (user_access('use on-page translation', $account)) { // Add an item to let the user disable the on-page tool. $items['l10n_client_disabled'] = array( '#type' => 'checkbox', '#title' => t('Hide on-page translation from you'), '#default_value' => !empty($account->l10n_client_disabled), ); } if (!empty($items)) { // Add items in a fieldset wrapper if any items available. $form = array('l10n_client' => array( '#type' => 'fieldset', '#title' => t('Localization client'), '#weight' => 1, 'items' => $items, ), ); } return $form; } elseif ($type == 'submit' && $category == 'account' && isset($edit['l10n_client_key'])) { $edit['l10n_client_key'] = trim($edit['l10n_client_key']); } } /** * Get user based semi unique token. This will ensure user keys are unique for each client. */ function l10n_client_user_token($account = NULL) { global $user; $account = isset($account) ? $account : $user; return md5('l10n_client'. $account->uid . drupal_get_private_key()); } /** * Submit translation to the server. */ function l10n_client_submit_translation($langcode, $source, $translation, $user_key, $user_token) { $server_uid = current(split(':', $user_key)); $signature = md5($user_key . $langcode . $source . $translation . $user_token); $server_url = variable_get('l10n_client_server', 'http://localize.drupal.org'); $response = xmlrpc( $server_url .'/xmlrpc.php', 'l10n.submit.translation', $langcode, $source, $translation, (int)$server_uid, $user_token, $signature ); if (!empty($response) && isset($response['status'])) { if ($response['status']) { $message = t('Translation sent and accepted by @server.', array('@server' => $server_url)); watchdog('l10n_client', 'Translation sent and accepted by @server.', array('@server' => $server_url)); } else { $message = t('Translation rejected by @server. Reason: %reason', array('%reason' => $response['reason'], '@server' => $server_url)); watchdog('l10n_client', 'Translation rejected by @server. Reason: %reason', array('%reason' => $response['reason'], '@server' => $server_url), WATCHDOG_WARNING); } return array($response['status'], $message); } else { $message = t('The connection with @server failed with the following error: %error_code: %error_message.', array('%error_code' => xmlrpc_errno(), '%error_message' => xmlrpc_error_msg(), '@server' => $server_url)); watchdog('l10n_client', 'The connection with @server failed with the following error: %error_code: %error_message.', array('%error_code' => xmlrpc_errno(), '%error_message' => xmlrpc_error_msg(), '@server' => $server_url), WATCHDOG_ERROR); return array(FALSE, $message); } }