Index: views_ui.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views_ui.module,v retrieving revision 1.109 diff -u -p -r1.109 views_ui.module --- views_ui.module 30 Jan 2009 00:56:01 -0000 1.109 +++ views_ui.module 20 Feb 2009 03:52:39 -0000 @@ -233,6 +233,10 @@ function views_ui_cache_load($name) { // Check to see if someone else is already editing this view. global $user; $view->locked = db_fetch_object(db_query("SELECT s.uid, v.updated FROM {views_object_cache} v INNER JOIN {sessions} s ON v.sid = s.sid WHERE s.sid != '%s' and v.name = '%s' and v.obj = 'view' ORDER BY v.updated ASC", session_id(), $view->name)); + // Set a flag to indicate that this view is being edited. + // This flag will be used e.g. to determine whether strings + // should be localized. + $view->editing = TRUE; } } Index: help/localization.html =================================================================== RCS file: help/localization.html diff -N help/localization.html --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ help/localization.html 20 Feb 2009 03:52:40 -0000 @@ -0,0 +1,28 @@ + +

On multilingual sites, custom and overridden views may contain text that could be translated into one or more languages. Views makes this data available for translation.

+ +

You can select which localization plugin to use at Administer >> Site building >> Views >> Tools. By default, Views supports "None" (don't localize these strings) and "Core" (use Drupal core's t() function).

+ +

While it "works", the Core plugin is not recommended, as it doesn't support updates to existing strings. If you need to translate Views labels into other languages, consider installing the Internationalization package's Views translation module.

+ +

To prevent security issues, PHP code is replaced with placeholders before being passed for translation. For example, a header with the following text + +Welcome, you are visitor number <?php echo visitor_count(); ?>. + +would be passed as + +Welcome, you are visitor number !php0. + +As well as addressing potential security holes, using placeholders in translations avoids presenting confusing code to translators.

+ +

To prevent the possible insertion of additional PHP in translations, translated text is passed through strip_tags(), a function used to strip out PHP and HTML tags from text.

+ +

If you wish to retain some HTML in e.g. a header or footer that accepts PHP: + +

+ +Following this approach will ensure that you can retain a subset of HTML tags while safely using PHP in translatable Views text.

Index: help/views.help.ini =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/help/views.help.ini,v retrieving revision 1.16 diff -u -p -r1.16 views.help.ini --- help/views.help.ini 30 Sep 2008 00:01:27 -0000 1.16 +++ help/views.help.ini 20 Feb 2009 03:52:40 -0000 @@ -152,6 +152,9 @@ parent = analyze-theme [overrides] title = What are overrides? +[localization] +title = Localizing views data like header and footer text + [embed] title = Embedding a view into other parts of your site Index: includes/admin.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/admin.inc,v retrieving revision 1.152 diff -u -p -r1.152 admin.inc --- includes/admin.inc 18 Feb 2009 02:01:03 -0000 1.152 +++ includes/admin.inc 20 Feb 2009 03:52:47 -0000 @@ -2728,6 +2728,14 @@ function views_ui_admin_tools() { '#default_value' => variable_get('views_no_javascript', FALSE), ); + $form['views_localization_plugin'] = array( + '#type' => 'radios', + '#title' => t('Localization plugin'), + '#options' => views_fetch_plugin_names('localization', NULL, array(), TRUE), + '#default_value' => variable_get('views_localization_plugin', 'core'), + '#description' => t('Select a plugin for translation of Views data like header, footer, and empty text.'), + ); + $regions = system_region_list(variable_get('theme_default', 'garland')); $form['views_devel_region'] = array( @@ -2962,11 +2970,13 @@ function views_fetch_fields($base, $type * 'summary', 'feed' or others based on the neds of the display. * @param $base * An array of possible base tables. + * @param $with_help + * If true, add help to the names. * * @return * A keyed array of in the form of 'base_table' => 'Description'. */ -function views_fetch_plugin_names($type, $key = NULL, $base = array()) { +function views_fetch_plugin_names($type, $key = NULL, $base = array(), $with_help = FALSE) { $data = views_fetch_plugin_data(); $plugins[$type] = array(); @@ -2977,7 +2987,7 @@ function views_fetch_plugin_names($type, continue; } if (empty($plugin['no ui']) && (empty($base) || empty($plugin['base']) || array_intersect($base, $plugin['base']))) { - $plugins[$type][$id] = $plugin['title']; + $plugins[$type][$id] = $plugin['title'] . ($with_help && $plugin['help'] ? ': ' . $plugin['help'] : ''); } } Index: includes/base.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/base.inc,v retrieving revision 1.2 diff -u -p -r1.2 base.inc --- includes/base.inc 6 Jun 2008 19:29:03 -0000 1.2 +++ includes/base.inc 20 Feb 2009 03:52:47 -0000 @@ -19,7 +19,8 @@ class views_object { * @code * 'option_name' => array( * - 'default' => default value, - * - 'translatable' => TRUE/FALSE (wrap in t() on export if true), + * - 'translatable' => TRUE/FALSE (use a localization plugin on display and + * wrap in t() on export if true), * - 'contains' => array of items this contains, with its own defaults, etc. * If contains is set, the default will be ignored and assumed to * be array() @@ -28,8 +29,6 @@ class views_object { * @endcode * Each option may have any of the following functions: * - export_option_OPTIONNAME -- Special export handling if necessary. - * - translate_option_OPTIONNAME -- Special handling for translating data - * within the option, if necessary. */ function option_definition() { return array(); } @@ -76,7 +75,7 @@ class views_object { * Unpack options over our existing defaults, drilling down into arrays * so that defaults don't get totally blown away. */ - function unpack_options(&$storage, $options, $definition = NULL) { + function unpack_options(&$storage, $options, $definition = NULL, $localization_keys = array()) { if (!is_array($options)) { return; } @@ -86,15 +85,38 @@ class views_object { } foreach ($options as $key => $value) { + // Ensure we have a localization plugin. + $this->view->init_localization(); + if (is_array($value)) { if (!isset($storage[$key]) || !is_array($storage[$key])) { $storage[$key] = array(); } - $this->unpack_options($storage[$key], $value, isset($definition[$key]) ? $definition[$key] : array()); + $this->unpack_options($storage[$key], $value, isset($definition[$key]) ? $definition[$key] : array(), array_merge($localization_keys, array($key))); } - else if (!empty($definition[$key]['translatable']) && !empty($value)) { - $storage[$key] = t($value); + + // Don't localize strings during editing. When editing, we need to work with + // the original data, not the translated version. + else if (!$this->view->editing && !empty($definition[$key]['translatable']) && !empty($value)) { + // Insert placeholders for any PHP code that the string may contain. + list($accepts_php, $search, $replace, $allowed_html) = $this->view->insert_php_placeholders($value, $options, $key); + if ($this->view->is_translatable()) { + // The $keys array is built from the view name, any localization keys + // sent in, and the name of the property being processed. + $storage[$key] = $this->view->localization_plugin->translate($value, array_merge(array($this->view->name), $localization_keys, array($key))); + // Ensure no PHP was added in the translation. + // This has the cost of stripping out HTML as well. + if ($accepts_php) { + $storage[$key] = strip_tags($storage[$key], $allowed_html); + } + } + // Otherwise, this is a code-based string, so we can use t(). + else { + $storage[$key] = t($value); + } + // In case placeholders were added above, restore any substituted PHP code. + $storage[$key] = str_replace($replace, $search, $storage[$key]); } else { $storage[$key] = $value; Index: includes/plugins.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/plugins.inc,v retrieving revision 1.152 diff -u -p -r1.152 plugins.inc --- includes/plugins.inc 7 Jan 2009 23:31:12 -0000 1.152 +++ includes/plugins.inc 20 Feb 2009 03:52:48 -0000 @@ -235,6 +235,25 @@ function views_views_plugins() { 'help topic' => 'access-perm', ), ), + 'localization' => array( + 'parent' => array( + 'no ui' => TRUE, + 'handler' => 'views_plugin_localization', + 'parent' => '', + ), + 'none' => array( + 'title' => t('None'), + 'help' => t('Do not pass admin strings for translation.'), + 'handler' => 'views_plugin_localization_none', + 'help topic' => 'localization-none', + ), + 'core' => array( + 'title' => t('Core'), + 'help' => t("Use Drupal core t() function. Not recommended, as it doesn't support updates to existing strings."), + 'handler' => 'views_plugin_localization_core', + 'help topic' => 'localization-core', + ), + ), ); } Index: includes/view.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/view.inc,v retrieving revision 1.151 diff -u -p -r1.151 view.inc --- includes/view.inc 18 Feb 2009 01:41:34 -0000 1.151 +++ includes/view.inc 20 Feb 2009 03:52:52 -0000 @@ -23,6 +23,7 @@ class view extends views_db_object { // State variables var $built = FALSE; var $executed = FALSE; + var $editing = FALSE; var $args = array(); var $build_info = array(); @@ -59,6 +60,9 @@ class view extends views_db_object { } $this->query = new stdClass(); + + // Initialize localization. + $this->init_localization(); } /** @@ -1316,6 +1320,9 @@ class view extends views_db_object { $this->_save_rows($key); } + // Save data for translation. + $this->save_locale_strings(); + cache_clear_all('views_urls', 'cache_views'); cache_clear_all(); // clear the page cache as well. } @@ -1341,6 +1348,8 @@ class view extends views_db_object { return; } + $this->delete_locale_strings(); + db_query("DELETE FROM {views_view} WHERE vid = %d", $this->vid); // Delete from all of our subtables as well. foreach ($this->db_objects() as $key) { @@ -1517,6 +1526,148 @@ class view extends views_db_object { return $errors ? $errors : TRUE; } + + /** + * Find and initialize the localizer plugin. + */ + function init_localization() { + if (isset($this->localization_plugin)) { + return is_object($this->localization_plugin); + } + + $this->localization_plugin = views_get_plugin('localization', variable_get('views_localization_plugin', 'core')); + + if (empty($this->localization_plugin)) { + return FALSE; + } + + return TRUE; + } + + /** + * Determine whether a view supports admin string translation. + */ + function is_translatable() { + // If the view is normal or overridden, use admin string translation. + return isset($this->type) && in_array($this->type, array('Normal', 'Overridden')); + } + + /** + * Send strings for localization. + */ + function save_locale_strings() { + $this->process_locale_strings('save'); + } + + /** + * Delete localized strings. + */ + function delete_locale_strings() { + $this->process_locale_strings('delete'); + } + + /** + * Process strings for localization or deletion. + */ + function process_locale_strings($op) { + // Ensure this view supports translation, we have a display, and we + // have a localization plugin. + if ($this->is_translatable() && $this->init_display() && $this->init_localization()) { + foreach ($this->display as $display_id => $display) { + $translatable = array(); + // Special handling for display title. + if (isset($display->display_title)) { + $translatable[] = array($display->display_title, array('display_title')); + } + $this->unpack_translatable($translatable, $display_id, $display->display_options); + foreach ($translatable as $data) { + list($string, $keys) = $data; + switch ($op) { + case 'save': + $this->localization_plugin->save($string, array_merge(array($this->name, $display_id), $keys)); + break; + case 'save': + $this->localization_plugin->delete($string, array_merge(array($this->name, $display_id), $keys)); + break; + } + } + } + } + } + + /** + * Unpack translatable properties and their values. + */ + function unpack_translatable(&$translatable, $display_id, $options, $definition = NULL, $keys = array()) { + + if (!is_array($options)) { + return; + } + + // Ensure we have displays with handlers. + $this->init_display(); + + if (!isset($definition)) { + $definition = $this->display[$display_id]->handler->option_definition(); + } + + foreach ($options as $key => $value) { + if (is_array($value)) { + $this->unpack_translatable($translatable, $display_id, $value, isset($definition[$key]) ? $definition[$key] : array(), array_merge($keys, array($key))); + } + else if (!empty($definition[$key]['translatable']) && !empty($value)) { + $this->insert_php_placeholders($value, $options, $key); + $translatable[] = array($value, array_merge($keys, array($key))); + } + } + } + + /** + * Convert PHP code to placeholders. + * + * Determines whether this is a property that accepts PHP and, if so, replaces + * PHP code with placeholders before localizing to avoid exposing + * potentially sensitive information to users who lack access to the PHP + * filter. + */ + function insert_php_placeholders(&$value, $options, $key) { + static $formats = array(); + static $allowed_html = array(); + + $search = $replace = array(); + // Look for a propertyname_format property. + if (isset($options[$key . '_format']) && $format = $options[$key . '_format']) { + if (!isset($formats[$format])) { + $formats[$format] = filter_list_format($format); + } + if ($accepts_php = isset($formats[$format]['php/0'])) { + // Find PHP code. + preg_match_all("/(<\?php)(.*?)(\?>)/", $value, $matches, PREG_SET_ORDER); + + // Build up arrays of search and replace values. + foreach ($matches as $index => $match) { + $search[] = $match[0]; + $replace[] = '!php' . $index; + } + $value = str_replace($search, $replace, $value); + // If the HTML filter is set, determine allowed HTML. + // The HTML filter must be configured to run after the PHP one. + if (isset($formats[$format]['filter/0']) && !isset($allowed_html[$format]) && variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP)) { + $allowed_html[$format] = variable_get("allowed_html_$format", '
    1. '); + } + } + } + return (array( + // Whether this format accepts PHP. + isset($accepts_php) && $accepts_php, + // An array of PHP strings. + $search, + // An array of placeholders. + $replace, + // HTML tags allowed by this format, if any. + isset($allowed_html[$format]) ? $allowed_html[$format] : '', + )); + } } /** Index: plugins/views_plugin_display.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/plugins/views_plugin_display.inc,v retrieving revision 1.18 diff -u -p -r1.18 views_plugin_display.inc --- plugins/views_plugin_display.inc 7 Jan 2009 23:31:13 -0000 1.18 +++ plugins/views_plugin_display.inc 20 Feb 2009 03:52:57 -0000 @@ -40,7 +40,9 @@ class views_plugin_display extends views unset($options['defaults']); } - $this->unpack_options($this->options, $options); + // Last argument is an array of keys to be used in identifying + // strings for translation. + $this->unpack_options($this->options, $options, NULL, array($display->id)); } function destroy() { Index: plugins/views_plugin_localization.inc =================================================================== RCS file: plugins/views_plugin_localization.inc diff -N plugins/views_plugin_localization.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_localization.inc 20 Feb 2009 03:52:57 -0000 @@ -0,0 +1,53 @@ +view = &$view; + } + + /** + * Translate a string. + * + * @param $string + * The string to be translated. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + */ + function translate($string, $keys = array()) { } + + /** + * Save a string for translation. + * + * @param $string + * The string to be saved. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + */ + function save($string, $keys = array()) { } + + /** + * Delete a string. + * + * @param $string + * The string to be deleted. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + */ + function delete($string, $keys = array()) { } + +} Index: plugins/views_plugin_localization_core.inc =================================================================== RCS file: plugins/views_plugin_localization_core.inc diff -N plugins/views_plugin_localization_core.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_localization_core.inc 20 Feb 2009 03:52:57 -0000 @@ -0,0 +1,74 @@ +language == 'en') { + $changed = TRUE; + $languages = language_list(); + $cached_language = $language; + unset($languages['en']); + $language = current($languages); + } + + t($string); + + if (isset($cached_language)) { + $language = $cached_language; + } + return TRUE; + } + + /** + * Delete a string. + * + * Deletion is not supported. + * + * @param $string + * The string to be deleted. + * @param $keys + * An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + */ + function delete($string, $keys = array()) { + return FALSE; + } +} + Index: plugins/views_plugin_localization_none.inc =================================================================== RCS file: plugins/views_plugin_localization_none.inc diff -N plugins/views_plugin_localization_none.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/views_plugin_localization_none.inc 20 Feb 2009 03:52:57 -0000 @@ -0,0 +1,36 @@ +