diff --git a/i18n.test b/i18n.test index ca01e6d..976ac69 100644 --- a/i18n.test +++ b/i18n.test @@ -10,9 +10,8 @@ class Drupali18nTestCase extends DrupalWebTestCase { protected $secondary_language; function setUpLanguages($admin_permissions = array()) { - // Setup users. - $this->admin_user = $this->drupalCreateUser(array_merge(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages'), $admin_permissions)); - $this->translator = $this->drupalCreateUser(array('translate interface')); + // Setup admin user. + $this->admin_user = $this->drupalCreateUser(array_merge(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages', 'translate interface'), $admin_permissions)); $this->drupalLogin($this->admin_user); @@ -43,8 +42,8 @@ class Drupali18nTestCase extends DrupalWebTestCase { ); $type = node_type_get_type($settings['type']); - - $this->translator = $this->drupalCreateUser(array( + // Create content editor with translation permissions. + $this->content_editor = $this->drupalCreateUser(array( 'create ' . $type->type . ' content', 'edit own ' . $type->type . ' content', 'translate content', diff --git a/i18n_block/i18n_block.test b/i18n_block/i18n_block.test index 5cee201..3fdd0d9 100644 --- a/i18n_block/i18n_block.test +++ b/i18n_block/i18n_block.test @@ -17,6 +17,7 @@ class i18nBlocksTestCase extends Drupali18nTestCase { function setUp() { parent::setUp('i18n_block'); parent::setUpLanguages(); + $this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings')); $format = filter_default_format(); variable_set('i18n_string_allowed_formats', array($format => $format)); @@ -25,7 +26,7 @@ class i18nBlocksTestCase extends Drupali18nTestCase { function testBlockTranslation() { - $block_translater = $this->drupalCreateUser(array('administer blocks', 'translate interface')); + $block_translater = $this->drupalCreateUser(array('administer blocks', 'translate interface', 'translate user-defined strings')); // Display Language switcher block $switcher = array('module' => 'locale', 'delta' => 'language', 'title' => t('Languages')); @@ -70,7 +71,7 @@ class i18nBlocksTestCase extends Drupali18nTestCase { $this->assertText(t('translated')); $this->clickLink(t('translate')); - debug($translations); + $this->assertFieldByName('strings[blocks:block:' . $box2['delta'] . ':title]', $translations['title']['es']); $this->assertFieldByName('strings[blocks:block:' . $box2['delta'] . ':body]', $translations['body']['es']); diff --git a/i18n_field/i18n_field.test b/i18n_field/i18n_field.test index 7db24dd..6fea159 100644 --- a/i18n_field/i18n_field.test +++ b/i18n_field/i18n_field.test @@ -17,8 +17,8 @@ class i18nFieldTestCase extends Drupali18nTestCase { function setUp() { parent::setUp('i18n_field', 'field_test'); - parent::setUpLanguages(array('translate interface', 'access field_test content', 'administer field_test content')); - + parent::setUpLanguages(array('access field_test content', 'administer field_test content')); + $this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings')); } /** diff --git a/i18n_menu/i18n_menu.test b/i18n_menu/i18n_menu.test index bfa865a..3f1592f 100644 --- a/i18n_menu/i18n_menu.test +++ b/i18n_menu/i18n_menu.test @@ -16,6 +16,7 @@ class i18nMenuTestCase extends Drupali18nTestCase { function setUp() { parent::setUp(array('i18n_menu', 'translation')); parent::setUpLanguages(array('administer menu')); + $this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings')); } function testMenuTranslateLocalize() { @@ -84,7 +85,7 @@ class i18nMenuTestCase extends Drupali18nTestCase { function testNodeMenuItems() { // Create menu and display it in a block. $menu = $this->createMenu(array( - 'i18n_mode' => I18N_MODE_MULTIPLE, + 'i18n_mode' => I18N_MODE_MULTIPLE, 'language' => LANGUAGE_NONE, 'menu_name' => 'test', )); diff --git a/i18n_select/i18n_select.test b/i18n_select/i18n_select.test index 0928483..b46fb98 100644 --- a/i18n_select/i18n_select.test +++ b/i18n_select/i18n_select.test @@ -40,7 +40,7 @@ class i18nSelectTestCase extends Drupali18nTestCase { $this->drupalPost(NULL, array(), t('Save settings')); // Create some content and check selection modes - $this->drupalLogin($this->translator); + $this->drupalLogin($this->content_editor); // variable_set('language_content_type_story', 1); $neutral = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1)); diff --git a/i18n_string/i18n_string.admin.inc b/i18n_string/i18n_string.admin.inc index bb5bb5a..6d2c290 100644 --- a/i18n_string/i18n_string.admin.inc +++ b/i18n_string/i18n_string.admin.inc @@ -12,7 +12,6 @@ include_once './includes/locale.inc'; * Form callback. Refresh textgroups. */ function i18n_string_admin_refresh_form() { - module_load_include('inc', 'i18n_string'); // Select textgroup/s. Just the ones that have a 'refresh callback' $groups = array(); foreach (i18n_string_group_info() as $name => $info) { @@ -63,7 +62,7 @@ function i18n_string_admin_refresh_form_submit($form, &$form_state) { */ function i18n_string_refresh_group($group, $delete = FALSE) { $result = FALSE; - + // Compile all strings for this group if ($strings = i18n_string_group_string_list($group)) { i18n_string_refresh_string_list($strings); @@ -179,7 +178,7 @@ function i18n_string_refresh_enabled_modules($modules) { foreach ($modules as $module) { if ($strings = i18n_string_module_string_list($module)) { $count += i18n_string_refresh_string_list($strings); - + } // Call module refresh if exists module_invoke($module, 'i18n_string_refresh', 'all'); @@ -287,7 +286,7 @@ function i18n_string_module_string_list($module) { if ($groups = module_invoke($module, 'i18n_string_info')) { foreach ($groups as $group) { $strings = i18n_string_array_merge($strings, i18n_string_group_string_list($group)); - } + } } else { $groups = array(); @@ -312,136 +311,3 @@ function i18n_string_module_string_list($module) { } return $strings; } - -/** - * User interface for string editing. - */ -function i18n_string_locale_translate_edit_form($form, &$form_state, $lid) { - // Fetch source string, if possible. - $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject(); - if (!$source) { - drupal_set_message(t('String not found.'), 'error'); - drupal_goto('admin/config/regional/translate/translate'); - } - - // Add original text to the top and some values for form altering. - $form['original'] = array( - '#type' => 'item', - '#title' => t('Original text'), - '#markup' => check_plain(wordwrap($source->source, 0)), - ); - if (!empty($source->context)) { - $form['context'] = array( - '#type' => 'item', - '#title' => t('Context'), - '#markup' => check_plain($source->context), - ); - } - $form['lid'] = array( - '#type' => 'value', - '#value' => $lid - ); - $form['textgroup'] = array( - '#type' => 'value', - '#value' => $source->textgroup, - ); - $form['location'] = array( - '#type' => 'value', - '#value' => $source->location - ); - - // Include default form controls with empty values for all languages. - // This ensures that the languages are always in the same order in forms. - $languages = language_list(); - - // We don't need the default language value, that value is in $source. - $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language(); - unset($languages[($omit)]); - $form['translations'] = array('#tree' => TRUE); - // Approximate the number of rows to use in the default textarea. - $rows = min(ceil(str_word_count($source->source) / 12), 10); - foreach ($languages as $langcode => $language) { - $form['translations'][$langcode] = array( - '#type' => 'textarea', - '#title' => t($language->name), - '#rows' => $rows, - '#default_value' => '', - ); - } - - // Fetch translations and fill in default values in the form. - $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit)); - foreach ($result as $translation) { - $form['translations'][$translation->language]['#default_value'] = $translation->translation; - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); - - // Restrict filter permissions and handle validation and submission for i18n strings - $context = db_select('i18n_string', 'i18ns') - ->fields('i18ns') - ->condition('lid', $form['lid']['#value']) - ->execute() - ->fetchObject(); - if ($source->textgroup != 'default' && $context) { - $form['i18n_string_context'] = array('#type' => 'value', '#value' => $context); - if ($context->format) { - $formats = filter_formats(); - $format = $formats[$context->format]; - $disabled = !filter_access($format); - if ($disabled) { - drupal_set_message(t('This string uses the %name text format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name)), 'warning'); - } - foreach (element_children($form['translations']) as $langcode) { - $form['translations'][$langcode]['#disabled'] = $disabled; - } - $form['translations']['format_help'] = array( - '#type' => 'markup', - '#markup' => '
' . t('Text format: @name', array('@name' => $format->name)) . '
' . theme('filter_tips', array('tips' => _filter_tips($context->format, FALSE))), - ); - $form['submit']['#disabled'] = $disabled; - } - } - return $form; -} - -/** - * Process string editing form validations. - * - * If it is an allowed format, skip default validation, the text will be filtered later - */ -function i18n_string_locale_translate_edit_form_validate($form, &$form_state) { - if (empty($form_state['values']['i18n_string_context']) || empty($form_state['values']['i18n_string_context']->format)) { - // If not text format use regular validation for all strings - $copy_state = $form_state; - $copy_state['values']['textgroup'] = 'default'; - module_load_include('admin.inc', 'locale'); - locale_translate_edit_form_validate($form, $copy_state); - } - elseif (!i18n_string_translate_access($form_state['values']['i18n_string_context'])) { - form_set_error('translations', t('You are not allowed to translate or edit texts with this text format.')); - } -} - -/** - * Process string editing form submissions. - * - * Mark translations as current. - */ -function i18n_string_locale_translate_edit_form_submit($form, &$form_state) { - // Invoke locale submission. - module_load_include('admin.inc', 'locale'); - locale_translate_edit_form_submit($form, $form_state); - $lid = $form_state['values']['lid']; - foreach ($form_state['values']['translations'] as $key => $value) { - if (!empty($value)) { - // An update has been made, so we assume the translation is now current. - db_update('locales_target') - ->fields(array('i18n_status' => I18N_STRING_STATUS_CURRENT)) - ->condition('lid', $lid) - ->condition('language', $key) - ->execute(); - } - } -} diff --git a/i18n_string/i18n_string.inc b/i18n_string/i18n_string.inc index 9fd3a90..7d71326 100644 --- a/i18n_string/i18n_string.inc +++ b/i18n_string/i18n_string.inc @@ -157,7 +157,7 @@ class i18n_string_object { * There's a hidden variable, 'i18n_string_debug', that when set to TRUE will display additional info */ public function format_translation($langcode, $options = array()) { - $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'sanitize default' => FALSE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug); + $options += array('langcode' => $langcode, 'sanitize' => TRUE, 'cache' => FALSE, 'debug' => $this->textgroup()->debug); if ($translation = $this->get_translation($langcode)) { $string = $translation; if (isset($options['filter'])) { @@ -167,15 +167,11 @@ class i18n_string_object { else { // Get default source string if no translation. $string = $this->get_string(); - $options['sanitize'] = $options['sanitize default']; + $options['sanitize'] = !empty($options['sanitize default']); } if (!empty($this->format)) { $options += array('format' => $this->format); } - elseif (empty($translation) && $options['sanitize']) { - // We are bout to display the source string without text format, force check_plain. - $string = check_plain($string); - } // Add debug information if enabled if ($options['debug']) { $info = array($langcode, $this->textgroup, $this->context); @@ -259,6 +255,19 @@ class i18n_string_object { public function remove($options = array()) { return $this->textgroup()->string_remove($this, $options); } + /** + * Check whether there is any problem for the user to translate a this string. + * + * @param $account + * Optional user account, defaults to current user. + * + * @return + * None if the user has access to translate the string. + * Error message if the user cannot translate that string. + */ + public function check_translate_access($account = NULL) { + return i18n_string_translate_check_string($this, $account); + } } /** @@ -441,7 +450,7 @@ class i18n_string_textgroup_default { /** * Remove string object. - * + * * @return * SAVED_DELETED | FALSE (If the operation failed because no source) */ @@ -749,9 +758,9 @@ class i18n_string_textgroup_default { * * @param $translations * Array of translation objects as loaded from the db. - * @param $langcode - * Language code, array of language codes or * to search all translations. - * + * @param $langcode + * Language code, array of language codes or * to search all translations. + * * @return array * Array of i18n string objects. */ @@ -1024,11 +1033,11 @@ class i18n_string_object_wrapper extends i18n_object_wrapper { /** * Get object strings for translation * - * This will return a simple array of string objects, indexed by full string name. - * - * @param $options - * Array with processing options. - * - 'empty', whether to return empty strings, defaults to FALSE. + * This will return a simple array of string objects, indexed by full string name. + * + * @param $options + * Array with processing options. + * - 'empty', whether to return empty strings, defaults to FALSE. */ public function get_strings($options = array()) { $options += array('empty' => FALSE); @@ -1064,7 +1073,7 @@ class i18n_string_object_wrapper extends i18n_object_wrapper { $this->properties = $this->build_properties(); // Call hook_i18n_string_list_TEXTGROUP_alter(), last chance for modules drupal_alter('i18n_string_list_' . $this->get_textgroup(), $this->properties, $this->type, $this->object); - + } return $this->properties; } @@ -1162,7 +1171,7 @@ class i18n_string_object_wrapper extends i18n_object_wrapper { * Translate access (localize strings) */ protected function localize_access() { - return user_access('translate interface'); + return user_access('translate interface') && user_access('translate user-defined strings'); } /** diff --git a/i18n_string/i18n_string.module b/i18n_string/i18n_string.module index 236951c..3e31360 100644 --- a/i18n_string/i18n_string.module +++ b/i18n_string/i18n_string.module @@ -149,7 +149,7 @@ function i18n_string_menu_alter(&$items) { 'page callback' => 'drupal_get_form', 'page arguments' => array('i18n_string_locale_translate_edit_form', 5), 'access arguments' => array('translate interface'), - 'file' => 'i18n_string.admin.inc', + 'file' => 'i18n_string.pages.inc', 'file path' => drupal_get_path('module', 'i18n_string'), ); } @@ -160,7 +160,7 @@ function i18n_string_menu_alter(&$items) { function i18n_string_hook_info() { $hooks['i18n_string_info'] = $hooks['i18n_string_list'] = - $hooks['i18n_string_refresh'] = + $hooks['i18n_string_refresh'] = $hooks['i18n_string_objects'] = array( 'group' => 'i18n', ); @@ -183,6 +183,24 @@ function i18n_string_locale($op = 'groups') { } /** + * Implements hook_permission(). + */ +function i18n_string_permission() { + return array( + 'translate user-defined strings' => array( + 'title' => t('Translate user-defined strings'), + 'description' => t('Translate user-defined strings that are created as part of content or configuration.'), + 'restrict access' => TRUE, + ), + 'translate admin strings' => array( + 'title' => t('Translate admin strings'), + 'description' => t('Translate administrative strings with a very permissive XSS/HTML filter that allows all HTML tags.'), + 'restrict access' => TRUE, + ), + ); +} + +/** * Implements hook_modules_enabled(). */ function i18n_string_modules_enabled($modules) { @@ -335,7 +353,8 @@ function i18n_string_allowed_format($format_id = NULL) { return TRUE; } else { - return in_array($format_id, variable_get('i18n_string_allowed_formats', array(filter_fallback_format())), TRUE); + // Check the format still exists an it is in the allowed formats list. + return filter_format_load($format_id) && in_array($format_id, variable_get('i18n_string_allowed_formats', array(filter_fallback_format())), TRUE); } } @@ -384,10 +403,42 @@ function i18n_string_element_mark(&$element) { /** * Get source string object. + * + * This returns the i18nstring object only when it has a source. + * + * @return i18n_string_object */ function i18n_string_get_source($name) { - list ($textgroup, $context) = i18n_string_context($name); - return i18n_string_textgroup($textgroup)->build_string($context)->get_source(); + return i18n_string_build($name)->get_source(); +} + +/** + * Get full string object. + * + * Builds string and loads the source if available. + * + * @return i18n_string_object + */ +function i18n_string_get_string($name, $default = NULL) { + $i18nstring = i18n_string_build($name, $default); + $i18nstring->get_source(); + return $i18nstring; +} + +/** + * Get full string object by lid. + */ +function i18n_string_get_by_lid($lid) { + $string = db_select('i18n_string', 's') + ->fields('s', array('textgroup', 'context')) + ->condition('lid', $lid) + ->execute() + ->fetchObject(); + if ($string) { + $i18nstring = i18n_string_textgroup($string->textgroup)->build_string($string->context); + $i18nstring->get_source(); + return $i18nstring; + } } /** @@ -476,9 +527,7 @@ function i18n_string_translate($name, $string, $options = array()) { } else { // If we don't want to translate to this language, format and return - if (!empty($options['sanitize default']) && empty($options['format'])) { - $string = check_plain($string); - } + $options['sanitize'] = !empty($options['sanitize default']); return i18n_string_format($string, $options); } } @@ -486,15 +535,57 @@ function i18n_string_translate($name, $string, $options = array()) { /** * Check user access to translate a specific string. - * + * * If the string has a format the user is not allowed to edit, it will return FALSE - * + * * @param $string_format; * String object or $format_id */ function i18n_string_translate_access($string_format, $account = NULL) { $format_id = is_object($string_format) ? i18n_object_field($string_format, 'format') : $string_format; - return empty($format_id) || i18n_string_allowed_format($format_id) && ($format = filter_format_load($format_id)) && filter_access($format, $account) ; + return user_access('translate interface', $account) && + (empty($format_id) || i18n_string_allowed_format($format_id) && ($format = filter_format_load($format_id)) && filter_access($format, $account)); +} + +/** + * Check whether there is any problem for the user to translate a specific string. + * + * Here we assume the user has 'translate interface' access that should have + * been checked for the page. Possible reasons a user cannot translate a string: + * + * @param $i18nstring + * String object. + * @param $account + * Optional user account, defaults to current user. + * + * @return + * None or empty string if the user has access to translate the string. + * Message if the user cannot translate that string. + */ +function i18n_string_translate_check_string($i18nstring, $account = NULL) { + if (!user_access('translate interface', $account) || !user_access('translate user-defined strings', $account)) { + return t('This is a user-defined string. You are not allowed to translate these strings.'); + } + elseif (!empty($i18nstring->format)) { + if (!i18n_string_allowed_format($i18nstring->format)) { + $format = filter_format_load($i18nstring->format); + return t('This string uses the %name text format. Strings with this format are not allowed for translation.', array('%name' => $format->name)); + } + elseif ($format = filter_format_load($i18nstring->format)) { + // It is a text format, check user access to that text format. + if (!filter_access($format, $account)) { + return t('This string uses the %name text format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name)); + } + } + else { + // This is one of our special formats, I18N_STRING_FILTER_* + if ($i18nstring->format == I18N_STRING_FILTER_XSS_ADMIN && !user_access('translate admin strings', $account)) { + return t('The source string is an administrative string. You are not allowed to translate these strings.'); + } + } + } + // No error message, it should be OK to translate. + return ''; } /** @@ -512,19 +603,24 @@ function i18n_string_translate_access($string_format, $account = NULL) { */ function i18n_string_format($string, $options = array()) { $options += array('langcode' => i18n_langcode(), 'format' => FALSE, 'sanitize' => TRUE, 'cache' => FALSE); - // Apply format and callback + // Apply format and callback if ($string) { - if ($options['format'] && $options['sanitize']) { - // Handle special format values (xss, xss_admin) - switch ($options['format']) { - case I18N_STRING_FILTER_XSS: - $string = !empty($options['allowed_tags']) ? filter_xss($string, $options['allowed_tags']) : filter_xss($string); - break; - case I18N_STRING_FILTER_XSS_ADMIN: - $string = filter_xss_admin($string); - break; - default: - $string = check_markup($string, $options['format'], $options['langcode'], $options['cache']); + if ($options['sanitize']) { + if ($options['format']) { + // Handle special format values (xss, xss_admin) + switch ($options['format']) { + case I18N_STRING_FILTER_XSS: + $string = !empty($options['allowed_tags']) ? filter_xss($string, $options['allowed_tags']) : filter_xss($string); + break; + case I18N_STRING_FILTER_XSS_ADMIN: + $string = filter_xss_admin($string); + break; + default: + $string = check_markup($string, $options['format'], $options['langcode'], $options['cache']); + } + } + else { + $string = check_plain($string); } } if (isset($options['callback'])) { @@ -642,10 +738,12 @@ function i18n_string_remove($name, $string = NULL, $options = array()) { function i18n_string_l10n_client_add($string, $langcode) { // If current language add to l10n client list for later on page translation. // If langcode translation was disabled we are not supossed to reach here. - if (($langcode == i18n_langcode()) && function_exists('l10_client_add_string_to_page') && i18n_string_translate_access($string)) { - $translation = $string->get_translation($langcode); - $source = !empty($string->source) ? $string->source : $string->string; - l10_client_add_string_to_page($source, $translation ? $translation : TRUE, $string->textgroup, $string->context); + if (($langcode == i18n_langcode()) && function_exists('l10_client_add_string_to_page') && user_access('translate interface')) { + if (!$string->check_translate_access()) { + $translation = $string->get_translation($langcode); + $source = !empty($string->source) ? $string->source : $string->string; + l10_client_add_string_to_page($source, $translation ? $translation : TRUE, $string->textgroup, $string->context); + } } } @@ -710,7 +808,7 @@ function i18n_string_object_remove($type, $object, $options = array()) { /** * Update object properties. - * + * * @param $type * Object type * @param $object @@ -730,20 +828,20 @@ function i18n_string_object_translate_page($object_type, $object_value, $languag /** * Preload all strings for this textroup/context. - * + * * This is a performance optimization to load all needed strings with a single query. - * + * * Examples of valid string name to search are: * - 'taxonomy:term:*:title' * This will find all titles for taxonomy terms * - array('taxonomy', 'term', array(1,2), '*') * This will find all properties for taxonomy terms 1 and 2 - * + * * @param $name * Specially crafted string name, it may take '*' and array parameters for each element. * @param $langcode * Language code to search translations. Defaults to current language. - * + * * @return array() * String objects indexed by context. */ @@ -764,7 +862,7 @@ function i18n_string_translation_search($name, $langcode = NULL) { * The language code to translate to a language other than what is used to display the page. * @param $source_string * Optional source string, just in case it needs to be created. - * + * * @return mixed * Source string object if update was successful. * Null if source string not found. @@ -775,21 +873,15 @@ function i18n_string_translation_update($name, $translation, $langcode, $source_ return i18n_string_multiple('translation_update', $name, $translation, $langcode); } elseif ($source = i18n_string_get_source($name)) { - if (i18n_string_translation_validate($source, $translation)) { - if ($langcode == i18n_string_source_language()) { - // It's the default language so we should update the string source as well. - i18n_string_update($name, $translation); - } - else { - list ($textgroup, $context) = i18n_string_context($name); - i18n_string_textgroup($textgroup)->update_translation($context, $langcode, $translation); - } - return $source; + if ($langcode == i18n_string_source_language()) { + // It's the default language so we should update the string source as well. + i18n_string_update($name, $translation); } else { - // We cannot update this string because of its input format. - return FALSE; + list ($textgroup, $context) = i18n_string_context($name); + i18n_string_textgroup($textgroup)->update_translation($context, $langcode, $translation); } + return $source; } elseif ($source_string) { // We don't have a source in the database, so we need to create it, but only if we've got the source too. @@ -801,18 +893,3 @@ function i18n_string_translation_update($name, $translation, $langcode, $source_ return NULL; } } - -/** - * Validate translation and check user access to input format - */ -function i18n_string_translation_validate($i18nstring, $translation) { - if (!empty($i18nstring->format)) { - // If we've got a text format, just need to check user access to it. - return i18n_string_translate_access($i18nstring); - } - else { - // If not text format use standard locale validation. - // Note: looks like locale.inc is included by locale_init() ?! - return locale_string_is_safe($translation); - } -} \ No newline at end of file diff --git a/i18n_string/i18n_string.pages.inc b/i18n_string/i18n_string.pages.inc index 7ca4289..dcab466 100644 --- a/i18n_string/i18n_string.pages.inc +++ b/i18n_string/i18n_string.pages.inc @@ -7,6 +7,10 @@ * @author Jose A. Reyero, 2007 */ +// Load locale libraries +require_once './includes/locale.inc'; +require_once drupal_get_path('module', 'locale') . '/locale.admin.inc'; + /** * Generate translate page from object */ @@ -44,9 +48,9 @@ function i18n_string_translate_page_overview_form($form, &$form_state, $object, // Set the default item key, assume it's the first. $item_title = reset($strings); $header = array( - 'language' => t('Language'), - 'title' => t('Title'), - 'status' => t('Status'), + 'language' => t('Language'), + 'title' => t('Title'), + 'status' => t('Status'), 'operations' => t('Operations') ); $source_language = variable_get_value('i18n_string_source_language'); @@ -96,7 +100,7 @@ function i18n_string_translate_page_overview_form($form, &$form_state, $object, /** * Form builder callback for in-place string translation. - * + * * @param $strings * Array of strings indexed by string name (may be indexed by group key too if $groups is present) * @param $langcode @@ -107,7 +111,8 @@ function i18n_string_translate_page_overview_form($form, &$form_state, $object, function i18n_string_translate_page_form($form, &$form_state, $strings, $langcode, $groups = NULL) { $form = i18n_string_translate_page_form_base($form, $langcode); if ($groups) { - $form['string_groups'] = array('#type' => 'value', '#value' => $groups); + // I we've got groups, string list is grouped by group key. + $form['string_groups'] = array('#type' => 'value', '#value' => $strings); foreach ($groups as $key => $title) { $form['display'] = array( '#type' => 'vertical_tabs', @@ -117,9 +122,12 @@ function i18n_string_translate_page_form($form, &$form_state, $strings, $langcod '#title' => $title, '#type' => 'fieldset', ) + i18n_string_translate_page_form_strings($strings[$key], $langcode); + $form['string_list']['#value'] += $strings[$key]; } } else { + // We add a single group with key 'all', but no tabs. + $form['string_groups'] = array('#type' => 'value', '#value' => array('all' => $strings)); $form['strings']['all'] = i18n_string_translate_page_form_strings($strings, $langcode); } return $form; @@ -144,9 +152,8 @@ function i18n_string_translate_page_form_base($form, $langcode, $redirect = NULL ); } // Add explicit validate and submit hooks so this can be used from inside any form. - $form['#validate'] = array('i18n_string_translate_page_form_validate'); $form['#submit'] = array('i18n_string_translate_page_form_submit'); - return $form; + return $form; } /** @@ -155,88 +162,72 @@ function i18n_string_translate_page_form_base($form, $langcode, $redirect = NULL function i18n_string_translate_page_form_strings($strings, $langcode) { $formats = filter_formats(); foreach ($strings as $item) { - $disabled = FALSE; - $description = ''; - // We may have a source or not. Maybe the format is disallowed for all. + // We may have a source or not. Load it, our string may get the format from it. $source = $item->get_source(); $format_id = $source ? $source->format : $item->format; - if ($format_id) { - $format = filter_format_load($format_id); - $disabled = !i18n_string_translate_access($item); - if ($disabled) { - $description = t('This string uses the %name text format. You are not allowed to translate or edit texts with this format.', array('%name' => $format->name)); - } - else { - $description = '
' . t('Text format: @name', array('@name' => $format->name)) . '
' . theme('filter_tips', array('tips' => _filter_tips($format->format, FALSE))); - } + $description = ''; + // Check permissions to translate this string, depends on format, etc.. + if ($message = $item->check_translate_access()) { + // We'll display a disabled element with the reason it cannot be translated. + $disabled = TRUE; + $description = $message; } - // If we don't have a source we create it. - if (!$source && !$disabled) { - // Enable messages just as a reminder these strings are not being updated properly. - $status = $item->update(array('messages' => TRUE)); - if ($status === FALSE || $status === SAVED_DELETED) { - // We don't have a source string so nothing to translate here - $disabled = TRUE; - } - else { - $source = $item->get_source(); + else { + $disabled = FALSE; + $description = ''; + // If we don't have a source and it can be translated, we create it. + if (!$source) { + // Enable messages just as a reminder these strings are not being updated properly. + $status = $item->update(array('messages' => TRUE)); + if ($status === FALSE || $status === SAVED_DELETED) { + // We don't have a source string so nothing to translate here + $disabled = TRUE; + } + else { + $source = $item->get_source(); + } } } + $default_value = $item->format_translation($langcode, array('langcode' => $langcode, 'sanitize' => FALSE, 'debug' => FALSE)); $form[$item->get_name()] = array( '#title' => $item->get_title(), '#type' => 'textarea', '#default_value' => $default_value, '#disabled' => $disabled, - '#description' => $description, - '#i18n_string_format' => $source ? $source->format : 0, + '#description' => $description . _i18n_string_translate_format_help($format_id), + //'#i18n_string_format' => $source ? $source->format : 0, // If disabled, provide smaller textarea (that can be expanded anyway). '#rows' => $disabled ? 1 : min(ceil(str_word_count($default_value) / 12), 10), '#parents' => array('strings', $item->get_name()), ); } - return $form; -} - -/** - * Validation submission callback for in-place string translation. - */ -function i18n_string_translate_page_form_validate($form, &$form_state) { - foreach ($form_state['values']['strings'] as $key => $value) { - // We don't need to validate disabled form fields because those are already - // validated by the FormAPI. - if (empty($form['strings'][$key]['#i18n_string_format'])) { - i18n_string_validate_submission("strings][$key", $value); - } - } + return $form; } /** * Form submission callback for in-place string translation. */ function i18n_string_translate_page_form_submit($form, &$form_state) { - foreach ($form_state['values']['strings'] as $key => $value) { - list($textgroup, $context) = i18n_string_context(explode(':', $key)); - i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value); + $count = $success = 0; + foreach ($form_state['values']['strings'] as $name => $value) { + $count++; + list($textgroup, $context) = i18n_string_context(explode(':', $name)); + $result = i18n_string_textgroup($textgroup)->update_translation($context, $form_state['values']['langcode'], $value); + $success += ($result ? 1 : 0); + } + if ($success) { + drupal_set_message(format_plural($success, 'A translation was saved successfully.', '@count translations were saved successfully.')); + } + if ($error = $count - $success) { + drupal_set_message(format_plural($error, 'A translation could not be saved.', '@count translations could not be saved.'), 'warning'); } - drupal_set_message(t('Translations saved.')); if (isset($form['#redirect'])) { $form_state['redirect'] = $form['#redirect']; } } /** - * String submission validation callback. - */ -function i18n_string_validate_submission($formkey, $value) { - // Validation based on locale_translate_edit_form_validate. - if (!locale_string_is_safe($value)) { - form_set_error($formkey, t('The submitted string contains disallowed HTML: %string', array('%string' => $value))); - watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING); - } -} - -/** * Menu callback. Saves a string translation coming as POST data. */ function i18n_string_l10n_client_save_string() { @@ -244,18 +235,29 @@ function i18n_string_l10n_client_save_string() { if (user_access('use on-page translation')) { $textgroup = !empty($_POST['textgroup']) ? $_POST['textgroup'] : 'default'; - // Default textgroup will be handled by l10n_client module - if ($textgroup == 'default') { + // Other textgroups will be handled by l10n_client module + if (!i18n_string_group_info($textgroup)) { return l10n_client_save_string(); } elseif (isset($_POST['source']) && isset($_POST['target']) && !empty($_POST['context']) && !empty($_POST['form_token']) && drupal_valid_token($_POST['form_token'], 'l10n_client_form')) { $name = $textgroup . ':' . $_POST['context']; - $result = i18n_string_translation_update($name, $_POST['target'], $language->language, $_POST['source']); - if ($result) { - $message = theme('l10n_client_message', array('message' => t('Translation saved locally for user defined string.'), 'level' => WATCHDOG_INFO)); - } - elseif ($result === FALSE) { - $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.'))); + if ($i18nstring = i18n_string_get_source($name)) { + // Since this is not a real form, we double check access permissions here too. + if ($error = $i18nstring->check_translate_access()) { + $message = theme('l10n_client_message', array('message' => t('Not saved due to: !reason', array('!reason' => $error)), 'level' => WATCHDOG_WARNING)); + } + else { + $result = i18n_string_translation_update($name, $_POST['target'], $language->language, $_POST['source']); + if ($result) { + $message = theme('l10n_client_message', array('message' => t('Translation saved locally for user defined string.'), 'level' => WATCHDOG_INFO)); + } + elseif ($result === FALSE) { + $message = theme('l10n_client_message', array('message' => t('Not saved due to insufficient permissions.'))); + } + else { + $message = theme('l10n_client_message', array('message' => t('Not saved due to unknown reason.'))); + } + } } else { $message = theme('l10n_client_message', array('message' => t('Not saved due to source string missing.'))); @@ -268,3 +270,166 @@ function i18n_string_l10n_client_save_string() { exit; } } + + +/** + * User interface for string editing. + */ +function i18n_string_locale_translate_edit_form($form, &$form_state, $lid) { + // Fetch source string, if possible. + $source = db_query('SELECT source, context, textgroup, location FROM {locales_source} WHERE lid = :lid', array(':lid' => $lid))->fetchObject(); + if (!$source) { + drupal_set_message(t('String not found.'), 'error'); + drupal_goto('admin/config/regional/translate/translate'); + } + + // Add original text to the top and some values for form altering. + $form['original'] = array( + '#type' => 'item', + '#title' => t('Original text'), + '#markup' => check_plain(wordwrap($source->source, 0)), + ); + if (!empty($source->context)) { + $form['context'] = array( + '#type' => 'item', + '#title' => t('Context'), + '#markup' => check_plain($source->context), + ); + } + $form['lid'] = array( + '#type' => 'value', + '#value' => $lid + ); + $form['textgroup'] = array( + '#type' => 'value', + '#value' => $source->textgroup, + ); + $form['location'] = array( + '#type' => 'value', + '#value' => $source->location + ); + + // Include default form controls with empty values for all languages. + // This ensures that the languages are always in the same order in forms. + $languages = language_list(); + + // We don't need the default language value, that value is in $source. + $omit = $source->textgroup == 'default' ? 'en' : i18n_string_source_language(); + unset($languages[($omit)]); + $form['translations'] = array('#tree' => TRUE); + // Approximate the number of rows to use in the default textarea. + $rows = min(ceil(str_word_count($source->source) / 12), 10); + foreach ($languages as $langcode => $language) { + $form['translations'][$langcode] = array( + '#type' => 'textarea', + '#title' => t($language->name), + '#rows' => $rows, + '#default_value' => '', + ); + } + + // Fetch translations and fill in default values in the form. + $result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid AND language <> :omit", array(':lid' => $lid, ':omit' => $omit)); + foreach ($result as $translation) { + $form['translations'][$translation->language]['#default_value'] = $translation->translation; + } + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); + + // Restrict filter permissions and handle validation and submission for i18n strings + $context = db_select('i18n_string', 'i18ns') + ->fields('i18ns') + ->condition('lid', $form['lid']['#value']) + ->execute() + ->fetchObject(); + + if (i18n_string_group_info($source->textgroup)) { + if ($i18nstring = i18n_string_get_by_lid($form['lid']['#value'])) { + $form['i18n_string'] = array('#type' => 'value', '#value' => $i18nstring); + if ($message = $i18nstring->check_translate_access()) { + drupal_set_message($message); + $disabled = TRUE; + } + // Add format help anyway, though the form may be disabled. + $form['translations']['format_help']['#markup'] = _i18n_string_translate_format_help($i18nstring->format); + } + else { + drupal_set_message(t('Source string not found.'), 'warning'); + $disabled = TRUE; + } + if (!empty($disabled)) { + // Disable all form elements + $form['submit']['#disabled'] = TRUE; + foreach (element_children($form['translations']) as $langcode) { + $form['translations'][$langcode]['#disabled'] = TRUE; + } + } + } + return $form; +} + +/** + * Process string editing form validations. + * + * If it is an allowed format, skip default validation, the text will be filtered later + */ +function i18n_string_locale_translate_edit_form_validate($form, &$form_state) { + if (empty($form_state['values']['i18n_string'])) { + // If not i18n string use regular locale validation. + $copy_state = $form_state; + locale_translate_edit_form_validate($form, $copy_state); + } +} + +/** + * Process string editing form submissions. + * + * Mark translations as current. + */ +function i18n_string_locale_translate_edit_form_submit($form, &$form_state) { + // Invoke locale submission. + locale_translate_edit_form_submit($form, $form_state); + $lid = $form_state['values']['lid']; + foreach ($form_state['values']['translations'] as $key => $value) { + if (!empty($value)) { + // An update has been made, so we assume the translation is now current. + db_update('locales_target') + ->fields(array('i18n_status' => I18N_STRING_STATUS_CURRENT)) + ->condition('lid', $lid) + ->condition('language', $key) + ->execute(); + } + } +} + +/** + * Help for text format. + */ +function _i18n_string_translate_format_help($format_id) { + $output = ''; + if ($format = filter_format_load($format_id)) { + $title = t('Text format: @name', array('@name' => $format->name)); + $tips = theme('filter_tips', array('tips' => _filter_tips($format_id, FALSE))); + } + elseif ($format_id == I18N_STRING_FILTER_XSS) { + $title = t('Standard XSS filter.'); + $allowed_html = '
    1. '; + $tips[] = t('Allowed HTML tags: @tags', array('@tags' => $allowed_html)); + } + elseif ($format_id == I18N_STRING_FILTER_XSS_ADMIN) { + $title = t('Administration XSS filter.'); + $tips[] = t('It will allow most HTML tags but not scripts nor styles.'); + } + elseif ($format_id) { + $title = t('Unknown filter: @name', array('@name' => $format_id)); + } + + if (!empty($title)) { + $output .= '
      ' . $title . '
      '; + } + if (!empty($tips)) { + $output .= is_array($tips) ? theme('item_list', array('items' => $tips)) : $tips; + } + return $output; +} \ No newline at end of file diff --git a/i18n_string/i18n_string.test b/i18n_string/i18n_string.test index 5d7fc5b..0d62642 100644 --- a/i18n_string/i18n_string.test +++ b/i18n_string/i18n_string.test @@ -26,6 +26,7 @@ class i18nStringTestCase extends Drupali18nTestCase { // We can use any of the modules that define a text group, to use it for testing parent::setUp('i18n_string', 'i18n_menu'); parent::setUpLanguages(); + $this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings')); } /** diff --git a/i18n_sync/i18n_sync.test b/i18n_sync/i18n_sync.test index f01854c..f9a673e 100644 --- a/i18n_sync/i18n_sync.test +++ b/i18n_sync/i18n_sync.test @@ -35,7 +35,7 @@ class i18nSyncTestCase extends Drupali18nTestCase { $this->drupalPost(NULL, array(), t('Save settings')); // Create some content and check selection modes - $this->drupalLogin($this->translator); + $this->drupalLogin($this->content_editor); // variable_set('language_content_type_story', 1); $source = $this->createNode('page', $this->randomName(), $this->randomString(20), language_default('language'), array('field_tags[und]' => $tag_name = $this->randomName())); @@ -60,7 +60,7 @@ class i18nSyncTestCase extends Drupali18nTestCase { $edit = array( 'promote' => 1, 'sticky' => 1, - 'field_tags[und]' => $new_tag, + 'field_tags[und]' => $new_tag, ); $this->drupalPost('node/' . $source->nid . '/edit', $edit, t('Save')); $terms = taxonomy_get_term_by_name($new_tag); diff --git a/i18n_taxonomy/i18n_taxonomy.test b/i18n_taxonomy/i18n_taxonomy.test index 6397760..3372bd9 100644 --- a/i18n_taxonomy/i18n_taxonomy.test +++ b/i18n_taxonomy/i18n_taxonomy.test @@ -31,6 +31,7 @@ class i18nTaxonomyTestCase extends Drupali18nTestCase { filter_permission_name($filtered_html_format), filter_permission_name($full_html_format), )); + $this->translator = $this->drupalCreateUser(array('translate interface', 'translate user-defined strings')); } function testTaxonomyTermLocalize() {