Index: includes/plugins.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/plugins.inc,v retrieving revision 1.152.2.7 diff -u -r1.152.2.7 plugins.inc --- includes/plugins.inc 26 Nov 2009 00:35:16 -0000 1.152.2.7 +++ includes/plugins.inc 26 Nov 2009 17:48:26 -0000 @@ -11,7 +11,7 @@ */ function views_views_plugins() { $path = drupal_get_path('module', 'views') . '/js'; - return array( + $plugins = array( 'module' => 'views', // This just tells our themes are elsewhere. 'display' => array( 'parent' => array( @@ -290,7 +290,31 @@ 'help topic' => 'exposed-form-input-required', ), ), + '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', + ), + ), ); + // Add a help message pointing to the i18views module if it is not present. + if (!module_exists('i18nviews')) { + $plugins['localization']['core']['help'] .= ' ' . t('If you need to translate Views labels into other languages, consider installing the Internationalization package\'s Views translation module.', array('!path' => url('http://drupal.org/project/i18n', array('absolute' => TRUE)))); + } + return $plugins; } /** Index: includes/base.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/base.inc,v retrieving revision 1.2.2.4 diff -u -r1.2.2.4 base.inc --- includes/base.inc 26 Nov 2009 00:35:16 -0000 1.2.2.4 +++ includes/base.inc 26 Nov 2009 17:48:26 -0000 @@ -19,7 +19,8 @@ * @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 @@ * @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 @@ * 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, $all = TRUE) { + function unpack_options(&$storage, $options, $definition = NULL, $all = TRUE, $localization_keys = array()) { if (!is_array($options)) { return; } @@ -84,6 +83,8 @@ if (!isset($definition)) { $definition = $this->option_definition(); } + // Ensure we have a localization plugin. + $this->view->init_localization(); foreach ($options as $key => $value) { if (is_array($value)) { @@ -99,10 +100,26 @@ continue; } - $this->unpack_options($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : array(), $all); + $this->unpack_options($storage[$key], $value, isset($definition[$key]['contains']) ? $definition[$key]['contains'] : array(), $all, array_merge($localization_keys, array($key))); } - else if (!empty($definition[$key]['translatable']) && !empty($value)) { - $storage[$key] = t($value); + else if (!empty($definition[$key]['translatable']) && !empty($value) || !empty($definition['contains'][$key]['translatable']) && !empty($value)) { + if ($this->view->is_translatable()) { + // Allow other modules to make changes to the string before it's + // sent for translation. + // The $keys array is built from the view name, any localization keys + // sent in, and the name of the property being processed. + $translation_data = array( + 'value' => $value, + 'format' => isset($options[$key . '_format']) ? $options[$key . '_format'] : NULL, + 'keys' => array_merge(array($this->view->name), $localization_keys, array($key)), + ); + $storage[$key] = $this->view->localization_plugin->translate($translation_data); + } + // Otherwise, this is a code-based string, so we can use t(). + else { + $storage[$key] = t($value); + } + } else if ($all || !empty($definition[$key])) { $storage[$key] = $value; Index: includes/admin.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/admin.inc,v retrieving revision 1.154.2.12 diff -u -r1.154.2.12 admin.inc --- includes/admin.inc 26 Nov 2009 00:35:16 -0000 1.154.2.12 +++ includes/admin.inc 26 Nov 2009 17:48:26 -0000 @@ -626,7 +626,7 @@ if (empty($form['base_table']['#disabled'])) { $view->base_table = $form_state['values']['base_table']; } - + $view->editing = TRUE; views_ui_cache_set($view); $form_state['redirect'] ='admin/build/views/edit/' . $view->name; } @@ -2804,6 +2804,14 @@ '#default_value' => variable_get('views_no_javascript', FALSE), ); + $form['views_localization_plugin'] = array( + '#type' => 'radios', + '#title' => t('Translation method'), + '#options' => views_fetch_plugin_names('localization', NULL, array(), TRUE), + '#default_value' => variable_get('views_localization_plugin', 'core'), + '#description' => t('Select a translation method to use for Views data like header, footer, and empty text.'), + ); + $regions = system_region_list(variable_get('theme_default', 'garland')); $regions['watchdog'] = t('Watchdog'); Index: includes/view.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/includes/view.inc,v retrieving revision 1.151.2.22 diff -u -r1.151.2.22 view.inc --- includes/view.inc 26 Nov 2009 00:35:16 -0000 1.151.2.22 +++ includes/view.inc 26 Nov 2009 17:48:28 -0000 @@ -23,6 +23,7 @@ // State variables var $built = FALSE; var $executed = FALSE; + var $editing = FALSE; var $args = array(); var $build_info = array(); @@ -63,6 +64,8 @@ foreach ($this->db_objects() as $object) { $this->$object = array(); } + // Initialize localization. + $this->init_localization(); } /** @@ -1316,7 +1319,10 @@ foreach ($this->db_objects() as $key) { $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,7 +1347,9 @@ if (empty($this->vid)) { 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) { @@ -1377,6 +1385,15 @@ $output .= $display->handler->export_options($indent, '$handler->options'); } + // Give the localization system a chance to export translatables to code. + if ($this->init_localization()) { + $this->export_locale_strings('export'); + $translatables = $this->localization_plugin->export_render($indent); + if (!empty($translatables)) { + $output .= $translatables; + } + } + return $output; } @@ -1510,6 +1527,120 @@ return $errors ? $errors : TRUE; } + + /** + * Find and initialize the localizer plugin. + */ + function init_localization() { + if (isset($this->localization_plugin) && is_object($this->localization_plugin)) { + return TRUE; + } + + $this->localization_plugin = views_get_plugin('localization', variable_get('views_localization_plugin', 'core')); + + if (empty($this->localization_plugin)) { + return FALSE; + } + + $this->localization_plugin->init($this); + + return TRUE; + } + + /** + * Determine whether a view supports admin string translation. + */ + function is_translatable() { + // If the view is normal or overridden, use admin string translation. + // A newly created view won't have a type. Accept this. + return (!isset($this->type) || in_array($this->type, array(t('Normal'), t('Overridden')))) ? TRUE : FALSE; + } + + /** + * 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'); + } + + /** + * Export localized strings. + */ + function export_locale_strings() { + $this->process_locale_strings('export'); + } + + /** + * Process strings for localization, deletion or export to code. + */ + function process_locale_strings($op) { + // Ensure this view supports translation, we have a display, and we + // have a localization plugin. + if (($this->is_translatable() || $op == 'export') && $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('value' => $display->display_title, 'keys' => array('display_title')); + } + $this->unpack_translatable($translatable, $display_id, $display->display_options); + foreach ($translatable as $data) { + $data['keys'] = array_merge(array($this->name, $display_id), $data['keys']); + list($string, $keys) = $data; + switch ($op) { + case 'save': + $this->localization_plugin->save($data); + break; + case 'delete': + $this->localization_plugin->delete($data); + break; + case 'export': + $this->localization_plugin->export($data); + 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) { + $translation_keys = array_merge($keys, array($key)); + if (is_array($value)) { + $this->unpack_translatable($translatable, $display_id, $value, isset($definition[$key]) ? $definition[$key] : array(), $translation_keys); + } + else if (!empty($definition[$key]['translatable']) && !empty($value)) { + // Build source data and add to the array + $translatable[] = array( + 'value' => $value, + 'keys' => $translation_keys, + 'format' => isset($options[$key . '_format']) ? $options[$key . '_format'] : NULL, + ); + } + } + } } /** Index: plugins/views_plugin_display.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/plugins/views_plugin_display.inc,v retrieving revision 1.20.2.18 diff -u -r1.20.2.18 views_plugin_display.inc --- plugins/views_plugin_display.inc 26 Nov 2009 00:35:16 -0000 1.20.2.18 +++ plugins/views_plugin_display.inc 26 Nov 2009 17:48:30 -0000 @@ -40,7 +40,9 @@ 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: views_ui.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/views_ui.module,v retrieving revision 1.109.2.1 diff -u -r1.109.2.1 views_ui.module --- views_ui.module 10 Nov 2009 23:20:05 -0000 1.109.2.1 +++ views_ui.module 26 Nov 2009 17:48:23 -0000 @@ -233,6 +233,10 @@ // 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/views.help.ini =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/views/help/views.help.ini,v retrieving revision 1.17.2.2 diff -u -r1.17.2.2 views.help.ini --- help/views.help.ini 18 Nov 2009 19:57:51 -0000 1.17.2.2 +++ help/views.help.ini 26 Nov 2009 17:48:23 -0000 @@ -159,6 +159,9 @@ [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: 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 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,127 @@ +view = &$view; + } + + /** + * Translate a string / text with format + * + * The $source parameter is an array with the following elements: + * - value, source string + * - format, input format in case the text has some format to be applied + * - keys. An array of keys to identify the string. Generally constructed from + * view name, display_id, and a property, e.g., 'header'. + * + * @param $source + * Full data for the string to be translated. + * + * @return string + * Translated string / text + */ + function translate($source) { + // Allow other modules to make changes to the string before and after translation + $source['pre_process'] = $this->invoke_translation_process($source, 'pre'); + $source['translation'] = $this->translate_string($source['value'], $source['keys']); + $source['post_process'] = $this->invoke_translation_process($source, 'post'); + return $source['translation']; + } + + /** + * 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($string, $keys = array()) {} + + /** + * Save string source for translation. + * + * @param $source + * Full data for the string to be translated. + */ + function save($source) { + // Allow other modules to make changes to the string before saving + $source['pre_process'] = $this->invoke_translation_process($source, 'pre'); + $this->save_string($source['value'], $source['keys']); + } + + /** + * Save a string for translation + * + * @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 save_string($string, $keys = array()) {} + + /** + * Delete a string. + * + * @param $source + * Full data for the string to be translated. + */ + function delete($source) { } + + /** + * Collect strings to be exported to code. + * + * @param $source + * Full data for the string to be translated. + */ + function export($source) { } + + /** + * Render any collected exported strings to code. + * + * @param $indent + * An optional indentation for prettifying nested code. + */ + function export_render($indent = ' ') { } + + /** + * Invoke hook_translation_pre_process() or hook_translation_post_process(). + * + * Like node_invoke_nodeapi(), this function is needed to enable both passing + * by reference and fetching return values. + */ + function invoke_translation_process(&$value, $op) { + $return = array(); + $hook = 'translation_' . $op . '_process'; + foreach (module_implements($hook) as $module) { + $function = $module . '_' . $hook; + $result = $function($value); + if (isset($result)) { + $return[$module] = $result; + } + } + return $return; + } +} 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 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,104 @@ +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 $source + * Full data for the string to be translated. + */ + function delete($source) { + return FALSE; + } + + /** + * Collect strings to be exported to code. + * + * String identifiers are not supported so strings are anonymously in an array. + * + * @param $source + * Full data for the string to be translated. + */ + function export($source) { + if (!empty($source['value'])) { + $this->export_strings[] = $source['value']; + } + } + + /** + * Render any collected exported strings to code. + * + * @param $indent + * An optional indentation for prettifying nested code. + */ + function export_render($indent = ' ') { + $output = ''; + if (!empty($this->export_strings)) { + $this->export_strings = array_unique($this->export_strings); + $output = $indent . '$translatables[\'' . $this->view->name . '\'] = array(' . "\n"; + foreach ($this->export_strings as $string) { + $output .= $indent . " t('" . str_replace("'", "\'", $string) . "'),\n"; + } + $output .= $indent . ");\n"; + } + return $output; + } +} 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 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,30 @@ + +
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, you may wish to install the PHP translation module, also part of the Internationalization package.
+
+When this module is installed, 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 have enabled PHP translation and wish to retain some HTML in e.g. a header or footer that accepts PHP: + +