Commit c6dd7bec authored by webchick's avatar webchick
Browse files

#52990 by Roger López: Vastly improve translation string search and editing interface.

parent 62b05eb6
......@@ -535,58 +535,135 @@ function locale_translate_overview_screen() {
* String search screen.
*/
function locale_translate_seek_screen() {
$output = _locale_translate_seek();
$output .= drupal_get_form('locale_translate_seek_form');
// Add CSS.
drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css', array('preprocess' => FALSE));
$output = drupal_get_form('locale_translation_filter_form');
$output .= _locale_translate_seek();
return $output;
}
/**
* @} End of "locale-translate-seek"
*/
/**
* User interface for the string search screen.
* List locale translation filters that can be applied.
*/
function locale_translate_seek_form() {
function locale_translation_filters() {
$filters = array();
// Get all languages, except English
$languages = locale_language_list('name', TRUE);
unset($languages['en']);
// Present edit form preserving previous user settings
$query = _locale_translate_seek_query();
$form = array();
$form['search'] = array('#type' => 'fieldset',
'#title' => t('Search'),
);
$form['search']['string'] = array('#type' => 'textfield',
'#title' => t('String contains'),
'#default_value' => @$query['string'],
'#description' => t('Leave blank to show all strings. The search is case sensitive.'),
$filters['string'] = array(
'title' => t('String contains'),
'description' => t('Leave blank to show all strings. The search is case sensitive.'),
);
$form['search']['language'] = array(
// Change type of form widget if more the 5 options will
// be present (2 of the options are added below).
'#type' => (count($languages) <= 3 ? 'radios' : 'select'),
'#title' => t('Language'),
'#default_value' => (!empty($query['language']) ? $query['language'] : 'all'),
'#options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages),
$filters['language'] = array(
'title' => t('Language'),
'options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages),
);
$form['search']['translation'] = array('#type' => 'radios',
'#title' => t('Search in'),
'#default_value' => (!empty($query['translation']) ? $query['translation'] : 'all'),
'#options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
$filters['translation'] = array(
'title' => t('Search in'),
'options' => array('all' => t('Both translated and untranslated strings'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')),
);
$groups = module_invoke_all('locale', 'groups');
$form['search']['group'] = array('#type' => 'radios',
'#title' => t('Limit search to'),
'#default_value' => (!empty($query['group']) ? $query['group'] : 'all'),
'#options' => array_merge(array('all' => t('All text groups')), $groups),
$filters['group'] = array(
'title' => t('Limit search to'),
'options' => array_merge(array('all' => t('All text groups')), $groups),
);
$form['search']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
$form['#redirect'] = FALSE;
return $filters;
}
/**
* Return form for locale translation filters.
*
* @ingroup forms
*/
function locale_translation_filter_form() {
$session = &$_SESSION['locale_translation_filter'];
$session = is_array($session) ? $session : array();
$filters = locale_translation_filters();
$form['filters'] = array(
'#type' => 'fieldset',
'#title' => t('Filter translatable strings'),
'#theme' => 'locale_translation_filters',
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
foreach ($filters as $key => $filter) {
// Special case for 'string' filter.
if ($key == 'string') {
$form['filters']['status']['string'] = array(
'#type' => 'textfield',
'#title' => $filter['title'],
'#description' => $filter['description'],
);
}
else {
$form['filters']['status'][$key] = array(
'#title' => $filter['title'],
'#type' => 'select',
'#multiple' => FALSE,
'#size' => 0,
'#options' => $filter['options'],
);
}
if (!empty($session[$key])) {
$form['filters']['status'][$key]['#default_value'] = $session[$key];
}
}
$form['filters']['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t('Filter'),
);
if (!empty($session)) {
$form['filters']['buttons']['reset'] = array(
'#type' => 'submit',
'#value' => t('Reset')
);
}
return $form;
}
/**
* @} End of "locale-translate-seek"
* Validate result from locale translation filter form.
*/
function locale_translation_filter_form_validate($form, &$form_state) {
if ($form_state['values']['op'] == t('Filter') && empty($form_state['values']['language']) && empty($form_state['values']['group'])) {
form_set_error('type', t('You must select something to filter by.'));
}
}
/**
* Process result from locale translation filter form.
*/
function locale_translation_filter_form_submit($form, &$form_state) {
$op = $form_state['values']['op'];
$filters = locale_translation_filters();
switch ($op) {
case t('Filter'):
foreach ($filters as $name => $filter) {
if (isset($form_state['values'][$name])) {
$_SESSION['locale_translation_filter'][$name] = $form_state['values'][$name];
}
}
break;
case t('Reset'):
$_SESSION['locale_translation_filter'] = array();
break;
}
$form_state['redirect'] = 'admin/build/translate/translate';
}
/**
* @defgroup locale-translate-import Translation import screen.
......@@ -781,7 +858,7 @@ function locale_translate_edit_form(&$form_state, $lid) {
$source = db_fetch_object(db_query('SELECT source, textgroup, location FROM {locales_source} WHERE lid = %d', $lid));
if (!$source) {
drupal_set_message(t('String not found.'), 'error');
drupal_goto('admin/build/translate/search');
drupal_goto('admin/build/translate/translate');
}
// Add original text to the top and some values for form altering.
......@@ -899,7 +976,7 @@ function locale_translate_edit_form_submit($form, &$form_state) {
_locale_invalidate_js();
cache_clear_all('locale:', 'cache', TRUE);
$form_state['redirect'] = 'admin/build/translate/search';
$form_state['redirect'] = 'admin/build/translate/translate';
return;
}
/**
......@@ -928,7 +1005,7 @@ function locale_translate_delete_page($lid) {
*/
function locale_translate_delete_form(&$form_state, $source) {
$form['lid'] = array('#type' => 'value', '#value' => $source->lid);
return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/build/translate/search', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel'));
return confirm_form($form, t('Are you sure you want to delete the string "%source"?', array('%source' => $source->source)), 'admin/build/translate/translate', t('Deleting the string will remove all translations of this string in all languages. This action cannot be undone.'), t('Delete'), t('Cancel'));
}
/**
......@@ -941,7 +1018,7 @@ function locale_translate_delete_form_submit($form, &$form_state) {
_locale_invalidate_js();
cache_clear_all('locale:', 'cache', TRUE);
drupal_set_message(t('The string has been removed.'));
$form_state['redirect'] = 'admin/build/translate/search';
$form_state['redirect'] = 'admin/build/translate/translate';
}
/**
* @} End of "locale-translate-delete"
......@@ -1988,89 +2065,96 @@ function _locale_translate_seek() {
$output = '';
// We have at least one criterion to match
if ($query = _locale_translate_seek_query()) {
$join = "SELECT s.source, s.location, s.lid, s.textgroup, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid ";
$arguments = array();
$limit_language = FALSE;
// Compute LIKE section
switch ($query['translation']) {
case 'translated':
$where = "WHERE (t.translation LIKE ?)";
$orderby = "ORDER BY t.translation";
$arguments[] = '%'. $query['string'] .'%';
break;
case 'untranslated':
$where = "WHERE (s.source LIKE ? AND t.translation IS NULL)";
$orderby = "ORDER BY s.source";
$arguments[] = '%'. $query['string'] .'%';
break;
case 'all' :
default:
$where = "WHERE (s.source LIKE ? OR t.translation LIKE ?)";
$orderby = '';
$arguments[] = '%'. $query['string'] .'%';
$arguments[] = '%'. $query['string'] .'%';
break;
}
$grouplimit = '';
if (!empty($query['group']) && $query['group'] != 'all') {
$grouplimit = " AND s.textgroup = ?";
$arguments[] = $query['group'];
}
switch ($query['language']) {
// Force search in source strings
case "en":
$sql = $join . " WHERE s.source LIKE ? $grouplimit ORDER BY s.source";
$arguments = array('%' . $query['string'] . '%'); // $where is not used, discard its arguments
if (!empty($grouplimit)) {
$arguments[] = $query['group'];
}
break;
// Search in all languages
case "all":
$sql = "$join $where $grouplimit $orderby";
break;
// Some different language
default:
$sql = "$join AND t.language = ? $where $grouplimit $orderby";
array_unshift($arguments, $query['language']);
// Don't show translation flags for other languages, we can't see them with this search.
$limit_language = $query['language'];
}
if (!($query = _locale_translate_seek_query())) {
$query = array(
'translation' => 'all',
'group' => 'all',
'language' => 'all',
'string' => '',
);
}
$result = pager_query($sql, 50, 0, NULL, $arguments);
$join = "SELECT s.source, s.location, s.lid, s.textgroup, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid ";
$arguments = array();
$groups = module_invoke_all('locale', 'groups');
$header = array(t('Text group'), t('String'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2'));
$arr = array();
while ($locale = db_fetch_object($result)) {
$arr[$locale->lid]['group'] = $groups[$locale->textgroup];
$arr[$locale->lid]['languages'][$locale->language] = $locale->translation;
$arr[$locale->lid]['location'] = $locale->location;
$arr[$locale->lid]['source'] = $locale->source;
}
$rows = array();
foreach ($arr as $lid => $value) {
$rows[] = array(
$value['group'],
array('data' => check_plain(truncate_utf8($value['source'], 150, FALSE, TRUE)) . '<br /><small>' . $value['location'] . '</small>'),
array('data' => _locale_translate_language_list($value['languages'], $limit_language), 'align' => 'center'),
array('data' => l(t('edit'), "admin/build/translate/edit/$lid"), 'class' => 'nowrap'),
array('data' => l(t('delete'), "admin/build/translate/delete/$lid"), 'class' => 'nowrap'),
);
}
$limit_language = FALSE;
// Compute LIKE section
switch ($query['translation']) {
case 'translated':
$where = "WHERE (t.translation LIKE ?)";
$orderby = "ORDER BY t.translation";
$arguments[] = '%'. $query['string'] .'%';
break;
case 'untranslated':
$where = "WHERE (s.source LIKE ? AND t.translation IS NULL)";
$orderby = "ORDER BY s.source";
$arguments[] = '%'. $query['string'] .'%';
break;
case 'all' :
default:
$where = "WHERE (s.source LIKE ? OR t.translation LIKE ?)";
$orderby = '';
$arguments[] = '%'. $query['string'] .'%';
$arguments[] = '%'. $query['string'] .'%';
break;
}
$grouplimit = '';
if (!empty($query['group']) && $query['group'] != 'all') {
$grouplimit = " AND s.textgroup = ?";
$arguments[] = $query['group'];
}
if (count($rows)) {
$output .= theme('table', $header, $rows);
if ($pager = theme('pager', NULL, 50)) {
$output .= $pager;
switch ($query['language']) {
// Force search in source strings
case "en":
$sql = $join . " WHERE s.source LIKE ? $grouplimit ORDER BY s.source";
$arguments = array('%' . $query['string'] . '%'); // $where is not used, discard its arguments
if (!empty($grouplimit)) {
$arguments[] = $query['group'];
}
break;
// Search in all languages
case "all":
$sql = "$join $where $grouplimit $orderby";
break;
// Some different language
default:
$sql = "$join AND t.language = ? $where $grouplimit $orderby";
array_unshift($arguments, $query['language']);
// Don't show translation flags for other languages, we can't see them with this search.
$limit_language = $query['language'];
}
$result = pager_query($sql, 50, 0, NULL, $arguments);
$groups = module_invoke_all('locale', 'groups');
$header = array(t('Text group'), t('String'), ($limit_language) ? t('Language') : t('Languages'), array('data' => t('Operations'), 'colspan' => '2'));
$arr = array();
while ($locale = db_fetch_object($result)) {
$arr[$locale->lid]['group'] = $groups[$locale->textgroup];
$arr[$locale->lid]['languages'][$locale->language] = $locale->translation;
$arr[$locale->lid]['location'] = $locale->location;
$arr[$locale->lid]['source'] = $locale->source;
}
$rows = array();
foreach ($arr as $lid => $value) {
$rows[] = array(
$value['group'],
array('data' => check_plain(truncate_utf8($value['source'], 150, FALSE, TRUE)) . '<br /><small>' . $value['location'] . '</small>'),
array('data' => _locale_translate_language_list($value['languages'], $limit_language), 'align' => 'center'),
array('data' => l(t('edit'), "admin/build/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => 'nowrap'),
array('data' => l(t('delete'), "admin/build/translate/delete/$lid", array('query' => drupal_get_destination())), 'class' => 'nowrap'),
);
}
if (count($rows)) {
$output .= theme('table', $header, $rows);
if ($pager = theme('pager', NULL, 50)) {
$output .= $pager;
}
else {
$output .= t('No strings found for your search.');
}
}
else {
$output .= t('No strings found for your search.');
}
return $output;
......@@ -2085,8 +2169,8 @@ function _locale_translate_seek_query() {
$query = array();
$fields = array('string', 'language', 'translation', 'group');
foreach ($fields as $field) {
if (isset($_REQUEST[$field])) {
$query[$field] = $_REQUEST[$field];
if (isset($_SESSION['locale_translation_filter'][$field])) {
$query[$field] = $_SESSION['locale_translation_filter'][$field];
}
}
}
......@@ -2260,7 +2344,7 @@ function _locale_rebuild_js($langcode = NULL) {
* List languages in search result table
*/
function _locale_translate_language_list($translation, $limit_language) {
// Add CSS
// Add CSS.
drupal_add_css(drupal_get_path('module', 'locale') . '/locale.css', array('preprocess' => FALSE));
$languages = language_list();
......
......@@ -4,3 +4,18 @@
font-style: normal;
text-decoration: line-through;
}
#edit-language-wrapper, #edit-translation-wrapper, #edit-group-wrapper {
float: left; /* LTR */
padding-right: .8em; /* LTR */
margin: 0.1em;
/**
* In Opera 9, DOM elements with the property of "overflow: auto"
* will partially hide its contents with unnecessary scrollbars when
* its immediate child is floated without an explicit width set.
*/
width: 15em;
}
#locale-translation-filter-form .form-item select.form-select {
width: 100%;
}
......@@ -32,7 +32,7 @@ function locale_help($path, $arg) {
case 'admin/settings/language':
$output = '<p>' . t("This page provides an overview of your site's enabled languages. If multiple languages are available and enabled, the text on your site interface may be translated, registered users may select their preferred language on the <em>My account</em> page, and site authors may indicate a specific language when creating posts. Languages will be displayed in the order you specify in places such as the language switcher block, or the language dropdown when creating or editing posts. The site's default language is used for anonymous visitors and for users who have not selected a preferred language.") . '</p>';
$output .= '<p>' . t('For each language available on the site, use the <em>edit</em> link to configure language details, including name, an optional language-specific path or domain, and whether the language is natively presented either left-to-right or right-to-left. These languages also appear in the <em>Language</em> selection when creating a post of a content type with multilingual support.') . '</p>';
$output .= '<p>' . t('Use the <a href="@add-language">add language page</a> to enable additional languages (and automatically import files from a translation package, if available), the <a href="@search">translate interface page</a> to locate strings for manual translation, or the <a href="@import">import page</a> to add translations from individual <em>.po</em> files. A number of contributed translation packages containing <em>.po</em> files are available on the <a href="@translations">Drupal.org translations page</a>.', array('@add-language' => url('admin/settings/language/add'), '@search' => url('admin/build/translate/search'), '@import' => url('admin/build/translate/import'), '@translations' => 'http://drupal.org/project/translations')) . '</p>';
$output .= '<p>' . t('Use the <a href="@add-language">add language page</a> to enable additional languages (and automatically import files from a translation package, if available), the <a href="@search">translate interface page</a> to locate strings for manual translation, or the <a href="@import">import page</a> to add translations from individual <em>.po</em> files. A number of contributed translation packages containing <em>.po</em> files are available on the <a href="@translations">Drupal.org translations page</a>.', array('@add-language' => url('admin/settings/language/add'), '@search' => url('admin/build/translate/translate'), '@import' => url('admin/build/translate/import'), '@translations' => 'http://drupal.org/project/translations')) . '</p>';
$output .= '<p>' . t('To rearrange languages, grab a drag-and-drop handle under the <em>English name</em> column and drag the item to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save configuration</em> button at the bottom of the page.') . '</p>';
return $output;
case 'admin/settings/language/add':
......@@ -55,7 +55,7 @@ function locale_help($path, $arg) {
return $output;
case 'admin/build/translate/export':
return '<p>' . t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') . '</p>';
case 'admin/build/translate/search':
case 'admin/build/translate/translate':
return '<p>' . t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to <a href="@export">export</a> strings for offline editing in a desktop Gettext translation editor.) Searches may be limited to strings found within a specific text group or in a specific language.', array('@export' => url('admin/build/translate/export'))) . '</p>';
case 'admin/build/block/configure':
if ($arg[4] == 'locale' && $arg[5] == 0) {
......@@ -124,8 +124,8 @@ function locale_menu() {
'weight' => 0,
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/build/translate/search'] = array(
'title' => 'Search',
$items['admin/build/translate/translate'] = array(
'title' => 'Translate',
'weight' => 10,
'type' => MENU_LOCAL_TASK,
'page callback' => 'locale_translate_seek_screen', // search results and form concatenated
......@@ -314,6 +314,9 @@ function locale_theme() {
'locale_languages_overview_form' => array(
'arguments' => array('form' => array()),
),
'locale_translation_filters' => array(
'arguments' => array('form' => array()),
),
);
}
......@@ -596,3 +599,17 @@ function locale_block_view($delta = '') {
return $block;
}
}
/**
* Theme locale translation filter selector.
*
* @ingroup themeable
*/
function theme_locale_translation_filters($form) {
$output = '';
foreach (element_children($form['status']) as $key) {
$output .= drupal_render($form['status'][$key]);
}
$output .= '<div id="locale-translation-buttons">' . drupal_render($form['buttons']) . '</div>';
return $output;
}
......@@ -37,7 +37,7 @@ class LocaleTestCase extends DrupalWebTestCase {
// Add language.
$this->drupalLogin($admin_user);
$edit = array (
$edit = array(
'langcode' => $langcode,
'name' => $name,
'native' => $native,
......@@ -65,7 +65,7 @@ class LocaleTestCase extends DrupalWebTestCase {
'translation' => 'all',
'group' => 'all',
);
$this->drupalPost('admin/build/translate/search', $search, t('Search'));
$this->drupalPost('admin/build/translate/translate', $search, t('Filter'));
// assertText seems to remove the input field where $name always could be
// found, so this is not a false assert. See how assertNoText succeeds
// later.
......@@ -75,7 +75,9 @@ class LocaleTestCase extends DrupalWebTestCase {
// reasonable.
$this->clickLink(t('edit'));
// We save the lid from the path.
$lid = preg_replace('/\D/', '', substr($this->getUrl(), strlen($base_url)));
$matches = array();
preg_match('!admin/build/translate/edit/(\d)+!', $this->getUrl(), $matches);
$lid = $matches[1];
// No t() here, it's surely not translated yet.
$this->assertText($name, 'name found on edit screen');
$edit = array (
......@@ -84,7 +86,7 @@ class LocaleTestCase extends DrupalWebTestCase {
$this->drupalPost(NULL, $edit, t('Save translations'));
$this->assertText(t('The string has been saved.'), 'The string has been saved.');
$this->assertTrue($name != $translation && t($name, array(), $langcode) == $translation, 't() works');
$this->drupalPost('admin/build/translate/search', $search, t('Search'));
$this->drupalPost('admin/build/translate/translate', $search, t('Filter'));
// The indicator should not be here.
$this->assertNoRaw($language_indicator, 'String is translated');
$this->drupalLogout();
......@@ -107,7 +109,7 @@ class LocaleTestCase extends DrupalWebTestCase {
$this->drupalLogin($translate_user);
$this->drupalPost('admin/build/translate/delete/' . $lid, array(), t('Delete'));
$this->assertText(t('The string has been removed.'), 'The string has been removed message.');
$this->drupalPost('admin/build/translate/search', $search, t('Search'));
$this->drupalPost('admin/build/translate/translate', $search, t('Filter'));
$this->assertNoText($name, 'Search now can not find the name');
}
......@@ -155,7 +157,7 @@ class LocaleTestCase extends DrupalWebTestCase {
'translation' => 'all',
'group' => 'all',
);
$this->drupalPost('admin/build/translate/search', $search, t('Search'));
$this->drupalPost('admin/build/translate/translate', $search, t('Filter'));
// Find the edit path
$content = $this->drupalGetContent();
$this->assertTrue(preg_match('@(admin/build/translate/edit/[0-9]+)@', $content, $matches), t('Found the edit path'));
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment