diff --git a/core/modules/config_translation/config_translation.admin.css b/core/modules/config_translation/config_translation.admin.css new file mode 100644 index 0000000..66d9ef3 --- /dev/null +++ b/core/modules/config_translation/config_translation.admin.css @@ -0,0 +1,38 @@ +/** + * @file + * Styles for configuration translation. + */ + +/** + * Hide the label, in an accessible way, for responsive screens which show the + * form in one column. + */ +.config-translation-form .translation-element-wrapper .translation label { + position: absolute !important; + clip: rect(1px, 1px, 1px, 1px); + overflow: hidden; + height: 1px; + width: 1px; +} + +/** + * For wider screens, show the label and display source and translation side by + * side. + */ +@media all and (min-width: 851px) { + .config-translation-form .translation-element-wrapper .source { + width: 48%; + float: left; + } + .config-translation-form .translation-element-wrapper .translation { + width: 48%; + float: right; + } + .config-translation-form .translation-element-wrapper .translation label { + position: static !important; + clip: auto; + overflow: visible; + height: auto; + width: auto; + } +} diff --git a/core/modules/config_translation/config_translation.api.php b/core/modules/config_translation/config_translation.api.php new file mode 100644 index 0000000..8878c34 --- /dev/null +++ b/core/modules/config_translation/config_translation.api.php @@ -0,0 +1,66 @@ +addName('othermodule.altered_settings_key'); + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/modules/config_translation/config_translation.info.yml b/core/modules/config_translation/config_translation.info.yml new file mode 100644 index 0000000..aebc7f4 --- /dev/null +++ b/core/modules/config_translation/config_translation.info.yml @@ -0,0 +1,8 @@ +name: 'Configuration Translation' +type: module +description: 'Provides a translation interface for configuration.' +package: Multilingual +version: VERSION +core: 8.x +dependencies: + - locale diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module new file mode 100644 index 0000000..cf17244 --- /dev/null +++ b/core/modules/config_translation/config_translation.module @@ -0,0 +1,276 @@ +' . t('About') . ''; + $output .= '

' . t('The Configuration Translation module lets you translate configuration from all around your Drupal site. Views, your site name, contact module categories, vocabularies, menus, blocks, and so on are all stored with Drupal\'s unified configuration system and can be translated with this module. Content, such as nodes, taxonomy terms, custom blocks, and so on are translatable with the Content translation module in Drupal core, while the built-in user interface (such as registration forms, content submission and administration interfaces) are translated with the Interface translation module. Use these three modules effectively together to translate your whole site to different languages.') . '

'; + return $output; + } +} + +/** + * Implements hook_menu(). + */ +function config_translation_menu() { + // Generate translation tabs for keys where a specific path can be + // determined. This makes it possible to use translation features in-context + // of the administration experience. + $config_groups = config_translation_get_groups(); + foreach ($config_groups as $group) { + $path = $group->getBasePath(); + if ($group->needsEditTab()) { + // For pages that do not have a default tab, we need a default local task + // on this level, so that the translate tab will show up. + $items[$path . '/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -100, + ); + } + $items[$path . '/translate'] = array( + 'title' => 'Translate', + 'route_name' => $group->getRouteName(), + 'type' => $group->getMenuItemType(), + 'weight' => 100, + ); + } + + return $items; +} + +/** + * Implements hook_permission(). + */ +function config_translation_permission() { + return array( + 'translate configuration' => array( + 'title' => t('Translate user edited configuration'), + 'description' => t('Translate any configuration not shipped with modules and themes.'), + ), + ); +} + +/** + * Implements hook_theme(). + */ +function config_translation_theme() { + return array( + 'config_translation_manage_form_element' => array( + 'render element' => 'element', + 'template' => 'config_translation_manage_form_element', + ), + ); +} + +/** + * Returns TRUE if a configuration key has schema coverage. + * + * @param string $name + * Configuration key. + * + * @return bool + * A boolean indicating if a configuration key has schema coverage. + */ +function config_translation_has_schema($name) { + // The schema system falls back on the Property class for unknown types. + // See http://drupal.org/node/1905230 + $definition = config_typed()->getDefinition($name); + return is_array($definition) && $definition['class'] != '\Drupal\Core\Config\Schema\Property'; +} + +/** + * Returns TRUE if the configuration data has translatable items. + * + * @param string $name + * Configuration key. + * + * @return bool + * A boolean indicating if the configuration data has translatable items. + */ +function config_translation_has_translatable($name) { + return config_translation_find_translatable(config_typed()->get($name)); +} + +/** + * Returns TRUE if at least one translatable element is found. + * + * @todo Make this conditional on the translatable schema property from + * http://drupal.org/node/1905152 - currently hardcodes label and text. + * + * @param mixed $schema + * Configuration schema definition which is a + * \Drupal\Core\Config\Schema\Element or an array of Elements. + * + * @return bool + * A boolean indicating if there is at least one translatable element. + */ +function config_translation_find_translatable($schema) { + foreach ($schema as $element) { + if ($element instanceof Element && config_translation_find_translatable($element)) { + return TRUE; + } + else{ + $definition = $element->getDefinition(); + if (isset($definition['translatable'])) { + return TRUE; + } + } + } + return FALSE; +} + +/** + * Retrieves original submission language code for configuration object. + * + * @param string $name + * Configuration key. + * + * @return string + * Language code string. + */ +function config_translation_original_langcode($name) { + $config = config($name)->get(); + return isset($config['langcode']) ? $config['langcode'] : 'en'; +} + +/** + * Checks whether a language has configuration translation. + * + * @param string $name + * Configuration name. + * @param \Drupal\Core\Language\Language $language + * A language object. + * + * @return bool + * A boolean indicating if a language has configuration translations. + */ +function config_translation_exists($name, Language $language) { + $config_translation = config('locale.config.' . $language->id . '.' . $name)->get(); + return !empty($config_translation); +} + +/** + * Gets group definitions from hooks and make it possible to alter groups. + * + * Configuration groups are used to get multiple configuration names used for + * one specific configuration form together. If contributed modules alter a + * form adding in additional settings stored elsewhere, the list of names + * can be expanded. + * + * @return array + * An array of configuration groups. + */ +function config_translation_get_groups() { + $config_groups = Drupal::ModuleHandler()->invokeAll('config_translation_group_info'); + + // Create an array of path indexed groups for easier altering. + $path_indexed_groups = array(); + foreach ($config_groups as $group) { + $path_indexed_groups[$group->getBasePath()] = $group; + } + Drupal::ModuleHandler()->alter('config_translation_group_info', $path_indexed_groups); + return $path_indexed_groups; +} + +/** + * Implements hook_config_translation_group_info(). + * + * Returns configuration name group mappings for core configuration files. + */ +function config_translation_config_translation_group_info() { + $items = array(); + + // Block. + $items[] = new ConfigEntityMapper('admin/structure/block/manage/{block}', 'block', t('@label block')); + + // Custom block. + $items[] = new ConfigEntityMapper('admin/structure/custom-blocks/manage/{custom_block_type}', 'custom_block_type', t('@label custom block type')); + + // Contact. + $items[] = new ConfigEntityMapper('admin/structure/contact/manage/{contact_category}', 'contact_category', t('@label contact category')); + + // Content types. + $items[] = new ConfigEntityMapper('admin/structure/types/manage/{node_type}', 'node_type', t('@label content type'), MENU_CALLBACK); + + // Date format. + $items[] = new ConfigEntityMapper('admin/config/regional/date-time/formats/manage/{date_format}', 'date_format', t('@label date format')); + + // Fields. + $entity_manager = Drupal::entityManager(); + foreach ($entity_manager->getDefinitions() as $entity_type => $entity_info) { + // Make sure entity type is fieldable and has base path. + if ($entity_info['fieldable'] && isset($entity_info['route_base_path'])) { + // Get all available bundles available for this entity type. + foreach (entity_get_bundles($entity_type) as $bundle => $bundle_info) { + if ($path = $entity_manager->getAdminPath($entity_type, $bundle)) { + $items[] = new ConfigEntityMapper($path . '/fields/{field_instance}' , 'field_instance', t('@label field'), MENU_CALLBACK); + } + } + } + } + + // Filter. + $items[] = new ConfigEntityMapper('admin/config/content/formats/manage/{filter_format}', 'filter_format', t('@label text format'), MENU_LOCAL_TASK, TRUE); + + // Images. + $items[] = new ConfigEntityMapper('admin/config/media/image-styles/manage/{image_style}', 'image_style', t('@label image style'), MENU_LOCAL_TASK); + + // Language. + $items[] = new ConfigEntityMapper('admin/config/regional/language/edit/{language_entity}', 'language_entity', t('@label language'), MENU_CALLBACK); + + // Menu. + $items[] = new ConfigEntityMapper('admin/structure/menu/manage/{menu}', 'menu', t('@label menu')); + + // Picture. + $items[] = new ConfigEntityMapper('admin/config/media/picturemapping/{picture_mapping}', 'picture_mapping', t('@label picture mapping'), MENU_LOCAL_TASK); + + // Shortcut. + $items[] = new ConfigEntityMapper('admin/config/user-interface/shortcut/manage/{shortcut_set}', 'shortcut_set', t('@label shortcut set')); + + // System. + $items[] = new ConfigGroupMapper('admin/config/development/maintenance', t('System maintenance'), array('system.maintenance'), MENU_LOCAL_TASK, TRUE); + $items[] = new ConfigGroupMapper('admin/config/system/site-information', t('Site information'), array('system.site'), MENU_LOCAL_TASK, TRUE); + $items[] = new ConfigGroupMapper('admin/config/services/rss-publishing', t('RSS publishing'), array('system.rss'), MENU_LOCAL_TASK, TRUE); + + // Taxonomy. + $items[] = new ConfigEntityMapper('admin/structure/taxonomy/manage/{taxonomy_vocabulary}', 'taxonomy_vocabulary', t('@label vocabulary')); + + // User. + $items[] = new ConfigGroupMapper('admin/config/people/accounts', t('Account settings'), array('user.settings', 'user.mail')); + $items[] = new ConfigEntityMapper('admin/people/roles/manage/{user_role}', 'user_role', t('@label user role'), MENU_LOCAL_TASK, TRUE); + + // Views. + $items[] = new ConfigEntityMapper('admin/structure/views/view/{view}', 'view', t('@label view'), MENU_CALLBACK); + + return $items; +} + +/** + * Implements hook_entity_operation_alter(). + */ +function config_translation_entity_operation_alter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity) { + if (user_access('translate configuration')) { + $uri = $entity->uri(); + $operations['translate'] = array( + 'title' => t('Translate'), + 'href' => $uri['path'] . '/translate', + 'options' => $uri['options'], + 'weight' => 50, + ); + } +} diff --git a/core/modules/config_translation/config_translation.services.yml b/core/modules/config_translation/config_translation.services.yml new file mode 100644 index 0000000..68fc96a --- /dev/null +++ b/core/modules/config_translation/config_translation.services.yml @@ -0,0 +1,9 @@ +services: + config_translation.subscriber: + class: Drupal\config_translation\Routing\RouteSubscriber + tags: + - { name: event_subscriber } + config_translation.access_check: + class: Drupal\config_translation\Access\ConfigNameCheck + tags: + - { name: access_check } diff --git a/core/modules/config_translation/core/modules/config_translation/config_translation.admin.css b/core/modules/config_translation/core/modules/config_translation/config_translation.admin.css new file mode 100644 index 0000000..66d9ef3 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/config_translation.admin.css @@ -0,0 +1,38 @@ +/** + * @file + * Styles for configuration translation. + */ + +/** + * Hide the label, in an accessible way, for responsive screens which show the + * form in one column. + */ +.config-translation-form .translation-element-wrapper .translation label { + position: absolute !important; + clip: rect(1px, 1px, 1px, 1px); + overflow: hidden; + height: 1px; + width: 1px; +} + +/** + * For wider screens, show the label and display source and translation side by + * side. + */ +@media all and (min-width: 851px) { + .config-translation-form .translation-element-wrapper .source { + width: 48%; + float: left; + } + .config-translation-form .translation-element-wrapper .translation { + width: 48%; + float: right; + } + .config-translation-form .translation-element-wrapper .translation label { + position: static !important; + clip: auto; + overflow: visible; + height: auto; + width: auto; + } +} diff --git a/core/modules/config_translation/core/modules/config_translation/config_translation.api.php b/core/modules/config_translation/core/modules/config_translation/config_translation.api.php new file mode 100644 index 0000000..e6ca187 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/config_translation.api.php @@ -0,0 +1,66 @@ +addName('othermodule.altered_settings_key'); + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/modules/config_translation/core/modules/config_translation/config_translation.info.yml b/core/modules/config_translation/core/modules/config_translation/config_translation.info.yml new file mode 100644 index 0000000..aebc7f4 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/config_translation.info.yml @@ -0,0 +1,8 @@ +name: 'Configuration Translation' +type: module +description: 'Provides a translation interface for configuration.' +package: Multilingual +version: VERSION +core: 8.x +dependencies: + - locale diff --git a/core/modules/config_translation/core/modules/config_translation/config_translation.module b/core/modules/config_translation/core/modules/config_translation/config_translation.module new file mode 100644 index 0000000..520b108 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/config_translation.module @@ -0,0 +1,382 @@ +' . t('About') . ''; + $output .= '

' . t('The Configuration Translation module lets you translate configuration from all around your Drupal site. Views, your site name, contact module categories, vocabularies, menus, blocks, and so on are all stored with Drupal\'s unified configuration system and can be translated with this module. Content, such as nodes, taxonomy terms, custom blocks, and so on are translatable with the Content translation module in Drupal core, while the built-in user interface (such as registration forms, content submission and administration interfaces) are translated with the Interface translation module. Use these tthree modules effectively together to translate your whole site to different languages.') . '

