diff --git includes/base.inc includes/base.inc index 98625b6..98798f8 100644 --- includes/base.inc +++ includes/base.inc @@ -196,4 +196,76 @@ class views_object { } return $output; } + + /** + * Unpacks each handler to store translatable texts. + */ + function unpack_translatables(&$translatable) { + dsm(get_class($this)); + foreach ($this->option_definition() as $option => $definition) { + $this->unpack_translatable($translatable, $this->options, $option, $definition, array(), array()); + } + } + + /** + * Unpack a single option definition. + */ + function unpack_translatable(&$translatable, $storage, $option, $definition, $parents, $keys = array()) { +// dvm($definition); + + // Do not export options for which we have no settings. + if (!isset($storage[$option])) { + return; + } + + // Special handling for some items + if (isset($definition['unpack_translatable']) && method_exists($this, $definition['unpack_translatable'])) { + return $this->{$definition['unpack_translatable']}($translatable, $storage, $option, $definition, $parents, $keys); + } + + if (isset($definition['translatable'])) { + if ($definition['translatable'] === FALSE) { + return; + } + } + + // Add the current option to the parents tree. + $parents[] = $option; + + // If it has child items, unpack those separately. + if (isset($definition['contains'])) { + foreach ($definition['contains'] as $sub_option => $sub_definition) { + $translation_keys = array_merge($keys, array($sub_option)); + $this->unpack_translatable($translatable, $storage[$option], $sub_option, $sub_definition, $parents, $translation_keys); + } + } + + // @todo Figure out this double definition stuff. + $options = $storage[$option]; + if (is_array($options)) { + foreach ($options as $key => $value) { + $translation_keys = array_merge($keys, array($key)); + if (is_array($value)) { + $this->unpack_translatable($translatable, $storage, $key, $definition, $parents, $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, + ); + } + } + } + else if (!empty($definition['translatable']) && !empty($options)) { + $value = $options; + // Build source data and add to the array + $translatable[] = array( + 'value' => $value, + 'keys' => $translation_keys, + 'format' => isset($options[$option . '_format']) ? $options[$option . '_format'] : NULL, + ); + } + } } diff --git includes/plugins.inc includes/plugins.inc index b30145b..e8c16ba 100644 --- includes/plugins.inc +++ includes/plugins.inc @@ -325,7 +325,30 @@ function views_views_plugins() { 'parent' => 'full', ), ), + '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)))); + } if (module_invoke('ctools', 'api_version', '1.3')) { $plugins['style']['jump_menu_summary'] = array( diff --git includes/view.inc includes/view.inc index d6cbee8..4cf0341 100644 --- includes/view.inc +++ includes/view.inc @@ -1454,6 +1454,15 @@ class view extends views_db_object { $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; } @@ -1587,6 +1596,95 @@ class view extends views_db_object { 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; + } + + /** + * Figure out whether there should be options. + */ + $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. + // @fixme Export does not init every handler. + 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); + // Unpack handlers + $this->display[$display_id]->handler->unpack_translatables($translatable); + 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; + } + } + } + } + } + } /** diff --git plugins/views_plugin_display.inc plugins/views_plugin_display.inc index 35a6653..cfdadbd 100644 --- plugins/views_plugin_display.inc +++ plugins/views_plugin_display.inc @@ -408,12 +408,12 @@ class views_plugin_display extends views_plugin { // and therefore need special handling. 'access' => array( 'contains' => array( - 'type' => array('default' => 'none', 'export' => 'export_plugin'), + 'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), ), ), 'cache' => array( 'contains' => array( - 'type' => array('default' => 'none', 'export' => 'export_plugin'), + 'type' => array('default' => 'none', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), ), ), // Note that exposed_form plugin has options in a separate array, @@ -424,13 +424,13 @@ class views_plugin_display extends views_plugin { // should be copied. 'exposed_form' => array( 'contains' => array( - 'type' => array('default' => 'basic', 'export' => 'export_plugin'), + 'type' => array('default' => 'basic', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), 'options' => array('default' => array(), 'export' => FALSE), ), ), 'pager' => array( 'contains' => array( - 'type' => array('default' => 'full', 'export' => 'export_plugin'), + 'type' => array('default' => 'full', 'export' => 'export_plugin', 'unpack_translatable' => 'unpack_plugin'), 'options' => array('default' => array(), 'export' => FALSE), ), ), @@ -441,6 +441,7 @@ class views_plugin_display extends views_plugin { 'style_plugin' => array( 'default' => 'default', 'export' => 'export_style', + 'unpack_translatable' => 'unpack_plugin', ), 'style_options' => array( 'default' => array(), @@ -449,6 +450,7 @@ class views_plugin_display extends views_plugin { 'row_plugin' => array( 'default' => 'fields', 'export' => 'export_style', + 'unpack_translatable' => 'unpack_plugin', ), 'row_options' => array( 'default' => array(), @@ -462,14 +464,17 @@ class views_plugin_display extends views_plugin { 'header' => array( 'default' => array(), 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_plugin', ), 'footer' => array( 'default' => array(), 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_plugin', ), 'empty' => array( 'default' => array(), 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_plugin', ), // We want these to export last. @@ -499,6 +504,7 @@ class views_plugin_display extends views_plugin { 'filters' => array( 'default' => array(), 'export' => 'export_handler', + 'unpack_translatable' => 'unpack_plugin', ), @@ -2345,6 +2351,19 @@ class views_plugin_display extends views_plugin { return $output; } + + /** + * Special handling for plugin unpacking. + */ + function unpack_plugin(&$translatable, $storage, $option, $definition, $parents) { + $plugin_type = end($parents); + $plugin = $this->get_plugin($plugin_type); + if ($plugin) { + // Write which plugin to use. + return $plugin->unpack_translatables($translatable); + } + + } } diff --git plugins/views_plugin_localization.inc plugins/views_plugin_localization.inc new file mode 100644 index 0000000..12e25d6 --- /dev/null +++ plugins/views_plugin_localization.inc @@ -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; + } +} diff --git plugins/views_plugin_localization_core.inc plugins/views_plugin_localization_core.inc new file mode 100644 index 0000000..f7a2983 --- /dev/null +++ plugins/views_plugin_localization_core.inc @@ -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; + } +} diff --git plugins/views_plugin_localization_none.inc plugins/views_plugin_localization_none.inc new file mode 100644 index 0000000..42969bb --- /dev/null +++ plugins/views_plugin_localization_none.inc @@ -0,0 +1,36 @@ +export_strings[] = $source['value']; + } + } + + function get_export_strings() { + return $this->export_strings; + } +} diff --git tests/views_query.test tests/views_query.test index eaec5d8..3daafab 100644 --- tests/views_query.test +++ tests/views_query.test @@ -244,7 +244,15 @@ abstract class ViewsSqlTest extends ViewsTestCase { * The views plugin definition. Override it if you test provides a plugin. */ protected function viewsPlugins() { - return array(); + return array( + 'localization' => array( + 'test' => array( + 'title' => t('Test'), + 'help' => t('This is a test description.'), + 'handler' => 'views_plugin_localization_test', + ), + ), + ); } /** diff --git tests/views_translatable.test tests/views_translatable.test new file mode 100644 index 0000000..852dff3 --- /dev/null +++ tests/views_translatable.test @@ -0,0 +1,74 @@ + 'Views Translatable Test', + 'description' => 'Tests the pluggable transltations', + 'group' => 'Views', + ); + } + + function setUp() { + parent::setUp(); + variable_set('views_localization_plugin', 'none'); + } + + function viewsPlugins() { + return array( + ); + } + + function testUnpackTranslatable() { + $view = $this->view_unpack_translatable(); + $view->set_display('default'); + $view->export(); + + $expected_strings = array('Defaults', 'Apply1', 'Sort By1', 'Asc1', 'Desc1', 'simple1'); + $result_strings = $view->localization_plugin->get_export_strings(); + $this->assertEqual($expected_strings, $result_strings, 'Localisation plugin got every translatable string.'); + } + + function view_unpack_translatable() { + $view = new view; + $view->name = 'view_unpack_translatable'; + $view->description = ''; + $view->tag = ''; + $view->view_php = ''; + $view->base_table = 'node'; + $view->is_cacheable = FALSE; + $view->api_version = 2; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['access']['type'] = 'none'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['exposed_form']['options']['submit_button'] = 'Apply1'; + $handler->display->display_options['exposed_form']['options']['exposed_sorts_label'] = 'Sort By1'; + $handler->display->display_options['exposed_form']['options']['sort_asc_label'] = 'Asc1'; + $handler->display->display_options['exposed_form']['options']['sort_desc_label'] = 'Desc1'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['style_plugin'] = 'default'; + $handler->display->display_options['row_plugin'] = 'fields'; + /* Field: Node: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['label'] = 'simple'; + $handler->display->display_options['fields']['nid']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['nid']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['nid']['alter']['trim'] = 0; + $handler->display->display_options['fields']['nid']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['nid']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['nid']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['nid']['alter']['html'] = 0; + $handler->display->display_options['fields']['nid']['hide_empty'] = 0; + $handler->display->display_options['fields']['nid']['empty_zero'] = 0; + $handler->display->display_options['fields']['nid']['link_to_node'] = 0; + + return $view; + } +}