'; + return $output; + } +} + +/** + * Implements hook_menu(). + */ +function config_translation_menu() { + // Generate translation tabs for keys where a specific path can be + // determined. This makes it possible to use translation features in-context + // of the administration experience. + $config_groups = config_translation_get_groups(); + foreach ($config_groups as $group) { + $path = $group->getBasePath(); + if ($group->hasEditTab()) { + // For pages that do not have a default tab, we need a default local task + // on this level, so that the translate tab will show up. + // @todo This is not very compatible with possible other alters. Look + // into other clever ways to make this happen, + // http://drupal.org/node/2011332. + $items[$path . '/edit'] = array( + 'title' => 'Edit', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -100, + ); + } + $items[$path . '/translate'] = array( + 'title' => 'Translate', + 'route_name' => $group->getRouteName(), + 'type' => $group->getMenuType(), + 'weight' => 100, + ); + $items[$path . '/translate/add/%language'] = array( + 'title' => 'Translate', + 'route_name' => $group->getRouteName() . '.add', + 'type' => MENU_CALLBACK, + ); + $items[$path . '/translate/edit/%language'] = array( + 'title' => 'Translate', + 'route_name' => $group->getRouteName() . '.edit', + 'type' => MENU_CALLBACK, + ); + $items[$path . '/translate/delete/%language'] = array( + 'title' => 'Delete', + 'route_name' => $group->getRouteName() . '.delete', + 'type' => MENU_CALLBACK, + ); + } + + return $items; +} + +/** + * Implements hook_permission(). + */ +function config_translation_permission() { + return array( + 'translate configuration' => array( + 'title' => t('Translate user edited configuration'), + 'description' => t('Translate any configuration not shipped with modules and themes.'), + ), + ); +} + +/** + * Implements hook_theme(). + */ +function config_translation_theme() { + return array( + 'config_translation_manage_form_element' => array( + 'render element' => 'element', + ), + ); +} + +/** + * Returns HTML for translation manage element. + * + * @param array $variables + * An associative array containing: + * - element: The element that contains the source and translation childs + * + * @see ConfigTranslationManageForm::buildConfigForm + * @ingroup themeable + */ +function theme_config_translation_manage_form_element($variables){ + $element = $variables['element']; + $result = ' +
+
+ ' . drupal_render($element['source']) . ' +
+
+ ' . drupal_render($element['translation']) . ' +
+
+ '; + return $result; +} + +/** + * Returns TRUE if a configuration key has schema coverage. + * + * @param string $name + * Configuration key. + * + * @return bool + * A boolean indicating if a configuration key has schema coverage. + */ +function config_translation_has_schema($name) { + // The schema system falls back on the Property class for unknown types. + // See http://drupal.org/node/1905230 + $definition = config_typed()->getDefinition($name); + return is_array($definition) && $definition['class'] != '\Drupal\Core\Config\Schema\Property'; +} + +/** + * Returns TRUE if the configuration data has translatable items. + * + * @param string $name + * Configuration key. + * + * @return bool + * A boolean indicating if the configuration data has translatable items. + */ +function config_translation_has_translatable($name) { + return config_translation_find_translatable(config_typed()->get($name)); +} + +/** + * Returns TRUE if at least one translatable element is found. + * + * @todo Make this conditional on the translatable schema property from + * http://drupal.org/node/1905152 - currently hardcodes label and text. + * + * @param mixed $schema + * Configuration schema definition which is a + * \Drupal\Core\Config\Schema\Element or an array of Elements. + * + * @return bool + * A boolean indicating if there is at least one translatable element. + */ +function config_translation_find_translatable($schema) { + foreach ($schema as $element) { + if ($element instanceof Element && config_translation_find_translatable($element)) { + return TRUE; + } + elseif (in_array($element->getType(), array('label', 'text'))) { + return TRUE; + } + } + return FALSE; +} + +/** + * Retrieves original submission language code for configuration object. + * + * @param string $name + * Configuration key. + * + * @return string + * Language code string. + */ +function config_translation_original_langcode($name) { + $config = config($name)->get(); + return isset($config['langcode']) ? $config['langcode'] : 'en'; +} + +/** + * Checks whether a language has config translation. + * + * @param string $name + * Configuration name. + * @param \Drupal\Core\Language\Language $language + * A language object. + * + * @return bool + * A boolean indicating if a language has config translations. + */ +function config_translation_exists($name, Language $language) { + $config_translation = config('locale.config.' . $language->langcode . '.' . $name)->get(); + return !empty($config_translation); +} + +/** + * Gets group definitions from hooks and make it possible to alter groups. + * + * Configuration groups are used to get multiple configuration names used for + * one specific configuration form together. If contributed modules alter a + * form adding in additional settings stored elsewhere, the list of names + * can be expanded. + * + * @return array + * An array of configuration groups. + */ +function config_translation_get_groups() { + $config_groups = Drupal::ModuleHandler()->invokeAll('config_translation_group_info'); + + // Create an array of path indexed groups for easier altering. + $path_indexed_groups = array(); + foreach ($config_groups as $group) { + $path_indexed_groups[$group->getBasePath()] = $group; + } + Drupal::ModuleHandler()->alter('config_translation_group_info', $path_indexed_groups); + return $path_indexed_groups; +} + +/** + * Implements hook_config_translation_group_info(). + * + * Returns configuration name group mappings for core configuration files. + */ +function config_translation_config_translation_group_info() { + $items = array(); + + // Block. + $items[] = new ConfigEntityMapper('admin/structure/block/manage/{block}', 4, 'block', t('@label block'), 'block.block'); + + // Custom block. + $items[] = new ConfigEntityMapper('admin/structure/custom-blocks/manage/{custom_block_type}', 4, 'custom_block_type', t('@label custom block type'), 'custom_block.type'); + + // Contact. + $items[] = new ConfigEntityMapper('admin/structure/contact/manage/{contact_category}', 4, 'contact_category', t('@label contact category'), 'contact.category'); + + // Filter. + $items[] = new ConfigEntityMapper('admin/config/content/formats/{filter_format}', 4, 'filter_format', t('@label text format'), 'filter.format', MENU_LOCAL_TASK, TRUE); + + // Menu. + $items[] = new ConfigEntityMapper('admin/structure/menu/manage/{menu}', 4, 'menu', t('@label menu'), 'menu.menu'); + + // Shortcut. + $items[] = new ConfigEntityMapper('admin/config/user-interface/shortcut/manage/{shortcut_set}', 5, 'shortcut', t('@label shortcut set'), 'shortcut.set'); + + // System. + $items[] = new ConfigGroupMapper('admin/config/development/maintenance', t('System maintenance'), array('system.maintenance'), TRUE); + $items[] = new ConfigGroupMapper('admin/config/system/site-information', t('Site information'), array('system.site'), MENU_LOCAL_TASK, TRUE); + + // Taxonomy. + $items[] = new ConfigEntityMapper('admin/structure/taxonomy/manage/{taxonomy_vocabulary}', 4, 'taxonomy_vocabulary', t('@label vocabulary'), 'taxonomy.vocabulary'); + + // User. + $items[] = new ConfigGroupMapper('admin/config/people/accounts', t('Account settings'), array('user.settings', 'user.mail')); + $items[] = new ConfigEntityMapper('admin/people/roles/edit/{user_role}', 4, 'user_role', t('@label user role'), 'user.role', MENU_LOCAL_TASK, TRUE); + + // Views. + $items[] = new ConfigEntityMapper('admin/structure/views/view/{view}', 4, 'view', t('@label view'), 'views.view', MENU_CALLBACK); + + return $items; +} + +/** + * Implements hook_entity_operation_alter(). + */ +function config_translation_entity_operation_alter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity) { + $uri = $entity->uri(); + $operations['translate'] = array( + 'title' => t('Translate badabumm'), + 'href' => $uri['path'] . '/translate', + 'options' => $uri['options'], + 'weight' => 50, + ); +} + +/** + * Alters form operations since there is no operations API. + * + * @todo Rework this in favor of an operations API. See + * http://drupal.org/node/2004428 + */ +function config_translation_form_alter(&$form, &$form_state, $form_id) { + if (!user_access('translate configuration')) { + // Do not alter forms with links to translations, if no access. + return; + } + + // Alter the filter admin overview to add translation links. + if ($form_id == 'filter_admin_overview') { + foreach ($form['formats'] as $id => &$format) { + if (is_array($format) && isset($format['operations'])) { + $format['operations']['#links']['translate'] = array( + 'title' => t('Translate'), + 'href' => 'admin/config/content/formats/' . $id . '/translate', + ); + } + } + } + + // Alter the user roles form to add translation links. + if ($form_id == 'user_admin_roles') { + foreach ($form['roles'] as $id => &$role) { + if (is_array($role) && isset($role['operations'])) { + $role['operations']['#links']['translate'] = array( + 'title' => t('Translate'), + 'href' => 'admin/people/roles/edit/' . $id . '/translate', + ); + } + } + } + + // Alter the blocks form to add translation links. + if ($form_id == 'block_admin_display_form') { + foreach ($form['blocks'] as $id => &$block) { + if (is_array($block) && isset($block['operations'])) { + $block['operations']['#links']['translate'] = array( + 'title' => t('Translate'), + 'href' => 'admin/structure/block/manage/' . $id . '/translate', + ); + } + } + } +} + +/** + * Alters page operations since there is no operations API. + * + * @todo Rework this in favor of an operations API. See + * http://drupal.org/node/2004428 + */ +function config_translation_page_alter(&$page) { + if (!user_access('translate configuration')) { + // Do not alter pages with links to translations, if no access. + return; + } + + if (isset($page['content'])) { + // Grab the first content item, which is the content block hopefully. + $content_keys = array_keys($page['content']); + $content = &$page['content'][reset($content_keys)]; + + // Fingerprint the views entity list page structure. + if (isset($content['content'][0]['#attributes']['id']) && + $content['content'][0]['#attributes']['id'] == 'views-entity-list') { + $tables = &$content['content'][0]; + foreach (array('enabled', 'disabled') as $status) { + foreach ($tables[$status]['table']['#rows'] as $id => &$row) { + $row['data']['operations']['data']['#links']['translate'] = array( + 'title' => t('Translate'), + 'href' => 'admin/structure/views/view/' . $id . '/translate', + ); + } + } + } + + if (isset($content['content'][0]['#rows'])) { + $row_keys = array_keys($content['content'][0]['#rows']); + $first_key = reset($row_keys); + $rows = &$content['content'][0]['#rows']; + + // Fingerprint the menu summary table structure. + if (isset($rows[$first_key]['title']['class']) && + in_array('menu-label', $rows[$first_key]['title']['class'])) { + foreach ($rows as $id => &$row) { + $row['operations']['data']['#links']['translate'] = array( + 'title' => t('Translate'), + 'href' => 'admin/structure/menu/manage/' . $id . '/translate', + ); + } + } + } + } +} diff --git a/core/modules/config_translation/core/modules/config_translation/config_translation.services.yml b/core/modules/config_translation/core/modules/config_translation/config_translation.services.yml new file mode 100644 index 0000000..68fc96a --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/config_translation.services.yml @@ -0,0 +1,9 @@ +services: + config_translation.subscriber: + class: Drupal\config_translation\Routing\RouteSubscriber + tags: + - { name: event_subscriber } + config_translation.access_check: + class: Drupal\config_translation\Access\ConfigNameCheck + tags: + - { name: access_check } diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php new file mode 100644 index 0000000..1af1390 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php @@ -0,0 +1,54 @@ +getRequirements()); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request) { + $mapper = $route->getDefault('mapper'); + $entity = $request->attributes->get($mapper->getType()); + // Get configuration group for this mapper. + $group = $mapper->getConfigGroup($entity); + $group_language = $group->getLanguageWithFallback(); + + $langcode = $request->attributes->get('langcode'); + $language = empty($langcode) ? NULL : language_load($langcode); + + // Only allow access to translate configuration, if proper permissions are + // granted, the configuration has translatable pieces, the source language + // and target language are not locked, and the target language is not the + // original submission language. Although technically config can be + // overlayed with translations in the same language, that is logically not + // a good idea. + return ( + user_access('translate configuration') && + $group->hasSchema() && + $group->hasTranslatable() && + !$group_language->locked && + (empty($language) || (!$language->locked && $language->langcode != $group_language->langcode)) + ); + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php new file mode 100644 index 0000000..f7dba2e --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php @@ -0,0 +1,188 @@ +base_path = $base_path; + $this->path_id_index = $path_id_index; + $this->entity_type = $entity_type; + $this->title = $title; + $this->config_prefix = $config_prefix; + $this->menu_type = $menu_type; + $this->add_edit_tab = $add_edit_tab; + $this->setRouteName(); + } + + /** + * {@inheritdoc} + */ + public function hasEditTab() { + return $this->add_edit_tab; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return $this->base_path; + } + + /** + * {@inheritdoc} + */ + public function getMenuType() { + return $this->menu_type; + } + + /** + * {@inheritdoc} + */ + public function getPathIdIndex() { + return $this->path_id_index; + } + + /** + * {@inheritdoc} + */ + public function getConfigGroup($arg = NULL) { + // In this implementation, $arg is the config entity loaded via the menu + // (for contact module for example) or a string (config entity ID), in + // which case we need to load the entity (for views for example). + if (!isset($arg)) { + return NULL; + } + if (is_string($arg)) { + $entity = entity_load($this->entity_type, $arg); + } + else { + $entity = $arg; + } + + // Replace path segment with the ID of the entity for further processing. + $path = explode('/', $this->base_path); + $path[$this->path_id_index] = $entity->id(); + $base_path = join('/', $path); + + // Replace entity label in template title. + $title = format_string($this->title, array('@label' => $entity->label())); + + // The list of config IDs belonging to this entity. + $names = array($this->config_prefix . '.' . $entity->id()); + + return new ConfigGroupMapper($base_path, $title, $names, $this->menu_type, $this->add_edit_tab); + } + + /** + * {@inheritdoc} + */ + public function setRouteName() { + $search = array('/', '-', '{', '}'); + $replace = array('.', '_', '_', '_'); + $this->route_name = 'config_translation.item.' . str_replace($search, $replace, $this->base_path); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->route_name; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->entity_type; + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php new file mode 100644 index 0000000..8ab5f3f --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php @@ -0,0 +1,263 @@ +base_path = $base_path; + $this->title = $title; + $this->names = $names; + $this->menu_type = $menu_type; + $this->add_edit_tab = $add_edit_tab; + $this->setRouteName(); + } + + /** + * {@inheritdoc} + */ + public function hasEditTab() { + return $this->add_edit_tab; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return $this->base_path; + } + + /** + * Returns title of this configuration group. + * + * @return string + * The group title. + */ + public function getTitle() { + return $this->title; + } + + /** + * Returns list of names in this configuration group. + * + * @return array + * An array of configuration names in this group. + */ + public function getNames() { + return $this->names; + } + + /** + * {@inheritdoc} + */ + public function getMenuType() { + return $this->menu_type; + } + + /** + * {@inheritdoc} + */ + public function getPathIdIndex() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getConfigGroup($arg = NULL) { + // This is already a config group. + return $this; + } + + /** + * Returns the original language code of the configuration group. + * + * @todo Revisit. This assumes the langcode of the first element for now and + * might lead to inconsistency. + */ + public function getLangcode() { + return config_translation_original_langcode($this->names[0]); + } + + /** + * Returns language object for the configuration group. + * + * If the language of the group is not a configured language on the site and + * it is English, we return a dummy language object to represent the + * built-in language. + * + * @return \Drupal\Core\Language\Language + * A configured language object instance or a dummy English language object. + */ + public function getLanguageWithFallback() { + $langcode = $this->getLangcode(); + $language = language_load($langcode); + if (empty($language) && $langcode == 'en') { + $language = new Language(array('langcode' => 'en', 'name' => t('Built-in English'))); + } + return $language; + } + + /** + * Returns an array with configuration data for the group. + * + * @return array + * Configuration data keyed by configuration names in the group. + */ + public function getConfigData() { + $config_data = array(); + foreach ($this->names as $name) { + $config_data[$name] = config($name)->get(); + } + return $config_data; + } + + /** + * Checks that all pieces of this configuration group has schema. + * + * @return bool + * TRUE if all of the group elements have schema, FALSE otherwise. + */ + public function hasSchema() { + foreach ($this->names as $name) { + if (!config_translation_has_schema($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * Checks that all pieces of this configuration group have translatables. + * + * @return bool + * TRUE if all of the group elements have translatables, FALSE otherwise. + */ + public function hasTranslatable() { + foreach ($this->names as $name) { + if (!config_translation_has_translatable($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * Checks whether there is already a translation for this group. + * + * @param \Drupal\Core\Language\Language $language + * A language object. + * + * @return bool + * TRUE if any of the group elements have a translation in the given + * language, FALSE otherwise. + */ + public function hasTranslation(Language $language) { + foreach ($this->names as $name) { + if (config_translation_exists($name, $language)) { + return TRUE; + } + } + return FALSE; + } + + /** + * Adds the given configuration name to the list of names in the group. + * + * @param string $name + * Configuration name. + */ + public function addName($name) { + $this->names[] = $name; + } + + /** + * {@inheritdoc} + */ + public function setRouteName() { + $this->route_name = 'config_translation.item.' . str_replace(array('/', '-'), array('.', '_'), $this->base_path); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->route_name; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return NULL; + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php new file mode 100644 index 0000000..4a0c4b1 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php @@ -0,0 +1,76 @@ +get('database')); + } + + /** + * Language translations overview page for a configuration name. + * + * @param Request $request + * Page request object. + * @param ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return array + * Page render array. + */ + public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper) { + $group = $this->getConfigGroup($request, $mapper); + drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH); + + // It is possible the original language this configuration was saved with is + // not on the system. For example, the configuration shipped in English but + // the site has no English configured. Represent the original language in the + // table even if it is not currently configured. + $languages = language_list(); + $original_langcode = $group->getLangcode(); + if (!isset($languages[$original_langcode])) { + $language_name = language_name($original_langcode); + if ($original_langcode == 'en') { + $language_name = t('Built-in English'); + } + // Create a dummy language object for this listing only. + $languages[$original_langcode] = new Language(array('langcode' => $original_langcode, 'name' => $language_name)); + } + + $path = $group->getBasePath(); + $header = array(t('Language'), t('Operations')); + $page = array(); + $page['languages'] = array( + '#type' => 'table', + '#header' => $header, + ); + foreach ($languages as $language) { + if ($language->langcode == $original_langcode) { + $page['languages'][$language->langcode]['language'] = array( + '#markup' => '' . t('@language (original)', array('@language' => $language->name)) . '' + ); + + // @todo: the user translating might as well not have access to + // edit the original configuration. They will get a 403 for this + // link when clicked. Do we know better? + $operations = array(); + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path, + ); + $page['languages'][$language->langcode]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + else { + $page['languages'][$language->langcode]['language'] = array( + '#markup' => $language->name, + ); + $operations = array(); + + // Check if translation exist for this language. + if (!$group->hasTranslation($language)) { + $operations['add'] = array( + 'title' => t('Add'), + 'href' => $path . '/translate/add/' . $language->langcode, + ); + } + else { + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path . '/translate/edit/' . $language->langcode, + ); + $operations['delete'] = array( + 'title' => t('Delete'), + 'href' => $path . '/translate/delete/' . $language->langcode, + ); + } + $page['languages'][$language->langcode]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + } + return $page; + } + + /** + * Renders translation item manage form. + * + * @param Request $request + * Page request object. + * @param string $action + * Action identifier, either 'add' or 'edit'. Used to provide proper + * labeling on the screen. + * @param ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return array + */ + public function itemTranslatePage(Request $request, $action, ConfigMapperInterface $mapper) { + $group = $this->getConfigGroup($request, $mapper); + $language = $this->getLanguage($request, $mapper); + + $replacement = array( + '%label' => $group->getTitle(), + '@language' => strtolower($language->name), + ); + switch ($action) { + case 'add': + drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH); + break; + case 'edit': + drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH); + break; + } + + // Make sure we are in the override free config context. For example, + // visiting the configuration page in another language would make those + // language overrides active by default. But we need the original values. + config_context_enter('config.context.free'); + + // Get base language configuration to display in the form before entering + // into the language context for the form. This avoids repetitively going + // in and out of the language context to get original values later. + $base_config = $group->getConfigData(); + + // Leave override free context. + config_context_leave(); + + // Enter context for the translation target language requested and generate + // form with translation data in that language. + config_context_enter('Drupal\language\LanguageConfigContext')->setLanguage($language); + $locale_storage = \Drupal::service('locale.storage'); + return drupal_get_form(new ConfigTranslationManageForm($locale_storage), $group, $language, $base_config); + } + + /** + * Item delete form. + * + * @param Request $request + * @param ConfigMapperInterface $mapper + * @return array|mixed + */ + public function itemDeletePage(Request $request, ConfigMapperInterface $mapper) { + return drupal_get_form(new ConfigTranslationDeleteForm(), $this->getConfigGroup($request, $mapper), $this->getLanguage($request, $mapper)); + } + + /** + * Helper to get config group. + * + * @param Request $request + * @param ConfigMapperInterface $mapper + * + * @return ConfigGroupMapper + */ + protected function getConfigGroup(Request $request, ConfigMapperInterface $mapper) { + // Get configuration group for this mapper. + $entity = $request->attributes->get($mapper->getType()); + return $mapper->getConfigGroup($entity); + } + + /** + * Helper to get language object. + * + * @param Request $request + * @param ConfigMapperInterface $mapper + * + * @return bool|\Drupal\core\Language\Language + * Returns Language object when langcode found in request; FALSE otherwise. + */ + protected function getLanguage(Request $request, ConfigMapperInterface $mapper) { + $langcode = $request->attributes->get('langcode'); + if ($langcode) { + return language_load($langcode); + } + return FALSE; + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php new file mode 100644 index 0000000..29cf0f5 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php @@ -0,0 +1,91 @@ + $this->group->getTitle(), '@language' => $this->language->name)); + + } + + /** + * {@inheritdoc} + */ + protected function getConfirmText() { + return t('Delete'); + } + + + /** + * {@inheritdoc} + */ + protected function getCancelPath() { + return $this->group->getBasePath() . '/translate'; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'config_translation_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, $language = NULL) { + $this->group = $group; + $this->language = $language; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + + $form_values = $form_state['values']; + + $storage = drupal_container()->get('config.storage'); + foreach ($this->group->getNames() as $name) { + $storage->delete('locale.config.' . $this->language->langcode . '.' . $name); + } + // @todo: do we need to flush caches? The config change may affect page display. + // drupal_flush_all_caches(); + + drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->group->getTitle(), '@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php new file mode 100644 index 0000000..e79ec9f --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php @@ -0,0 +1,299 @@ +localeStorage = $locale_storage; + } + + /** + * {@inheritdoc}. + */ + public function getFormID() { + return 'config_translation_form'; + } + + /** + * Build configuration form with metadata and values. + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, Language $language = NULL, array $base_config = NULL) { + $this->group = $group; + $this->language = $language; + $this->source_language = $this->group->getConfigGroup()->getLanguageWithFallback(); + $this->base_config = $base_config; + + foreach ($this->group->getNames() as $id => $name) { + $form[$id] = array( + '#type' => 'container', + '#tree' => TRUE, + ); + $form[$id] += $this->buildConfigForm(config_typed()->get($name), config($name)->get(), $this->base_config[$name]); + } + + $form['#attached']['css'] = array(drupal_get_path('module', 'config_translation') . '/config_translation.admin.css'); + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translation')); + return $form; + } + + /** + * @inheritdoc + */ + public function validateForm(array &$form, array &$form_state) { + + } + + /** + * @inheritdoc + */ + public function submitForm(array &$form, array &$form_state) { + $form_values = $form_state['values']; + + // For the form submission handling, use the override free context. + config_context_enter('config.context.free'); + + foreach ($this->group->getNames() as $id => $name) { + // Set config values based on form submission and original values. + $base_config = config($name); + $translation_config = config('locale.config.' . $this->language->langcode . '.' . $name); + $locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $name)); + + $this->setConfig($this->language, $base_config, $translation_config, $form_values[$id], !empty($locations)); + + // If no overrides, delete language specific config file. + $saved_config = $translation_config->get(); + if (empty($saved_config)) { + $translation_config->delete(); + } + else { + $translation_config->save(); + } + } + + config_context_leave(); + + drupal_set_message(t('Updated @language configuration translations successfully.', array('@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + + /** + * Formats configuration schema as a form tree. + * + * @param $schema + * Schema definition of configuration. + * @param $config + * Configuration object of requested language. + * @param $base_config + * Configuration object of base language. + * @param bool $collapsed + * Flag to set collapsed state. + * @param string|NULL $base_key + * Base configuration key. + * + * @return array + * An associative array containing the structure of the form. + * + * @todo + * Make this conditional on the translatable schema property from + * http://drupal.org/node/1905152 - currently hardcodes label and text. + */ + protected function buildConfigForm($schema, $config, $base_config, $collapsed = FALSE, $base_key = '') { + $build = array(); + foreach ($schema as $key => $element) { + $element_key = implode('.', array_filter(array($base_key, $key))); + $definition = $element->getDefinition() + array('label' => t('N/A')); + if ($element instanceof Element) { + // Build sub-structure and include it with a wrapper in the form + // if there are any translatable elements there. + $sub_build = $this->buildConfigForm($element, $config[$key], $base_config[$key], TRUE, $element_key); + if (!empty($sub_build)) { + // For some configuration elements the same element structure can + // repeat multiple times, (like views displays, filters, etc.). + // So try to find a more usable title for the details summary. First + // check if there is an element which is called title or label, then + // check if there is an element which contains these words. + $title = ''; + if (isset($sub_build['title']['source'])) { + $title = $sub_build['title']['source']['#markup']; + } + elseif (isset($sub_build['label']['source'])) { + $title = $sub_build['label']['source']['#markup']; + } + else { + foreach (array_keys($sub_build) as $title_key) { + if (isset($sub_build[$title_key]['source']) && (strpos($title_key, 'title') !== FALSE || strpos($title_key, 'label') !== FALSE)) { + $title = $sub_build[$title_key]['source']['#markup']; + break; + } + } + } + $build[$key] = array( + '#type' => 'details', + '#title' => (!empty($title) ? (strip_tags($title) . ' ') : '') . t($definition['label']), + '#collapsible' => TRUE, + '#collapsed' => $collapsed, + ) + $sub_build; + } + } + else { + $type = $element->getType(); + switch ($type) { + case 'label': + $type = 'textfield'; + break; + case 'text': + $type = 'textarea'; + break; + default: + continue(2); + break; + } + $value = $config[$key]; + $build[$element_key] = array( + '#theme' => 'config_translation_manage_form_element', + ); + $build[$element_key]['source'] = array( + '#markup' => $base_config[$key] ? ('' . nl2br($base_config[$key] . '')) : t('(Empty)'), + '#title' => t($definition['label']) . ' ('. $this->source_language->name . ')', + '#type' => 'item', + ); + $rows_words = ceil(str_word_count($value) / 5); + $rows_newlines = substr_count($value, "\n" ) + 1; + $rows = max($rows_words, $rows_newlines); + $build[$element_key]['translation'] = array( + '#type' => $type, + '#default_value' => $value, + '#title' => t($definition['label']) . ' (' . $this->language->name . ')', + '#rows' => $rows, + '#attributes' => array('lang' => $this->language->langcode), + ); + } + } + return $build; + } + + /** + * Sets configuration based on a nested form value array. + * + * @param Language $language + * Language object. + * @param \Drupal\Core\Config\Config $base_config + * Base language configuration values instance. + * @param \Drupal\Core\Config\Config $translation_config + * Translation configuration instance. Values from $config_values will be set + * in this instance. + * @param array $config_values + * A simple one dimensional or recursive array: + * - simple: + * array(name => array('translation' => 'French site name')) + * - recursive: + * cancel_confirm => array( + * cancel_confirm.subject => array('translation' => 'Subject') + * cancel_confirm.body => array('translation' => 'Body content') + * ); + * Either format is used, the nested arrays are just containers and not + * needed for saving the data. + * @param bool $shipped_config + * Flag to specify whether the configuration had a shipped version and + * therefore should also be stored in the locale database. + */ + protected function setConfig(Language $language, Config $base_config, Config $translation_config, array $config_values, $shipped_config = FALSE) { + //dpm($config_values); + foreach ($config_values as $key => $value) { + if (is_array($value) && !isset($value['translation'])) { + // Traverse into this level in the configuration. + $this->setConfig($language, $base_config, $translation_config, $value, $shipped_config); + } + else { + + // If the config file being translated was originally shipped, we should + // update the locale translation storage. The string should already be + // there, but we make sure to check. + if ($shipped_config && $source_string = $this->localeStorage->findString(array('source' => $base_config->get($key)))) { + + // Get the translation for this original source string from locale. + $conditions = array( + 'lid' => $source_string->lid, + 'language' => $language->langcode, + ); + $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE)); + // If we got a translation, take that, otherwise create a new one. + $translation = reset($translations) ?: $this->localeStorage->createTranslation($conditions); + + // If we have a new translation or different from what is stored in + // locale before, save this as an updated customize translation. + if ($translation->isNew() || $translation->getString() != $value['translation']) { + $translation->setString($value['translation']) + ->setCustomized() + ->save(); + } + } + + // Save value, if different from original configuration. If same as + // original configuration, remove override. + if ($base_config->get($key) !== $value['translation']) { + $translation_config->set($key, $value['translation']); + } + else { + $translation_config->clear($key); + } + } + } + } +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php new file mode 100644 index 0000000..3c1a63b --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php @@ -0,0 +1,83 @@ +getRouteCollection(); + $config_groups = config_translation_get_groups(); + foreach ($config_groups as $group) { + $path = $group->getBasePath(); + + $route = new Route($path . '/translate', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemOverviewPage', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouteName(), $route); + + $route = new Route($path . '/translate/add/{langcode}', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemTranslatePage', + 'action' => 'add', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouteName() . '.add', $route); + + $route = new Route($path . '/translate/edit/{langcode}', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemTranslatePage', + 'action' => 'edit', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouteName() . '.edit', $route); + + $route = new Route($path . '/translate/delete/{langcode}', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemDeletePage', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouteName() . '.delete', $route); + } + } + +} diff --git a/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php new file mode 100644 index 0000000..e6525f7 --- /dev/null +++ b/core/modules/config_translation/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php @@ -0,0 +1,466 @@ + 'Configuration Translation', + 'description' => 'Translate settings and entities to various languages', + 'group' => 'Configuration Translation', + ); + } + + function setUp() { + parent::setUp(); + $permissions = array( + 'translate configuration', + 'administer languages', + 'administer site configuration', + 'administer contact forms', + 'access site-wide contact form' + ); + // Create and login user. + $admin_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($admin_user); + + // Add languages. + foreach ($this->langcodes as $langcode) { + $language = new Language(array('langcode' => $langcode)); + language_save($language); + } + $this->localeStorage = $this->container->get('locale.storage'); + } + + /** + * Test case for site information translation interface. + */ + function testSiteInformationTranslationUI() { + $site_name = 'Site name for testing configuration translation'; + $site_slogan = 'Site slogan for testing configuration translation'; + $fr_site_name = 'Nom du site pour tester la configuration traduction'; + $fr_site_slogan = 'Slogan du site pour tester la traduction de configuration'; + $translation_base_url = 'admin/config/system/site-information/translate'; + + // Set site name and slogan for default language. + $this->setSiteInformation($site_name, $site_slogan); + + $this->drupalGet('admin/config/system/site-information'); + // Check translation tab exist. + $this->assertLinkByHref($translation_base_url); + + $this->drupalGet($translation_base_url); + + // Check 'Add' link of French to visit add page. + $this->assertLinkByHref($translation_base_url . '/add/fr'); + $this->clickLink(t('Add')); + + // Make sure original text is present on this page. + $this->assertRaw($site_name); + $this->assertRaw($site_slogan); + + // Update site name and slogan for French. + $edit = array( + '0[name][translation]' => $fr_site_name, + '0[slogan][translation]' => $fr_site_slogan, + ); + $this->drupalPost($translation_base_url . '/add/fr', $edit, t('Save translation')); + $this->assertRaw(t('Updated French configuration translations successfully.')); + + // Check for edit, delete links (and no 'add' link) for French language. + $this->assertNoLinkByHref($translation_base_url . '/add/fr'); + $this->assertLinkByHref($translation_base_url . '/edit/fr'); + $this->assertLinkByHref($translation_base_url . '/delete/fr'); + + // Check translation saved proper. + $this->drupalGet($translation_base_url . '/edit/fr'); + $this->assertFieldByName('0[name][translation]', $fr_site_name); + $this->assertFieldByName('0[slogan][translation]', $fr_site_slogan); + + // Check French translation of site name and slogan are in place. + $this->drupalGet('fr'); + $this->assertRaw($fr_site_name); + $this->assertRaw($fr_site_slogan); + + // Visit French site to ensure base language string present as source. + $this->drupalGet('fr/' . $translation_base_url . '/edit/fr'); + $this->assertText($site_name); + $this->assertText($site_slogan); + } + + /** + * Test case for site information translation interface. + */ + function testSourceValueDuplicateSave() { + $site_name = 'Site name for testing configuration translation'; + $site_slogan = 'Site slogan for testing configuration translation'; + $translation_base_url = 'admin/config/system/site-information/translate'; + $this->setSiteInformation($site_name, $site_slogan); + + $this->drupalGet($translation_base_url); + + // Case 1: Update new value for site slogan and site name. + $edit = array( + '0[name][translation]' => 'FR ' . $site_name, + '0[slogan][translation]' => 'FR ' . $site_slogan, + ); + // First time, no overrides, so just Add link. + $this->drupalPost($translation_base_url . '/add/fr', $edit, t('Save translation')); + + // Read overridden file from active config. + $file_storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect both name and slogan in language specific file. + $expected = array( + 'name' => 'FR ' . $site_name, + 'slogan' => 'FR ' . $site_slogan + ); + $this->assertEqual($expected, $config_parsed); + + // Case 2: Update new value for site slogan and default value for site name. + $edit = array( + '0[name][translation]' => $site_name, + '0[slogan][translation]' => 'FR ' . $site_slogan, + ); + $this->drupalPost($translation_base_url . '/edit/fr', $edit, t('Save translation')); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect only slogan in language specific file. + $expected = array( + 'slogan' => 'FR ' . $site_slogan + ); + $this->assertEqual($expected, $config_parsed); + + // Case 3: Keep default value for site name and slogan. + $edit = array( + '0[name][translation]' => $site_name, + '0[slogan][translation]' => $site_slogan, + ); + $this->drupalPost($translation_base_url . '/edit/fr', $edit, t('Save translation')); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect no language specific file. + $this->assertFalse($config_parsed); + } + + /** + * Test case for contact category translation. + */ + function testContactConfigEntityTranslation() { + $this->drupalGet('admin/structure/contact'); + + // Check for default contact form config entity from Contact module. + $this->assertLinkByHref('admin/structure/contact/manage/feedback'); + + // Save default language configuration. + $edit = array( + 'label' => 'Send your feedback', + 'recipients' => 'sales@example.com,support@example.com', + 'reply' => 'Thank you for your mail' + ); + $this->drupalPost('admin/structure/contact/manage/feedback', $edit, t('Save')); + + // Visit the form to confirm the changes. + $this->drupalGet('contact/feedback'); + $this->assertText('Send your feedback'); + + foreach ($this->langcodes as $langcode) { + // Update translatable fields. + $edit = array( + '0[label][translation]' => 'Website feedback - ' . $langcode, + '0[reply][translation]' => 'Thank you for your mail - ' . $langcode + ); + + // Save language specific version of form. + $this->drupalPost('admin/structure/contact/manage/feedback/translate/add/' . $langcode, $edit, t('Save translation')); + + // Visit language specific version of form to check label exists. + $this->drupalGet($langcode . '/contact/feedback'); + $this->assertText('Website feedback - ' . $langcode); + + // Submit feedback. + $edit = array( + 'subject' => 'Test subject', + 'message' => 'Test message' + ); + $this->drupalPost(NULL, $edit, t('Send message')); + } + + // We get all emails so no need to check inside the loop. + $captured_emails = $this->drupalGetMails(); + + // Check language specific auto reply text in email body. + foreach ($captured_emails as $email) { + if ($email['id'] == 'contact_page_autoreply') { + // Trim because we get an added newline for the body. + $this->assertEqual(trim($email['body']), 'Thank you for your mail - ' . $email['langcode']); + } + } + + } + + /** + * Test case for account settings translation interface. + * + * This is the only special case so far where we have multiple config names + * involved building up one configuration translation form. Test that the + * translations are saved for all configuration names properly. + */ + function testAccountSettingsConfigurationTranslation() { + $this->drupalGet('admin/config/people/accounts/translate'); + $this->assertLinkByHref('admin/config/people/accounts/translate/add/fr'); + + // Update account settings fields for French. + $edit = array( + '0[anonymous][translation]' => 'Anonyme', + '1[status_blocked][status_blocked.subject][translation]' => 'Testing, your account is blocked.', + '1[status_blocked][status_blocked.body][translation]' => 'Testing account blocked body.' + ); + + $this->drupalPost('admin/config/people/accounts/translate/add/fr', $edit, t('Save translation')); + + // Make sure the changes are saved and loaded back properly. + $this->drupalGet('admin/config/people/accounts/translate/edit/fr'); + foreach ($edit as $value) { + $this->assertRaw($value); + } + } + + /** + * Test source and target language edge cases. + */ + function testSourceAndTargetLanguage() { + // Loading translation page for not-specified language (und) + // should return 403. + $this->drupalGet('admin/config/system/site-information/translate/add/und'); + $this->assertResponse(403); + + // Check the source language doesn't have 'Add' or 'Delete' link and + // make sure source language edit goes to original configuration page + // not the translation specific edit page. + $this->drupalGet('admin/config/system/site-information/translate'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate/edit/en'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate/add/en'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate/delete/en'); + $this->assertLinkByHref('admin/config/system/site-information'); + + // Translation addition to source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate/add/en'); + $this->assertResponse(403); + + // Translation editing in source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate/edit/en'); + $this->assertResponse(403); + + // Translation deletion in source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate/delete/en'); + $this->assertResponse(403); + + // Set default language of site information to not-specified language (und). + config('system.site') + ->set('langcode', Language::LANGCODE_NOT_SPECIFIED) + ->save(); + + // Make sure translation tab does not exist on the config page. + $this->drupalGet('admin/config/system/site-information'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate'); + + // If source language is not specified, translation page should be 403. + $this->drupalGet('admin/config/system/site-information/translate'); + $this->assertResponse(403); + } + + /** + * Test case for views translation interface. + */ + function testViewsTranslationUI() { + $description = 'A list of nodes marked for display on the front page.'; + $human_readable_name = 'Frontpage'; + $display_settings_master = 'Master'; + $display_options_master = '(Empty)'; + $translation_base_url = 'admin/structure/views/view/frontpage/translate'; + + $this->drupalGet($translation_base_url); + + // Check 'Add' link of French to visit add page. + $this->assertLinkByHref($translation_base_url . '/add/fr'); + $this->clickLink(t('Add')); + + // Make sure original text is present on this page. + $this->assertRaw($description); + $this->assertRaw($human_readable_name); + + // Update Views Fields for French. + $edit = array( + '0[description][translation]' => $description . " FR", + '0[label][translation]' => $human_readable_name . " FR", + '0[display][default][display.default.display_title][translation]' => $display_settings_master . " FR", + '0[display][default][display_options][display.default.display_options.title][translation]' => $display_options_master . " FR", + ); + $this->drupalPost($translation_base_url . '/add/fr', $edit, t('Save translation')); + $this->assertRaw(t('Updated French configuration translations successfully.')); + + // Check for edit, delete links (and no 'add' link) for French language. + $this->assertNoLinkByHref($translation_base_url . '/add/fr'); + $this->assertLinkByHref($translation_base_url . '/edit/fr'); + $this->assertLinkByHref($translation_base_url . '/delete/fr'); + + // Check translation saved proper. + $this->drupalGet($translation_base_url . '/edit/fr'); + $this->assertFieldByName('0[description][translation]', $description . " FR"); + $this->assertFieldByName('0[label][translation]', $human_readable_name . " FR"); + $this->assertFieldByName('0[display][default][display.default.display_title][translation]', $display_settings_master . " FR"); + $this->assertFieldByName('0[display][default][display_options][display.default.display_options.title][translation]', $display_options_master . " FR"); + } + + /** + * Test translation storage in locale storage. + */ + function testLocaleDBStorage() { + $langcode = 'xx'; + $name = $this->randomName(16); + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Make sure there is no translation stored in locale storage before edit. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertTrue(empty($translation)); + + // Add custom translation. + $edit = array( + '0[anonymous][translation]' => 'Anonyme', + ); + $this->drupalPost('admin/config/people/accounts/translate/add/fr', $edit, t('Save translation')); + + // Make sure translation stored in locale storage after saved language + // specific configuration translation. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertEqual('Anonyme', $translation->getString()); + + // revert custom translations to base translation. + $edit = array( + '0[anonymous][translation]' => 'Anonymous', + ); + $this->drupalPost('admin/config/people/accounts/translate/edit/fr', $edit, t('Save translation')); + + // Make sure there is no translation stored in locale storage after revert. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertEqual('Anonymous', $translation->getString()); + } + + /** + * Test case for single language existing. + */ + function testSingleLanguageUI() { + // Delete French language + $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete')); + $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'French', '%langcode' => 'fr'))); + + // Change default language to Tamil. + $edit = array( + 'site_default_language' => 'ta', + ); + $this->drupalPost('admin/config/regional/settings', $edit, t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.')); + + // Delete English language + $this->drupalPost('admin/config/regional/language/delete/en', array(), t('Delete')); + $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'English', '%langcode' => 'en'))); + + // Visit account setting translation page, this should not + // throw any notices. + $this->drupalGet('admin/config/people/accounts/translate'); + $this->assertResponse(200); + } + + /** + * Gets translation from locale storage. + * + * @param $config_name + * Configuration object. + * @param $key + * Translation config field key. + * @param $langcode + * String language code to load translation. + * + * @return bool|mixed + * Returns translation if exists, FALSE otherwise. + */ + protected function getTranslation($config_name, $key, $langcode) { + $settings_locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $config_name)); + $this->assertTrue(!empty($settings_locations), format_string('Configuration locations found for %config_name.', array('%config_name' => $config_name))); + + if (!empty($settings_locations)) { + $source = $this->container->get('config.factory')->get($config_name)->get($key); + $source_string = $this->localeStorage->findString(array('source' => $source, 'type' => 'configuration')); + $this->assertTrue(!empty($source_string), format_string('Found string for %config_name.%key.', array('%config_name' => $config_name, '%key' => $key))); + + if (!empty($source_string)) { + $conditions = array( + 'lid' => $source_string->lid, + 'language' => $langcode, + ); + $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE)); + return reset($translations); + } + } + return FALSE; + } + + /** + * Helper to set site name and slogan for default language. + * + * @param string $site_name + * @param string $site_slogan + */ + protected function setSiteInformation($site_name, $site_slogan) { + $edit = array( + 'site_name' => $site_name, + 'site_slogan' => $site_slogan + ); + $this->drupalPost('admin/config/system/site-information', $edit, t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.')); + + } +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php new file mode 100644 index 0000000..e068181 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php @@ -0,0 +1,56 @@ +getRequirements()); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request) { + $mapper = $route->getDefault('mapper'); + $entity = $request->attributes->get($mapper->getType()); + // Get configuration group for this mapper. + $group = $mapper->getConfigGroup($entity); + $group_language = $group->getLanguageWithFallback(); + $language = NULL; + + if ($request->query->has('langcode')) { + $language = language_load($request->query->get('langcode')); + } + + // Only allow access to translate configuration, if proper permissions are + // granted, the configuration has translatable pieces, the source language + // and target language are not locked, and the target language is not the + // original submission language. Although technically configuration can be + // overlayed with translations in the same language, that is logically not + // a good idea. + return ( + user_access('translate configuration') && + $group->hasSchema() && + $group->hasTranslatable() && + !$group_language->locked && + (empty($language) || (!$language->locked && $language->id != $group_language->id)) + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php new file mode 100644 index 0000000..bed5a50 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php @@ -0,0 +1,160 @@ +basePath = $base_path; + $this->entityType = $entity_type; + $this->title = $title; + $this->menuItemType = $menu_item_type; + $this->addEditTab = $add_edit_tab; + $this->setRouteName(); + } + + /** + * {@inheritdoc} + */ + public function needsEditTab() { + return $this->addEditTab; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return $this->basePath; + } + + /** + * {@inheritdoc} + */ + public function getMenuItemType() { + return $this->menuItemType; + } + + /** + * {@inheritdoc} + */ + public function getConfigGroup($arg = NULL) { + // In this implementation, $arg is the configuration entity loaded via the + // menu (for contact module for example) or a string (configuration entity + // ID), in which case we need to load the entity (for views for example). + if (!isset($arg)) { + return NULL; + } + if (is_string($arg)) { + $entity = entity_load($this->entityType, $arg); + } + else { + $entity = $arg; + } + + // Replace path segment with the ID of the entity for further processing. + $base_path = str_replace('{' . $this->entityType . '}', $entity->id(), $this->basePath); + + // Replace entity label in template title. + $title = format_string($this->title, array('@label' => $entity->label())); + + // The list of configuration IDs belonging to this entity. + $entity_type_info = entity_get_info($this->entityType); + $names = array($entity_type_info['config_prefix'] . '.' . $entity->id()); + + return new ConfigGroupMapper($base_path, $title, $names, $this->menuItemType, $this->addEditTab); + } + + /** + * {@inheritdoc} + */ + public function setRouteName() { + $search = array('/', '-', '{', '}'); + $replace = array('.', '_', '_', '_'); + $this->routeName = 'config_translation.item.' . str_replace($search, $replace, $this->basePath); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->routeName; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->entityType; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php new file mode 100644 index 0000000..44c83aa --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigGroupMapper.php @@ -0,0 +1,256 @@ +basePath = $base_path; + $this->title = $title; + $this->names = $names; + $this->menuItemType = $menu_item_type; + $this->addEditTab = $add_edit_tab; + $this->setRouteName(); + } + + /** + * {@inheritdoc} + */ + public function needsEditTab() { + return $this->addEditTab; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return $this->basePath; + } + + /** + * Returns title of this configuration group. + * + * @return string + * The group title. + */ + public function getTitle() { + return $this->title; + } + + /** + * Returns list of names in this configuration group. + * + * @return array + * An array of configuration names in this group. + */ + public function getNames() { + return $this->names; + } + + /** + * {@inheritdoc} + */ + public function getMenuItemType() { + return $this->menuItemType; + } + + /** + * {@inheritdoc} + */ + public function getConfigGroup($arg = NULL) { + // This is a ConfigGroupMapper already. + return $this; + } + + /** + * Returns the original language code of the configuration group. + * + * @todo Revisit. This assumes the langcode of the first element for now and + * might lead to inconsistency. + */ + public function getLangcode() { + return config_translation_original_langcode($this->names[0]); + } + + /** + * Returns language object for the configuration group. + * + * If the language of the group is not a configured language on the site and + * it is English, we return a dummy language object to represent the + * built-in language. + * + * @return \Drupal\Core\Language\Language + * A configured language object instance or a dummy English language object. + */ + public function getLanguageWithFallback() { + $langcode = $this->getLangcode(); + $language = language_load($langcode); + if (empty($language) && $langcode == 'en') { + $language = new Language(array('id' => 'en', 'name' => t('Built-in English'))); + } + return $language; + } + + /** + * Returns an array with configuration data for the group. + * + * @return array + * Configuration data keyed by configuration names in the group. + */ + public function getConfigData() { + $config_data = array(); + foreach ($this->names as $name) { + $config_data[$name] = config($name)->get(); + } + return $config_data; + } + + /** + * Checks that all pieces of this configuration group has schema. + * + * @return bool + * TRUE if all of the group elements have schema, FALSE otherwise. + */ + public function hasSchema() { + foreach ($this->names as $name) { + if (!config_translation_has_schema($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * Checks that all pieces of this configuration group have translatables. + * + * @return bool + * TRUE if all of the group elements have translatables, FALSE otherwise. + */ + public function hasTranslatable() { + foreach ($this->names as $name) { + if (!config_translation_has_translatable($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * Checks whether there is already a translation for this group. + * + * @param \Drupal\Core\Language\Language $language + * A language object. + * + * @return bool + * TRUE if any of the group elements have a translation in the given + * language, FALSE otherwise. + */ + public function hasTranslation(Language $language) { + foreach ($this->names as $name) { + if (config_translation_exists($name, $language)) { + return TRUE; + } + } + return FALSE; + } + + /** + * Adds the given configuration name to the list of names in the group. + * + * @param string $name + * Configuration name. + */ + public function addName($name) { + $this->names[] = $name; + } + + /** + * {@inheritdoc} + */ + public function setRouteName() { + $this->routeName = 'config_translation.item.' . str_replace(array('/', '-'), array('.', '_'), $this->basePath); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->routeName; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return NULL; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php new file mode 100644 index 0000000..91b4da1 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php @@ -0,0 +1,72 @@ +get('database')); + } + + /** + * Language translations overview page for a configuration name. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return array + * Page render array. + */ + public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper) { + if ($request->query->has('action') && $request->query->has('langcode') ) { + $action = $request->query->get('action'); + $langcode = $request->query->get('langcode'); + switch ($action) { + case 'add': + case 'edit': + return $this->itemTranslatePage($request, $action, $mapper); + break; + case 'delete': + return $this->itemDeletePage($request, $mapper); + break; + } + } + $group = $this->getConfigGroup($request, $mapper); + drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH); + + // It is possible the original language this configuration was saved with is + // not on the system. For example, the configuration shipped in English but + // the site has no English configured. Represent the original language in + // the table even if it is not currently configured. + $languages = language_list(); + $original_langcode = $group->getLangcode(); + if (!isset($languages[$original_langcode])) { + $language_name = language_name($original_langcode); + if ($original_langcode == 'en') { + $language_name = t('Built-in English'); + } + // Create a dummy language object for this listing only. + $languages[$original_langcode] = new Language(array('id' => $original_langcode, 'name' => $language_name)); + } + + $path = $group->getBasePath(); + $header = array(t('Language'), t('Operations')); + $page = array(); + $page['languages'] = array( + '#type' => 'table', + '#header' => $header, + ); + foreach ($languages as $language) { + if ($language->id == $original_langcode) { + $page['languages'][$language->id]['language'] = array( + '#markup' => '' . t('@language (original)', array('@language' => $language->name)) . '', + ); + + // @todo The user translating might as well not have access to + // edit the original configuration. They will get a 403 for this + // link when clicked. Do we know better? + $operations = array(); + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path, + ); + $page['languages'][$language->id]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + else { + $page['languages'][$language->id]['language'] = array( + '#markup' => $language->name, + ); + $operations = array(); + $path_options = array('langcode' => $language->id); + // If no translation exists for this language, link to add one. + if (!$group->hasTranslation($language)) { + $operations['add'] = array( + 'title' => t('Add'), + 'href' => $path . '/translate', + 'query' => array('action' => 'add') + $path_options, + ); + } + else { + // Otherwise, link to edit the existing translation. + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path . '/translate', + 'query' => array('action' => 'edit') + $path_options, + ); + $operations['delete'] = array( + 'title' => t('Delete'), + 'href' => $path . '/translate', + 'query' => array('action' => 'delete') + $path_options, + ); + } + + $page['languages'][$language->id]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + } + return $page; + } + + /** + * Renders translation item manage form. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param string $action + * Action identifier, either 'add' or 'edit'. Used to provide proper + * labeling on the screen. + * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return array + * The render array for the translation item manage form. + */ + public function itemTranslatePage(Request $request, $action, ConfigMapperInterface $mapper) { + $group = $this->getConfigGroup($request, $mapper); + $language = $this->getLanguage($request, $mapper); + + $replacement = array( + '%label' => $group->getTitle(), + '@language' => strtolower($language->name), + ); + switch ($action) { + case 'add': + drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH); + break; + case 'edit': + drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH); + break; + } + + // Make sure we are in the override free configuration context. For example, + // visiting the configuration page in another language would make those + // language overrides active by default. But we need the original values. + config_context_enter('config.context.free'); + + // Get base language configuration to display in the form before entering + // into the language context for the form. This avoids repetitively going + // in and out of the language context to get original values later. + $base_config = $group->getConfigData(); + + // Leave override free context. + config_context_leave(); + + // Enter context for the translation target language requested and generate + // form with translation data in that language. + config_context_enter('Drupal\language\LanguageConfigContext')->setLanguage($language); + $locale_storage = \Drupal::service('locale.storage'); + return drupal_get_form(new ConfigTranslationManageForm($locale_storage), $group, $language, $base_config); + } + + /** + * Renders the item delete form. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return array + * The render array for the translation item delete form. + */ + public function itemDeletePage(Request $request, ConfigMapperInterface $mapper) { + return drupal_get_form(new ConfigTranslationDeleteForm(), $this->getConfigGroup($request, $mapper), $this->getLanguage($request, $mapper)); + } + + /** + * Gets the configuration group. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return \Drupal\config_translation\ConfigGroupMapper + * The configuration group. + */ + protected function getConfigGroup(Request $request, ConfigMapperInterface $mapper) { + // Get configuration group for this mapper. + $entity = $request->attributes->get($mapper->getType()); + return $mapper->getConfigGroup($entity); + } + + /** + * Gets the language object. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param \Drupal\config_translation\ConfigMapperInterface $mapper + * Configuration mapper. + * + * @return false|\Drupal\core\Language\Language + * Returns Language object when langcode found in request, FALSE otherwise. + */ + protected function getLanguage(Request $request, ConfigMapperInterface $mapper) { + if ($request->query->has('langcode')) { + return language_load($request->query->get('langcode')); + } + return FALSE; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php new file mode 100644 index 0000000..a1d647b --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php @@ -0,0 +1,87 @@ + $this->group->getTitle(), '@language' => $this->language->name)); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelPath() { + return $this->group->getBasePath() . '/translate'; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'config_translation_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, Language $language = NULL) { + $this->group = $group; + $this->language = $language; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $storage = drupal_container()->get('config.storage'); + foreach ($this->group->getNames() as $name) { + $storage->delete('locale.config.' . $this->language->id . '.' . $name); + } + // @todo Do we need to flush caches with drupal_flush_all_caches()? The + // configuration change may affect page display. + + drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->group->getTitle(), '@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php new file mode 100644 index 0000000..bad96f3 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php @@ -0,0 +1,332 @@ +localeStorage = $locale_storage; + } + + /** + * {@inheritdoc}. + */ + public function getFormID() { + return 'config_translation_form'; + } + + /** + * Implements \Drupal\Core\Form\FormInterface::buildForm(). + * + * Builds configuration form with metadata and values from the source + * language. + * + * @param \Drupal\config_translation\ConfigMapperInterface $group + * The configuration group the form is being built for. + * @param Language $language + * The language the form is adding or editing. + * @param array $base_config_data + * The base configuration in the source language. + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, Language $language = NULL, array $base_config_data = NULL) { + $this->group = $group; + $this->language = $language; + $this->sourceLanguage = $this->group->getConfigGroup()->getLanguageWithFallback(); + $this->baseConfigData = $base_config_data; + + foreach ($this->group->getNames() as $id => $name) { + $form[$id] = array( + '#type' => 'container', + '#tree' => TRUE, + ); + $form[$id] += $this->buildConfigForm(config_typed()->get($name), config($name)->get(), $this->baseConfigData[$name]); + } + + $form['#attached']['css'] = array(drupal_get_path('module', 'config_translation') . '/config_translation.admin.css'); + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translation')); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $form_values = $form_state['values']; + + // For the form submission handling, use the override free context. + config_context_enter('config.context.free'); + + foreach ($this->group->getNames() as $id => $name) { + // Set configuration values based on form submission and source values. + $base_config = config($name); + $translation_config = config('locale.config.' . $this->language->id . '.' . $name); + $locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $name)); + + $this->setConfig($this->language, $base_config, $translation_config, $form_values[$id], !empty($locations)); + + // If no overrides, delete language specific configuration file. + $saved_config = $translation_config->get(); + if (empty($saved_config)) { + $translation_config->delete(); + } + else { + $translation_config->save(); + } + } + + config_context_leave(); + + drupal_set_message(t('Updated @language configuration translations successfully.', array('@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + + /** + * Formats configuration schema as a form tree. + * + * @param mixed $schema + * Schema definition of configuration which is a + * \Drupal\Core\Config\Schema\Element or a + * \Drupal\Core\TypedData\TypedConfigManager. + * @param array|string $config_data + * Configuration object of requested language, a string when done traversing + * the data building each sub-structure for the form. + * @param array|string $base_config_data + * Configuration object of base language, a string when done traversing + * the data building each sub-structure for the form. + * @param bool $collapsed + * Flag to set collapsed state. + * @param string|NULL $base_key + * Base configuration key. + * + * @return array + * An associative array containing the structure of the form. + * + * @todo Make this conditional on the translatable schema property from + * http://drupal.org/node/2035393 - currently hardcodes label and text. + */ + protected function buildConfigForm($schema, $config_data, $base_config_data, $collapsed = FALSE, $base_key = '') { + $build = array(); + foreach ($schema as $key => $element) { + // Make the specific element key, "$base_key.$key". + $element_key = implode('.', array_filter(array($base_key, $key))); + $definition = $element->getDefinition() + array('label' => t('N/A')); + if ($element instanceof Element) { + // Build sub-structure and include it with a wrapper in the form + // if there are any translatable elements there. + $sub_build = $this->buildConfigForm($element, $config_data[$key], $base_config_data[$key], TRUE, $element_key); + if (!empty($sub_build)) { + // For some configuration elements the same element structure can + // repeat multiple times, (like views displays, filters, etc.). + // So try to find a more usable title for the details summary. First + // check if there is an element which is called title or label, then + // check if there is an element which contains these words. + $title = ''; + if (isset($sub_build['title']['source'])) { + $title = $sub_build['title']['source']['#markup']; + } + elseif (isset($sub_build['label']['source'])) { + $title = $sub_build['label']['source']['#markup']; + } + else { + foreach (array_keys($sub_build) as $title_key) { + if (isset($sub_build[$title_key]['source']) && (strpos($title_key, 'title') !== FALSE || strpos($title_key, 'label') !== FALSE)) { + $title = $sub_build[$title_key]['source']['#markup']; + break; + } + } + } + $build[$key] = array( + '#type' => 'details', + '#title' => (!empty($title) ? (strip_tags($title) . ' ') : '') . t($definition['label']), + '#collapsible' => TRUE, + '#collapsed' => $collapsed, + ) + $sub_build; + } + } + else { + $definition = $element->getDefinition(); + + // Create form element only for translatable items. + if (!isset($definition['translatable']) || !isset($definition['type'])) { + continue; + } + + $value = $config_data[$key]; + $build[$element_key] = array( + '#theme' => 'config_translation_manage_form_element', + ); + $build[$element_key]['source'] = array( + '#markup' => $base_config_data[$key] ? ('' . nl2br($base_config_data[$key] . '')) : t('(Empty)'), + '#title' => t($definition['label']) . ' ('. $this->sourceLanguage->name . ')', + '#type' => 'item', + ); + + // Estimate a comfortable size of the input textarea. + $rows_words = ceil(str_word_count($value) / 5); + $rows_newlines = substr_count($value, "\n" ) + 1; + $rows = max($rows_words, $rows_newlines); + + $build[$element_key]['translation'] = array( + '#type' => $this->getFormElementType($definition['type']), + '#default_value' => $value, + '#title' => t($definition['label']) . ' (' . $this->language->name . ')', + '#rows' => $rows, + '#attributes' => array('lang' => $this->language->id), + ); + } + } + return $build; + } + + /** + * Sets configuration based on a nested form value array. + * + * @param \Drupal\Core\Language\Language $language + * Set the configuration in this language. + * @param \Drupal\Core\Config\Config $base_config + * Base configuration values, in the source language. + * @param \Drupal\Core\Config\Config $translation_config + * Translation configuration instance. Values from $config_values will be + * set in this instance. + * @param array $config_values + * A simple one dimensional or recursive array: + * - simple: + * array(name => array('translation' => 'French site name')); + * - recursive: + * cancel_confirm => array( + * cancel_confirm.subject => array('translation' => 'Subject'), + * cancel_confirm.body => array('translation' => 'Body content'), + * ); + * Either format is used, the nested arrays are just containers and not + * needed for saving the data. + * @param bool $shipped_config + * Flag to specify whether the configuration had a shipped version and + * therefore should also be stored in the locale database. + */ + protected function setConfig(Language $language, Config $base_config, Config $translation_config, array $config_values, $shipped_config = FALSE) { + foreach ($config_values as $key => $value) { + if (is_array($value) && !isset($value['translation'])) { + // Traverse into this level in the configuration. + $this->setConfig($language, $base_config, $translation_config, $value, $shipped_config); + } + else { + + // If the configuration file being translated was originally shipped, we + // should update the locale translation storage. The string should + // already be there, but we make sure to check. + if ($shipped_config && $source_string = $this->localeStorage->findString(array('source' => $base_config->get($key)))) { + + // Get the translation for this original source string from locale. + $conditions = array( + 'lid' => $source_string->lid, + 'language' => $language->id, + ); + $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE)); + // If we got a translation, take that, otherwise create a new one. + $translation = reset($translations) ?: $this->localeStorage->createTranslation($conditions); + + // If we have a new translation or different from what is stored in + // locale before, save this as an updated customize translation. + if ($translation->isNew() || $translation->getString() != $value['translation']) { + $translation->setString($value['translation']) + ->setCustomized() + ->save(); + } + } + + // Save value, if different from the source value in the base + // configuration. If same as original configuration, remove override. + if ($base_config->get($key) !== $value['translation']) { + $translation_config->set($key, $value['translation']); + } + else { + $translation_config->clear($key); + } + } + } + } + + /** + * Provides a mapping of the TypedData element type to form element type. + * + * @param string $element_type + * The element type from the definition of a TypedData object. + * + * @return string + * The form element type. + */ + protected function getFormElementType($element_type) { + switch ($element_type) { + case 'text': + return 'textarea'; + break; + default: + // By default provide text field. + return 'textfield'; + break; + } + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php new file mode 100644 index 0000000..ad11867 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php @@ -0,0 +1,58 @@ +getRouteCollection(); + $config_groups = config_translation_get_groups(); + foreach ($config_groups as $group) { + $path = $group->getBasePath(); + + $route = new Route($path . '/translate', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemOverviewPage', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouteName(), $route); + + } + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php new file mode 100644 index 0000000..f11aa3c --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php @@ -0,0 +1,450 @@ + 'Configuration Translation lists', + 'description' => 'Visit all lists.', + 'group' => 'Configuration Translation', + ); + } + + /** + * Admin user with all needed permissions. + * + * @var \Drupal\user\Plugin\Core\Entity\User + */ + protected $adminUser; + + public function setUp() { + parent::setUp(); + + $permissions = array( + 'access site-wide contact form', + 'administer blocks', + 'administer contact forms', + 'administer content types', + 'administer filters', + 'administer menu', + 'administer permissions', + 'administer shortcuts', + 'administer site configuration', + 'administer taxonomy', + 'administer users', + 'administer languages', + 'administer image styles', + 'administer pictures', + 'translate configuration', + ); + + // Create and log in user. + $this->adminUser = $this->drupalCreateUser($permissions); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests the block listing for the translate operation. + * + * There are no blocks placed in the testing profile. Add one, then check + * for Translate operation. + */ + protected function doBlockListTest() { + // Add a test block, any block will do. + // Set the machine name so the translate link can be built later. + $block_machine_name = Unicode::strtolower($this->randomName(16)); + $this->drupalPlaceBlock('system_powered_by_block', array('machine_name' => $block_machine_name)); + + // Get the Block listing. + $this->drupalGet('admin/structure/block'); + + $translate_link = 'admin/structure/block/manage/stark.' . $block_machine_name . '/translate'; + // Test if the link to translate the block is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the menu listing for the translate operation. + */ + protected function doMenuListTest() { + // Create a test menu to decouple looking for translate operations link so + // this does not test more than necessary. + $this->drupalGet('admin/structure/menu/add'); + // Lowercase the machine name. + $menu_name = Unicode::strtolower($this->randomName(16)); + $label = $this->randomName(16); + $edit = array( + 'id' => $menu_name, + 'description' => '', + 'label' => $label, + ); + // Create the menu by posting the form. + $this->drupalPost('admin/structure/menu/add', $edit, t('Save')); + + // Get the Menu listing. + $this->drupalGet('admin/structure/menu'); + + // Custom Menus are automatically prefixed by 'menu-'. + $menu_name = 'menu-' . $menu_name; + $translate_link = 'admin/structure/menu/manage/' . $menu_name . '/translate'; + // Test if the link to translate the menu is on the page. + $this->assertLinkByHref($translate_link); + + // Check if the Link is not added if you are missing 'translate + // configuration' permission. + $permissions = array( + 'administer menu', + ); + $user = $this->drupalCreateUser($permissions); + $this->drupalLogin($user); + + // Get the Menu listing. + $this->drupalGet('admin/structure/menu'); + + $translate_link = 'admin/structure/menu/manage/' . $menu_name . '/translate'; + // Test if the link to translate the menu is NOT on the page. + $this->assertNoLinkByHref($translate_link); + + // Login as Admin again otherwise the rest will fail. + $this->drupalLogin($this->adminUser); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the vocabulary listing for the translate operation. + */ + protected function doVocabularyListTest() { + // Create a test vocabulary to decouple looking for translate operations + // link so this does not test more than necessary. + $vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => $this->randomName(), + 'description' => $this->randomName(), + 'vid' => Unicode::strtolower($this->randomName()), + )); + $vocabulary->save(); + + // Get the Taxonomy listing. + $this->drupalGet('admin/structure/taxonomy'); + + $translate_link = 'admin/structure/taxonomy/manage/' . $vocabulary->id() . '/translate'; + // Test if the link to translate the vocabulary is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the custom block listing for the translate operation. + */ + function doCustomBlockTypeListTest() { + // Create a test custom block type to decouple looking for translate + // operations link so this does not test more than necessary. + $custom_block_type = entity_create('custom_block_type', array( + 'id' => Unicode::strtolower($this->randomName(16)), + 'label' => $this->randomName(), + 'revision' => FALSE + )); + $custom_block_type->save(); + + // Get the custom block listing. + $this->drupalGet('admin/structure/custom-blocks'); + + $translate_link = 'admin/structure/custom-blocks/manage/' . $custom_block_type->id() . '/translate'; + // Test if the link to translate the custom block is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the contact forms listing for the translate operation. + */ + function doContactFormsListTest() { + // Create a test contact form to decouple looking for translate operations + // link so this does not test more than necessary. + $contact_form = entity_create('contact_category', array( + 'id' => Unicode::strtolower($this->randomName(16)), + 'label' => $this->randomName(), + )); + $contact_form->save(); + + // Get the contact form listing. + $this->drupalGet('admin/structure/contact'); + + $translate_link = 'admin/structure/contact/manage/' . $contact_form->id() . '/translate'; + // Test if the link to translate the contact form is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + + /** + * Tests the content type listing for the translate operation. + */ + function doContentTypeListTest() { + // Create a test content type to decouple looking for translate operations + // link so this does not test more than necessary. + $content_type = entity_create('node_type', array( + 'type' => Unicode::strtolower($this->randomName(16)), + 'name' => $this->randomName(), + )); + $content_type->save(); + + // Get the content type listing. + $this->drupalGet('admin/structure/types'); + + $translate_link = 'admin/structure/types/manage/' . $content_type->id() . '/translate'; + // Test if the link to translate the content type is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the formats listing for the translate operation. + */ + function doFormatsListTest() { + // Create a test format to decouple looking for translate operations + // link so this does not test more than necessary. + $filter_format = entity_create('filter_format', array( + 'format' => Unicode::strtolower($this->randomName(16)), + 'name' => $this->randomName(), + )); + $filter_format->save(); + + // Get the format listing. + $this->drupalGet('admin/config/content/formats'); + + $translate_link = 'admin/config/content/formats/manage/' . $filter_format->id() . '/translate'; + // Test if the link to translate the format is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the shortcut listing for the translate operation. + */ + function doShortcutListTest() { + // Create a test shortcut to decouple looking for translate operations + // link so this does not test more than necessary. + $shortcut = entity_create('shortcut_set', array( + 'id' => Unicode::strtolower($this->randomName(16)), + 'label' => $this->randomString(), + )); + $shortcut->save(); + + // Get the shortcut listing. + $this->drupalGet('admin/config/user-interface/shortcut'); + + $translate_link = 'admin/config/user-interface/shortcut/manage/' . $shortcut->id() . '/translate'; + // Test if the link to translate the shortcut is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the role listing for the translate operation. + */ + function doUserRoleListTest() { + // Create a test role to decouple looking for translate operations + // link so this does not test more than necessary. + $role_id = Unicode::strtolower($this->randomName(16)); + $this->drupalCreateRole(array(), $role_id); + + // Get the role listing. + $this->drupalGet('admin/people/roles'); + + $translate_link = 'admin/people/roles/manage/' . $role_id . '/translate'; + // Test if the link to translate the role is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the language listing for the translate operation. + */ + function doLanguageListTest() { + // Create a test language to decouple looking for translate operations + // link so this does not test more than necessary. + $language = new Language(array('id' => 'ga', 'name' => 'Irish')); + language_save($language); + + // Get the language listing. + $this->drupalGet('admin/config/regional/language'); + + $translate_link = 'admin/config/regional/language/edit/ga/translate'; + // Test if the link to translate the language is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the image style listing for the translate operation. + */ + function doImageStyleListTest() { + // Get the image style listing. + $this->drupalGet('admin/config/media/image-styles'); + + $translate_link = 'admin/config/media/image-styles/manage/medium/translate'; + // Test if the link to translate the style is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the picture mapping listing for the translate operation. + */ + function doPictureListTest() { + $edit = array(); + $edit['label'] = $this->randomName(); + $edit['id'] = strtolower($edit['label']); + + $this->drupalPost('admin/config/media/picturemapping/add', $edit, t('Save')); + $this->assertRaw(t('Picture mapping %label saved.', array('%label' => $edit['label']))); + + // Get the picture mapping listing. + $this->drupalGet('admin/config/media/picturemapping'); + + $translate_link = 'admin/config/media/picturemapping/' . $edit['id'] . '/translate'; + // Test if the link to translate the style is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests the date format listing for the translate operation. + */ + function doDateFormatListTest() { + // Get the date format listing. + $this->drupalGet('admin/config/regional/date-time'); + + $translate_link = 'admin/config/regional/date-time/formats/manage/long/translate'; + // Test if the link to translate the format is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests a given settings page for the translate operation. + * + * @param string $link + * URL of the settings page to test. + */ + function doSettingsPageTest($link) { + // Get the settings page. + $this->drupalGet($link); + + $translate_link = $link . '/translate'; + // Test if the link to translate the settings page is present. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + + /** + * Tests if translate link is added to operations in all configuration lists. + */ + public function testTranslateOperationInListUI() { + + // All lists from config_translation_config_translation_group_info(). + + $this->doBlockListTest(); + $this->doMenuListTest(); + $this->doVocabularyListTest(); + $this->doCustomBlockTypeListTest(); + $this->doContactFormsListTest(); + $this->doContentTypeListTest(); + $this->doFormatsListTest(); + $this->doShortcutListTest(); + $this->doUserRoleListTest(); + $this->doLanguageListTest(); + $this->doImageStyleListTest(); + $this->doPictureListTest(); + $this->doDateFormatListTest(); + + // Views is tested in Drupal\config_translation\Tests\ConfigTranslationViewListUITest + + // Test the maintenance settings page. + $this->doSettingsPageTest('admin/config/development/maintenance'); + // Test the site information settings page. + $this->doSettingsPageTest('admin/config/system/site-information'); + // Test the account settings page. + $this->doSettingsPageTest('admin/config/people/accounts'); + // Test the RSS settings page. + $this->doSettingsPageTest('admin/config/services/rss-publishing'); + + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php new file mode 100644 index 0000000..16c8819 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php @@ -0,0 +1,486 @@ + 'Configuration Translation', + 'description' => 'Translate settings and entities to various languages', + 'group' => 'Configuration Translation', + ); + } + + public function setUp() { + parent::setUp(); + $permissions = array( + 'translate configuration', + 'administer languages', + 'administer site configuration', + 'administer contact forms', + 'access site-wide contact form' + ); + // Create and login user. + $admin_user = $this->drupalCreateUser($permissions); + $this->drupalLogin($admin_user); + + // Add languages. + foreach ($this->langcodes as $langcode) { + $language = new Language(array('id' => $langcode)); + language_save($language); + } + $this->localeStorage = $this->container->get('locale.storage'); + } + + /** + * Tests the site information translation interface. + */ + function testSiteInformationTranslationUI() { + $site_name = 'Site name for testing configuration translation'; + $site_slogan = 'Site slogan for testing configuration translation'; + $fr_site_name = 'Nom du site pour tester la configuration traduction'; + $fr_site_slogan = 'Slogan du site pour tester la traduction de configuration'; + $translation_base_url = 'admin/config/system/site-information/translate'; + + // Set site name and slogan for default language. + $this->setSiteInformation($site_name, $site_slogan); + + $this->drupalGet('admin/config/system/site-information'); + // Check translation tab exist. + $this->assertLinkByHref($translation_base_url); + + $this->drupalGet($translation_base_url); + + // Check 'Add' link of French to visit add page. + $this->assertLinkByHref($translation_base_url . '?action=add&langcode=fr'); + $this->clickLink(t('Add')); + + // Make sure original text is present on this page. + $this->assertRaw($site_name); + $this->assertRaw($site_slogan); + + // Update site name and slogan for French. + $edit = array( + '0[name][translation]' => $fr_site_name, + '0[slogan][translation]' => $fr_site_slogan, + ); + + $this->drupalPost($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + $this->assertRaw(t('Updated French configuration translations successfully.')); + + // Check for edit, delete links (and no 'add' link) for French language. + $this->assertNoLinkByHref($translation_base_url . '?action=add&langcode=fr'); + $this->assertLinkByHref($translation_base_url . '?action=edit&langcode=fr'); + $this->assertLinkByHref($translation_base_url . '?action=delete&langcode=fr'); + + // Check translation saved proper. + $this->drupalGet($translation_base_url, $this->getUrlOptions('edit', 'fr')); + $this->assertFieldByName('0[name][translation]', $fr_site_name); + $this->assertFieldByName('0[slogan][translation]', $fr_site_slogan); + + // Check French translation of site name and slogan are in place. + $this->drupalGet('fr'); + $this->assertRaw($fr_site_name); + $this->assertRaw($fr_site_slogan); + + // Visit French site to ensure base language string present as source. + $this->drupalGet('fr/' . $translation_base_url, $this->getUrlOptions('edit', 'fr')); + $this->assertText($site_name); + $this->assertText($site_slogan); + } + + /** + * Tests the site information translation interface. + */ + function testSourceValueDuplicateSave() { + $site_name = 'Site name for testing configuration translation'; + $site_slogan = 'Site slogan for testing configuration translation'; + $translation_base_url = 'admin/config/system/site-information/translate'; + $this->setSiteInformation($site_name, $site_slogan); + + $this->drupalGet($translation_base_url); + + // Case 1: Update new value for site slogan and site name. + $edit = array( + '0[name][translation]' => 'FR ' . $site_name, + '0[slogan][translation]' => 'FR ' . $site_slogan, + ); + // First time, no overrides, so just Add link. + $this->drupalPost($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + + // Read overridden file from active config. + $file_storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect both name and slogan in language specific file. + $expected = array( + 'name' => 'FR ' . $site_name, + 'slogan' => 'FR ' . $site_slogan + ); + $this->assertEqual($expected, $config_parsed); + + // Case 2: Update new value for site slogan and default value for site name. + $edit = array( + '0[name][translation]' => $site_name, + '0[slogan][translation]' => 'FR ' . $site_slogan, + ); + $this->drupalPost($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('edit', 'fr')); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect only slogan in language specific file. + $expected = array( + 'slogan' => 'FR ' . $site_slogan + ); + $this->assertEqual($expected, $config_parsed); + + // Case 3: Keep default value for site name and slogan. + $edit = array( + '0[name][translation]' => $site_name, + '0[slogan][translation]' => $site_slogan, + ); + $this->drupalPost($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('edit', 'fr')); + $config_parsed = $file_storage->read('locale.config.fr.system.site'); + + // Expect no language specific file. + $this->assertFalse($config_parsed); + } + + /** + * Tests the contact category translation. + */ + function testContactConfigEntityTranslation() { + $this->drupalGet('admin/structure/contact'); + + // Check for default contact form configuration entity from Contact module. + $this->assertLinkByHref('admin/structure/contact/manage/feedback'); + + // Save default language configuration. + $edit = array( + 'label' => 'Send your feedback', + 'recipients' => 'sales@example.com,support@example.com', + 'reply' => 'Thank you for your mail' + ); + $this->drupalPost('admin/structure/contact/manage/feedback', $edit, t('Save')); + + // Visit the form to confirm the changes. + $this->drupalGet('contact/feedback'); + $this->assertText('Send your feedback'); + + foreach ($this->langcodes as $langcode) { + // Update translatable fields. + $edit = array( + '0[label][translation]' => 'Website feedback - ' . $langcode, + '0[reply][translation]' => 'Thank you for your mail - ' . $langcode + ); + + // Save language specific version of form. + $this->drupalPost('admin/structure/contact/manage/feedback/translate', $edit, t('Save translation'), $this->getUrlOptions('add', $langcode)); + + // Visit language specific version of form to check label exists. + $this->drupalGet($langcode . '/contact/feedback'); + $this->assertText('Website feedback - ' . $langcode); + + // Submit feedback. + $edit = array( + 'subject' => 'Test subject', + 'message' => 'Test message' + ); + $this->drupalPost(NULL, $edit, t('Send message')); + } + + // We get all emails so no need to check inside the loop. + $captured_emails = $this->drupalGetMails(); + + // Check language specific auto reply text in email body. + foreach ($captured_emails as $email) { + if ($email['id'] == 'contact_page_autoreply') { + // Trim because we get an added newline for the body. + $this->assertEqual(trim($email['body']), 'Thank you for your mail - ' . $email['langcode']); + } + } + } + + /** + * Tests the account settings translation interface. + * + * This is the only special case so far where we have multiple configuration + * names involved building up one configuration translation form. Test that + * the translations are saved for all configuration names properly. + */ + function testAccountSettingsConfigurationTranslation() { + $this->drupalGet('admin/config/people/accounts/translate'); + $this->assertLinkByHref('admin/config/people/accounts/translate?action=add&langcode=fr'); + + // Update account settings fields for French. + $edit = array( + '0[anonymous][translation]' => 'Anonyme', + '1[status_blocked][status_blocked.subject][translation]' => 'Testing, your account is blocked.', + '1[status_blocked][status_blocked.body][translation]' => 'Testing account blocked body.' + ); + + $this->drupalPost('admin/config/people/accounts/translate', $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + + // Make sure the changes are saved and loaded back properly. + $this->drupalGet('admin/config/people/accounts/translate', $this->getUrlOptions('edit', 'fr')); + foreach ($edit as $value) { + $this->assertRaw($value); + } + } + + /** + * Tests source and target language edge cases. + */ + function testSourceAndTargetLanguage() { + // Loading translation page for not-specified language (und) + // should return 403. + $this->drupalGet('admin/config/system/site-information/translate', $this->getUrlOptions('add', 'und')); + $this->assertResponse(403); + + // Check the source language doesn't have 'Add' or 'Delete' link and + // make sure source language edit goes to original configuration page + // not the translation specific edit page. + $this->drupalGet('admin/config/system/site-information/translate'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate?action=edit&langcode=en'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate?action=add&langcode=en'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate?action=delete&langcode=en'); + $this->assertLinkByHref('admin/config/system/site-information'); + + // Translation addition to source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate', $this->getUrlOptions('add', 'en')); + $this->assertResponse(403); + + // Translation editing in source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate', $this->getUrlOptions('edit', 'en')); + $this->assertResponse(403); + + // Translation deletion in source language should return 403. + $this->drupalGet('admin/config/system/site-information/translate', $this->getUrlOptions('delete', 'en')); + $this->assertResponse(403); + + // Set default language of site information to not-specified language (und). + config('system.site') + ->set('langcode', Language::LANGCODE_NOT_SPECIFIED) + ->save(); + + // Make sure translation tab does not exist on the configuration page. + $this->drupalGet('admin/config/system/site-information'); + $this->assertNoLinkByHref('admin/config/system/site-information/translate'); + + // If source language is not specified, translation page should be 403. + $this->drupalGet('admin/config/system/site-information/translate'); + $this->assertResponse(403); + } + + /** + * Tests the views translation interface. + */ + function testViewsTranslationUI() { + $description = 'A list of nodes marked for display on the front page.'; + $human_readable_name = 'Frontpage'; + $display_settings_master = 'Master'; + $display_options_master = '(Empty)'; + $translation_base_url = 'admin/structure/views/view/frontpage/translate'; + + $this->drupalGet($translation_base_url); + + // Check 'Add' link of French to visit add page. + $this->assertLinkByHref($translation_base_url . '?action=add&langcode=fr'); + $this->clickLink(t('Add')); + + // Make sure original text is present on this page. + $this->assertRaw($description); + $this->assertRaw($human_readable_name); + + // Update Views Fields for French. + $edit = array( + '0[description][translation]' => $description . " FR", + '0[label][translation]' => $human_readable_name . " FR", + '0[display][default][display.default.display_title][translation]' => $display_settings_master . " FR", + '0[display][default][display_options][display.default.display_options.title][translation]' => $display_options_master . " FR", + ); + $this->drupalPost($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + $this->assertRaw(t('Updated French configuration translations successfully.')); + + // Check for edit, delete links (and no 'add' link) for French language. + $this->assertNoLinkByHref($translation_base_url . '?action=add&langcode=fr'); + $this->assertLinkByHref($translation_base_url . '?action=edit&langcode=fr'); + $this->assertLinkByHref($translation_base_url . '?action=delete&langcode=fr'); + + // Check translation saved proper. + $this->drupalGet($translation_base_url, $this->getUrlOptions('edit', 'fr')); + $this->assertFieldByName('0[description][translation]', $description . " FR"); + $this->assertFieldByName('0[label][translation]', $human_readable_name . " FR"); + $this->assertFieldByName('0[display][default][display.default.display_title][translation]', $display_settings_master . " FR"); + $this->assertFieldByName('0[display][default][display_options][display.default.display_options.title][translation]', $display_options_master . " FR"); + } + + /** + * Test translation storage in locale storage. + */ + function testLocaleDBStorage() { + $langcode = 'xx'; + $name = $this->randomName(16); + $edit = array( + 'predefined_langcode' => 'custom', + 'langcode' => $langcode, + 'name' => $name, + 'direction' => '0', + ); + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add custom language')); + + // Make sure there is no translation stored in locale storage before edit. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertTrue(empty($translation)); + + // Add custom translation. + $edit = array( + '0[anonymous][translation]' => 'Anonyme', + ); + $this->drupalPost('admin/config/people/accounts/translate', $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + + // Make sure translation stored in locale storage after saved language + // specific configuration translation. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertEqual('Anonyme', $translation->getString()); + + // revert custom translations to base translation. + $edit = array( + '0[anonymous][translation]' => 'Anonymous', + ); + $this->drupalPost('admin/config/people/accounts/translate', $edit, t('Save translation'), $this->getUrlOptions('edit', 'fr')); + + // Make sure there is no translation stored in locale storage after revert. + $translation = $this->getTranslation('user.settings', 'anonymous', 'fr'); + $this->assertEqual('Anonymous', $translation->getString()); + } + + /** + * Tests the single language existing. + */ + function testSingleLanguageUI() { + // Delete French language + $this->drupalPost('admin/config/regional/language/delete/fr', array(), t('Delete')); + $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'French', '%langcode' => 'fr'))); + + // Change default language to Tamil. + $edit = array( + 'site_default_language' => 'ta', + ); + $this->drupalPost('admin/config/regional/settings', $edit, t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.')); + + // Delete English language + $this->drupalPost('admin/config/regional/language/delete/en', array(), t('Delete')); + $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'English', '%langcode' => 'en'))); + + // Visit account setting translation page, this should not + // throw any notices. + $this->drupalGet('admin/config/people/accounts/translate'); + $this->assertResponse(200); + } + + /** + * Gets translation from locale storage. + * + * @param $config_name + * Configuration object. + * @param $key + * Translation configuration field key. + * @param $langcode + * String language code to load translation. + * + * @return bool|mixed + * Returns translation if exists, FALSE otherwise. + */ + protected function getTranslation($config_name, $key, $langcode) { + $settings_locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $config_name)); + $this->assertTrue(!empty($settings_locations), format_string('Configuration locations found for %config_name.', array('%config_name' => $config_name))); + + if (!empty($settings_locations)) { + $source = $this->container->get('config.factory')->get($config_name)->get($key); + $source_string = $this->localeStorage->findString(array('source' => $source, 'type' => 'configuration')); + $this->assertTrue(!empty($source_string), format_string('Found string for %config_name.%key.', array('%config_name' => $config_name, '%key' => $key))); + + if (!empty($source_string)) { + $conditions = array( + 'lid' => $source_string->lid, + 'language' => $langcode, + ); + $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE)); + return reset($translations); + } + } + return FALSE; + } + + /** + * Sets site name and slogan for default language, helps in tests. + * + * @param string $site_name + * @param string $site_slogan + */ + protected function setSiteInformation($site_name, $site_slogan) { + $edit = array( + 'site_name' => $site_name, + 'site_slogan' => $site_slogan + ); + $this->drupalPost('admin/config/system/site-information', $edit, t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.')); + } + + /** + * Provides URL query string for given action and langcode. + * + * @param string $action + * Performed action. Allowed values are add, edit, delete. + * @param string $langcode + * The Language ID. + * + * @return array + * Array in query format of an URL. + */ + protected function getUrlOptions($action, $langcode) { + return array( + 'query' => array( + 'action' => $action, + 'langcode' => $langcode + ) + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUITest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUITest.php new file mode 100644 index 0000000..326f5e0 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUITest.php @@ -0,0 +1,67 @@ +drupalCreateUser($permissions); + $this->drupalLogin($user); + } + + public static function getInfo() { + return array( + 'name' => 'Configuration Translation view list', + 'description' => 'Visit view list and test if translate is available.', + 'group' => 'Configuration Translation', + ); + } + + /* + * Tests views_ui list to see if translate link is added to operations. + */ + function testTranslateOperationInViewListUI() { + // Views UI List 'admin/structure/views'. + $this->drupalGet('admin/structure/views'); + $translate_link = 'admin/structure/views/view/test_view/translate'; + // Test if the link to translate the test_view is on the page. + $this->assertLinkByHref($translate_link); + + // Test if the link to translate actually goes to the translate page. + $this->drupalGet($translate_link); + $this->assertRaw('' . t('Language') . ''); + } + +}