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..e448647 --- /dev/null +++ b/core/modules/config_translation/config_translation.api.php @@ -0,0 +1,79 @@ +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'])) { + $info[$entity_type . '_fields'] = array( + 'type' => 'entity', + 'base_path' => $entity_info['route_base_path'] . '/fields/{field_instance}', + 'entity_type' => 'field_instance', + 'title' => t('!label field'), + 'menu_item_type' => 'MENU_CALLBACK', + ); + } + } +} + +/** + * Alter existing translation tabs for translation of configuration. + * + * This hook is useful to extend existing configuration mappers with new + * configuration names, for example when altering existing forms with new + * settings stored elsewhere. This allows the translation experience to also + * reflect the compound form element in one screen. + * + * @param array $info + * An associative array of discovered configuration mappers. Use an entity + * name for the key (for entity mapping) or a unique string for configuration + * name list mapping. The values of the associative array are arrays + * themselves in the same structure as the *.configuration_translation.yml + * files. + * + * @see hook_translation_info() + * @see \Drupal\config_translation\ConfigMapperManager + */ +function hook_config_translation_info_alter(&$info) { + // Make all translation tabs the default local tasks. + foreach ($info as &$mapping) { + $mapping['menu_item_type'] = MENU_DEFAULT_LOCAL_TASK; + } +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/core/modules/config_translation/config_translation.config_translation.yml b/core/modules/config_translation/config_translation.config_translation.yml new file mode 100644 index 0000000..21d048c --- /dev/null +++ b/core/modules/config_translation/config_translation.config_translation.yml @@ -0,0 +1,32 @@ +maintenance: + type: names + base_path: 'admin/config/development/maintenance' + names: + - 'system.maintenance' + title: 'System maintenance' + class: '\Drupal\config_translation\ConfigNamesMapper' + +site_information: + type: names + base_path: 'admin/config/system/site-information' + names: + - 'system.site' + title: 'System information' + class: '\Drupal\config_translation\ConfigNamesMapper' + +rss_publishing: + type: names + base_path: 'admin/config/services/rss-publishing' + names: + - 'system.rss' + title: 'RSS publishing' + class: '\Drupal\config_translation\ConfigNamesMapper' + +people: + type: names + base_path: 'admin/config/people/accounts' + names: + - 'user.settings' + - 'user.mail' + title: 'Account settings' + class: '\Drupal\config_translation\ConfigNamesMapper' 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.local_tasks.yml b/core/modules/config_translation/config_translation.local_tasks.yml new file mode 100644 index 0000000..2726da2 --- /dev/null +++ b/core/modules/config_translation/config_translation.local_tasks.yml @@ -0,0 +1,3 @@ +config_translation.local_tasks: + derivative: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks' + weight: 100 diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module new file mode 100644 index 0000000..c874a7b --- /dev/null +++ b/core/modules/config_translation/config_translation.module @@ -0,0 +1,159 @@ +' . t('About') . ''; + $output .= '

' . t('The Configuration Translation module allows configurations to be translated into different languages. Views, your site name, contact module categories, vocabularies, menus, blocks, and so on are all stored within the 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.') . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Translating') . '
'; + $output .= '
' . t('To translate configuration items, select the translate tab when viewing the configuration, select the language for which you wish to provide translations and then enter the content.') . '
'; + $output .= '
'; + return $output; + + case 'admin/config/regional/config-translation': + $output = '

' . t('This page lists all configuration items on your site which have translatable text, like your site name, role names, etc.') . '

'; + return $output; + } +} + +/** + * Implements hook_menu(). + */ +function config_translation_menu() { + $items = array(); + + $items['admin/config/regional/config-translation'] = array( + 'title' => 'Configuration translation', + 'description' => 'Translate the configuration.', + 'route_name' => 'config_translation.mapper_list', + 'weight' => 30, + ); + + // 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. + $mappers = \Drupal::service('plugin.manager.config_translation')->getMappers(); + foreach ($mappers as $mapper) { + $path = $mapper->getBasePathPattern(); + $items[$path . '/translate'] = array( + 'title callback' => 'config_translation_item_title', + 'title arguments' => array(drupal_strtolower($mapper->getTypeName())), + 'route_name' => $mapper->getRouteName(), + 'type' => $mapper->getMenuItemType(), + 'context' => MENU_CONTEXT_INLINE, + '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.'), + ), + ); +} + +/** + * Translation tab title callback. + * + * @param string $type_name + * Type name to be included in title. + */ +function config_translation_item_title($type_name) { + return t('Translate @title', array('@title' => $type_name)); +} + +/** + * Implements hook_theme(). + */ +function config_translation_theme() { + return array( + 'config_translation_manage_form_element' => array( + 'render element' => 'element', + 'template' => 'config_translation_manage_form_element', + ), + ); +} + +/** + * Implements hook_config_translation_info(). + */ +function config_translation_config_translation_info(&$info) { + // Add fields entity mappers to all fieldable entity types defined. + $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'])) { + $info[$entity_type . '_fields'] = array( + 'type' => 'entity', + 'base_path' => $entity_info['route_base_path'] . '/fields/{field_instance}', + 'route_name' => 'config_translation.item.field_ui.instance_edit_' . $entity_type, + 'entity_type' => 'field_instance', + 'title' => t('!label field'), + 'menu_item_type' => 'MENU_CALLBACK', + 'class' => '\Drupal\config_translation\ConfigEntityMapper', + 'base_entity_type' => $entity_type, + 'list_controller' => '\Drupal\config_translation\Controller\ConfigTranslationFieldInstanceListController', + ); + } + } +} + +/** + * Implements hook_entity_operation_alter(). + */ +function config_translation_entity_operation_alter(array &$operations, \Drupal\Core\Entity\EntityInterface $entity) { + if (\Drupal::currentUser()->hasPermission('translate configuration')) { + $uri = $entity->uri(); + $operations['translate'] = array( + 'title' => t('Translate'), + 'href' => $uri['path'] . '/translate', + 'options' => $uri['options'], + 'weight' => 50, + ); + } +} + +/** + * Implements hook_config_type_info_alter(). + */ +function config_translation_config_type_info_alter(&$definitions) { + // Enhance the config date type definition with a class to generate proper + // form elements in ConfigTranslationFormBase. + $definitions['text']['form_element_class'] = '\Drupal\config_translation\FormElement\Textarea'; + $definitions['date_format']['form_element_class'] = '\Drupal\config_translation\FormElement\DateFormat'; +} + +/** + * Implements hook_library_info(). + */ +function config_translation_library_info() { + $libraries['drupal.config_translation.admin'] = array( + 'title' => 'Configuration translation admin', + 'version' => \Drupal::VERSION, + 'css' => array( + drupal_get_path('module', 'config_translation') . '/config_translation.admin.css' => array(), + ), + ); + return $libraries; +} diff --git a/core/modules/config_translation/config_translation.routing.yml b/core/modules/config_translation/config_translation.routing.yml new file mode 100644 index 0000000..dd6b71f --- /dev/null +++ b/core/modules/config_translation/config_translation.routing.yml @@ -0,0 +1,14 @@ +config_translation.mapper_list: + path: '/admin/config/regional/config-translation' + defaults: + _title: 'Configuration translation' + _content: '\Drupal\config_translation\Controller\ConfigTranslationMapperList::render' + requirements: + _permission: 'translate configuration' + +config_translation.entity_list: + path: '/admin/config/regional/config-translation/{config_translation_mapper}' + defaults: + _content: '\Drupal\config_translation\Controller\ConfigTranslationListController::listing' + requirements: + _permission: 'translate configuration' 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..4be3911 --- /dev/null +++ b/core/modules/config_translation/config_translation.services.yml @@ -0,0 +1,20 @@ +services: + config_translation.subscriber: + class: Drupal\config_translation\Routing\RouteSubscriber + arguments: ['@plugin.manager.config_translation'] + tags: + - { name: event_subscriber } + config_translation.access_check: + class: Drupal\config_translation\Access\ConfigNameCheck + arguments: ['@plugin.manager.config_translation', '@current_user'] + tags: + - { name: access_check } + plugin.manager.config_translation: + class: Drupal\config_translation\ConfigMapperManager + arguments: + - '@cache.cache' + - '@language_manager' + - '@module_handler' + - '@entity.manager' + - '@config.typed' + - '@string_translation' 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..c9b9dc9 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigNameCheck.php @@ -0,0 +1,84 @@ +configMapperManager = $config_mapper_manager; + $this->account = $account; + } + + /** + * {@inheritdoc} + */ + public function applies(Route $route) { + return array_key_exists('_config_translation_config_name_access', $route->getRequirements()); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request) { + $mapper_plugin = $route->getDefault('mapper_plugin'); + $mapper = $this->configMapperManager->createInstance($mapper_plugin['plugin_id'], $mapper_plugin['plugin_definition']); + $mapper->populateFromRequest($request); + + $source_language = $mapper->getLanguageWithFallback(); + $target_language = NULL; + if ($request->query->has('langcode')) { + $target_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 ( + $this->account->hasPermission('translate configuration') && + $mapper->hasSchema() && + $mapper->hasTranslatable() && + !$source_language->locked && + (empty($target_language) || (!$target_language->locked && $target_language->id != $source_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..74d196c --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php @@ -0,0 +1,226 @@ +setType($plugin_definition['entity_type']); + + $this->entityManager = $entity_manager; + // @todo Remove this when https://drupal.org/node/2095203 gets in. + $this->entityManager->_serviceId = 'entity.manager'; + + // Field instances are grouped by the entity type they are attached to. + // Create a useful label from the entity type they are attached to. + if ($plugin_definition['entity_type'] == 'field_instance') { + $base_entity_type = $this->entityManager->getDefinition($plugin_definition['base_entity_type']); + $this->typeLabel = t('@label fields', array('@label' => $base_entity_type['label'])); + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static ( + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('locale.config.typed'), + $container->get('plugin.manager.config_translation'), + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function populateFromRequest(Request $request) { + $entity = $request->attributes->get($this->entityType); + $this->setEntity($entity); + } + + /** + * Sets the entity instance for this mapper. + * + * This method can only be invoked when the concrete entity is known, that is + * in a request for an entity translation path. After this method is called, + * the mapper is fully populated with the proper display title and + * configuration names to use to serve to check permissions or display a + * translation screen. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to set. + * + * @return bool + * TRUE, if the entity was set successfully; FALSE otherwise. + */ + public function setEntity(EntityInterface $entity) { + if (isset($this->entity)) { + return FALSE; + } + + $this->entity = $entity; + + // Replace title placeholder with entity label. It is later escaped for + // display. + $this->title = t($this->title, array('!label' => $entity->label())); + + // Add the list of configuration IDs belonging to this entity. We add on a + // possibly existing list of names. This allows modules to alter the entity + // page with more names if form altering added more configuration to an + // entity. This is not a Drupal 8 best practice (ideally the configuration + // would have pluggable components), but this may happen as well. + $entity_type_info = $this->entityManager->getDefinition($this->entityType); + $this->names = array_merge($this->names, array($entity_type_info['config_prefix'] . '.' . $entity->id())); + + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function getRouteParams() { + return array($this->entityType => $this->entity->id()); + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return str_replace('{' . $this->entityType . '}', $this->entity->id(), $this->basePathPattern); + } + + /** + * Set entity type for this mapper. + * + * This should be set in initialization. A mapper that knows its type but + * not yet its names is still useful for router item and tab generation. The + * concrete entity only turns out later with actual controller invocations, + * when the setEntity() method is invoked before the rest of the methods are + * used. + * + * @param string $entity_type + * The entity type to set. + * + * @return bool + * TRUE if the entity type was set correctly; FALSE otherwise. + */ + public function setType($entity_type) { + if (isset($this->entityType)) { + return FALSE; + } + $this->entityType = $entity_type; + return TRUE; + } + + /** + * Gets the entity type from this mapper. + * + * @return string + */ + public function getType() { + return $this->entityType; + } + + /** + * {@inheritdoc} + */ + public function getTypeName() { + $entity_type_info = $this->entityManager->getDefinition($this->entityType); + return $entity_type_info['label']; + } + + /** + * {@inheritdoc} + */ + public function getTypeLabel() { + // The typeLabel is used to override the default entity type label in + // configuration translation UI. It is used to distinguish field instances + // from each other, but also can easily override in other mapper + // implementations. + if (isset($this->typeLabel)) { + return $this->typeLabel; + } + + $entityType = $this->entityManager->getDefinition($this->entityType); + return $entityType['label']; + } + + /** + * {@inheritdoc} + */ + public function getOperations() { + return array( + '#type' => 'operations', + '#links' => array( + 'list' => array( + 'title' => t('List'), + 'href' => 'admin/config/regional/config-translation/' . $this->getId(), + ), + ), + ); + } + +} 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..82ea6e8 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php @@ -0,0 +1,171 @@ + 'MENU_LOCAL_TASK', + ); + + /** + * Creates a config mapper manager instance. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + * @param \Drupal\Core\Language\LanguageManager $language_manager + * The language manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * The entity manager. + * @param \Drupal\Core\Config\TypedConfigManager + * The typed config manager. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager + * The string translation manager. + */ + public function __construct(CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler, EntityManager $entity_manager, TypedConfigManager $typed_config_manager, TranslationInterface $translation_manager) { + $this->entityManager = $entity_manager; + $this->typedConfigManager = $typed_config_manager; + $this->translationManager = $translation_manager; + + $this->factory = new ContainerFactory($this); + + // Let others alter definitions with hook_config_translation_info_alter(). + $this->alterInfo($module_handler, 'config_translation_info'); + $this->setCacheBackend($cache_backend, $language_manager, 'config_translation_info'); + } + + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $plugin_definition = array()) { + return $this->factory->createInstance($plugin_id, array()); + } + + /** + * Returns a list of all mappers found. + */ + public function getMappers() { + $mappers = array(); + foreach($this->getDefinitions() as $id => $definition) { + $mappers[$definition['base_path']] = $this->createInstance($id, $definition); + } + + return $mappers; + } + + /** + * Finds plugin definitions. + * + * @return array + * List of definitions to store in cache. + */ + protected function findDefinitions() { + $definitions = $this->findConfigEntityDefinitions(); + + // Look at all themes and modules. + $directories = array(); + foreach ($this->moduleHandler->getModuleList() as $module => $filename) { + $directories[$module] = dirname($filename); + } + foreach (list_themes() as $theme_id => $theme) { + $directories[$theme->name] = drupal_get_path('theme', $theme->name); + } + + // Check for files named MODULE.config_translation.yml and + // THEME.config_translation.yml in module/theme roots. + $discovery = new YamlDiscovery('config_translation', $directories); + $discovery = new InfoHookDecorator($discovery, 'config_translation_info'); + $discovery = new ContainerDerivativeDiscoveryDecorator($discovery); + $definitions += $discovery->getDefinitions(); + + foreach ($definitions as $plugin_id => &$definition) { + $this->processDefinition($definition, $plugin_id); + } + if ($this->alterHook) { + $this->moduleHandler->alter($this->alterHook, $definitions); + } + return $definitions; + } + + /** + * Finds plugin definitions by scanning configuration entity information. + */ + protected function findConfigEntityDefinitions() { + $definitions = array(); + foreach($this->entityManager->getDefinitions() as $entity_type => $entity_info) { + // Determine base path for entities automatically if provided via the + // configuration entity. + if ( + !in_array('Drupal\Core\Config\Entity\ConfigEntityInterface', class_implements($entity_info['class'])) || + !isset($entity_info['links']['edit-form']) + ) { + // Do not record this entity mapper if the entity type does not + // provide a base path. We'll surely not be able to do anything with + // it anyway. Configuration entities with a dynamic base path, such as + // field instances, need special treatment. See below. + continue; + } + + $base_path = $entity_info['links']['edit-form']; + + // Use the entity type as the plugin ID. + $definitions[$entity_type] = array( + 'class' => '\Drupal\config_translation\ConfigEntityMapper', + 'base_path' => $base_path, + // Set title unescaped. It is later escaped for display. The !label + // gets replaced later on when the entity instance is set. + 'title' => $this->t('!label !entity_type', array('!entity_type' => Unicode::strtolower($entity_info['label']))), + 'names' => array(), + 'entity_type' => $entity_type, + ); + + if ($entity_type == 'block') { + $definitions[$entity_type]['list_controller'] = '\Drupal\config_translation\Controller\ConfigTranslationBlockListController'; + } + } + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function hasTranslatable($name) { + return $this->findTranslatable($this->typedConfigManager->get($name)); + } + + /** + * Returns TRUE if at least one translatable element is found. + * + * @param \Drupal\Core\TypedData\TypedDataInterface $element + * Configuration schema element. + * + * @return bool + * A boolean indicating if there is at least one translatable element. + */ + protected function findTranslatable(TypedDataInterface $element) { + // In case this is a sequence or a mapping check whether any child element + // is translatable. + if ($element instanceof ArrayElement) { + foreach ($element as $child_element) { + if ($this->findTranslatable($child_element)) { + return TRUE; + } + } + // If none of the child elements are translatable, return FALSE. + return FALSE; + } + else { + $definition = $element->getDefinition(); + return isset($definition['translatable']) && $definition['translatable']; + } + } + + + /** + * Translates a string to the current language or to a given language. + * + * See the t() documentation for details. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translationManager->translate($string, $args, $options); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php new file mode 100644 index 0000000..6bf05af --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php @@ -0,0 +1,25 @@ +basePathPattern = $plugin_definition['base_path']; + $this->title = $plugin_definition['title']; + $this->names = isset($plugin_definition['names']) ? $plugin_definition['names'] : array(); + $this->menuItemType = constant($plugin_definition['menu_item_type']); + $this->id = $plugin_id; + $this->pluginDefinition = $plugin_definition; + + // Set route name based on base path if not provided. + // @todo: only take route name and compute everything from there. + if (!empty($plugin_definition['route_name'])) { + $this->routeName = $plugin_definition['route_name']; + } + else { + $this->routeName = $this->setRouteName($this->basePathPattern); + } + + $this->configFactory = $config_factory; + $this->localeConfigManager = $locale_config_manager; + $this->configMapperManager = $config_mapper_manager; + + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static ( + $plugin_id, + $plugin_definition, + $container->get('config.factory'), + $container->get('locale.config.typed'), + $container->get('plugin.manager.config_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getId() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function populateFromRequest(Request $request) { + // A name mapper is fully populated without request data. + } + + /** + * {@inheritdoc} + */ + public function getBasePathPattern() { + return $this->basePathPattern; + } + + /** + * {@inheritdoc} + */ + public function getBasePath() { + return $this->basePathPattern; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function getNames() { + return $this->names; + } + + /** + * {@inheritdoc} + */ + public function getMenuItemType() { + return $this->menuItemType; + } + + /** + * Returns the label of the mapper. + * + * @return string + */ + public function getTypeLabel() { + return $this->title; + } + + /** + * {@inheritdoc} + */ + public function getLangcode() { + $config_factory = $this->configFactory; + $langcodes = array_map(function($name) use ($config_factory) { + return $config_factory->get($name)->get('langcode') ?: 'en'; + }, $this->names); + + if (count(array_unique($langcodes)) > 1) { + throw new \RuntimeException('A config mapper can only contain configuration for a single language.'); + } + + return reset($langcodes); + } + + /** + * {@inheritdoc} + */ + 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; + } + + /** + * {@inheritdoc} + */ + public function getConfigData() { + $config_data = array(); + foreach ($this->names as $name) { + $config_data[$name] = $this->configFactory->get($name)->get(); + } + return $config_data; + } + + /** + * {@inheritdoc} + */ + public function hasSchema() { + foreach ($this->names as $name) { + if (!$this->localeConfigManager->hasConfigSchema($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function hasTranslatable() { + foreach ($this->names as $name) { + if (!$this->configMapperManager->hasTranslatable($name)) { + return FALSE; + } + } + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function addName($name) { + $this->names[] = $name; + } + + /** + * {@inheritdoc} + */ + public function hasTranslation(Language $language) { + foreach ($this->names as $name) { + if ($this->localeConfigManager->hasTranslation($name, $language)) { + return TRUE; + } + } + return FALSE; + } + + /** + * Sets route name for the mapper. + * + * @param string $base_path + * Base path of the route. + * + * @return string + * Route name for the mapper. + */ + protected function setRouteName($base_path){ + $route_provider = \Drupal::service('router.route_provider'); + if ($suffix = key($route_provider->getRoutesByPattern('/' . $base_path)->all())) { + return "config_translation.item.$suffix"; + } + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->routeName; + } + + /** + * {@inheritdoc} + */ + public function getTypeName() { + return t('Settings'); + } + + /** + * {@inheritdoc} + */ + public function getRoutes() { + $path = $this->getBasePathPattern(); + $name = $this->getRouteName(); + return array( + $name => new Route($path . '/translate', + array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage', + 'mapper_plugin' => array('plugin_id' => $this->getId(), 'plugin_definition' => $this->pluginDefinition), + ), + array( + '_config_translation_config_name_access' => 'TRUE', + ) + ), + ); + return array(); + } + + /** + * {@inheritdoc} + */ + public function getRouteParams() { + return array(); + } + + /** + * Returns the config mapper weight. + * + * @return int + */ + public function getWeight() { + return $this->weight; + } + + /** + * Provides an array of information to build a list of operation links. + * + * @return array + * An associative array of operation link data for this list, keyed by + * operation name, containing the following key-value pairs: + * - title: The localized title of the operation. + * - href: The path for the operation. + * - options: An array of URL options for the path. + * - weight: The weight of this operation. + */ + public function getOperations() { + return array( + '#type' => 'operations', + '#links' => array( + 'translate' => array( + 'title' => t('Translate'), + 'href' => $this->getBasePath() . '/translate', + ), + ), + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php new file mode 100644 index 0000000..1f02522 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php @@ -0,0 +1,101 @@ +themes = list_themes(); + } + + /** + * {@inheritdoc} + */ + public function getFilterLabels() { + $info = parent::getFilterLabels(); + + $info['placeholder'] = $this->t('Enter block, theme or category'); + $info['description'] = $this->t('Enter a part of the block, theme or category to filter by.'); + + return $info; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $theme = $entity->get('theme'); + $plugin_definition = $entity->getPlugin()->getPluginDefinition(); + + $row['label'] = array( + 'data' => String::checkPlain($entity->label()), + 'class' => 'table-filter-text-source', + ); + + $row['theme'] = array( + 'data' => String::checkPlain($this->themes[$theme]->info['name']), + 'class' => 'table-filter-text-source', + ); + + $row['category'] = array( + 'data' => String::checkPlain($plugin_definition['category']), + 'class' => 'table-filter-text-source', + ); + + $row['operations']['data'] = $this->buildOperations($entity); + + return $row; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = t('Block'); + $header['theme'] = t('Theme'); + $header['category'] = t('Category'); + $header['operations'] = t('Operations'); + return $header; + } + + /** + * Sorts an array by value. + * + * @param array $a + * First item for comparison. + * @param array $b + * Second item for comparison. + * + * @return int + * The comparison result for uasort(). + */ + public function sortRows($a, $b) { + return $this->sortRowsMultiple($a, $b, array('theme', 'category', 'label')); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php new file mode 100644 index 0000000..6a27598 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php @@ -0,0 +1,182 @@ +configMapperManager = $config_mapper_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('plugin.manager.config_translation')); + } + + /** + * Language translations overview page for a configuration name. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * Page request object. + * @param array $mapper_plugin + * An array of plugin details with the following keys: + * - plugin_id: The plugin ID of the mapper. + * - plugin_definition: An array of mapper details with the following keys: + * - base_path_pattern: Base path pattern to attach + * the translation user interface to. + * - title: The title for translation editing screen. + * - names: The list of configuration names for this mapper. + * - menu_item_type: (optional) The menu item type to add. + * - entity_type: (optional) The type of the entity. + * + * @return array + * Page render array. + */ + public function itemPage(Request $request, array $mapper_plugin) { + /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */ + $mapper = $this->configMapperManager->createInstance($mapper_plugin['plugin_id'], $mapper_plugin['plugin_definition']); + $mapper->populateFromRequest($request); + + // This is only necessary as we have to provide multiple forms and pages + // on a single route due to the hook_menu() parent limitation. + if ($request->query->has('action') && $request->query->has('langcode') ) { + $container = \Drupal::getContainer(); + switch ($request->query->get('action')) { + case 'add': + $form = ConfigTranslationAddForm::create($container); + break; + + case 'edit': + $form = ConfigTranslationEditForm::create($container); + break; + + case 'delete': + $form = ConfigTranslationDeleteForm::create($container); + break; + + default: + throw new NotFoundHttpException(); + } + + $target_language = language_load($request->query->get('langcode')); + if (!$target_language) { + throw new NotFoundHttpException(); + } + return drupal_get_form($form, $mapper, $target_language); + } + + $page = array(); + $page['#title'] = $this->t('Translations for %label', array('%label' => $mapper->getTitle())); + + // 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 = $mapper->getLangcode(); + if (!isset($languages[$original_langcode])) { + $language_name = language_name($original_langcode); + if ($original_langcode == 'en') { + $language_name = $this->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 = $mapper->getBasePath(); + $header = array($this->t('Language'), $this->t('Operations')); + $page['languages'] = array( + '#type' => 'table', + '#header' => $header, + ); + foreach ($languages as $language) { + if ($language->id == $original_langcode) { + $page['languages'][$language->id]['language'] = array( + '#markup' => '' . $this->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' => $this->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 (!$mapper->hasTranslation($language)) { + $operations['add'] = array( + 'title' => $this->t('Add'), + 'href' => $path . '/translate', + 'query' => array('action' => 'add') + $path_options, + ); + } + else { + // Otherwise, link to edit the existing translation. + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'href' => $path . '/translate', + 'query' => array('action' => 'edit') + $path_options, + ); + $operations['delete'] = array( + 'title' => $this->t('Delete'), + 'href' => $path . '/translate', + 'query' => array('action' => 'delete') + $path_options, + ); + } + + $page['languages'][$language->id]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + } + return $page; + } +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php new file mode 100644 index 0000000..5f834fc --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php @@ -0,0 +1,139 @@ + $this->t('Enter label'), + 'description' => $this->t('Enter a part of the label or description to filter by.'), + ); + } + + /** + * {@inheritdoc} + */ + public function render() { + $table = parent::render(); + $filter = $this->getFilterLabels(); + + usort($table['#rows'], array($this, 'sortRows')); + + $build['filters'] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array('table-filter', 'js-show'), + ), + ); + + $build['filters']['text'] = array( + '#type' => 'search', + '#title' => $this->t('Search'), + '#size' => 30, + '#placeholder' => $filter['placeholder'], + '#attributes' => array( + 'class' => array('table-filter-text'), + 'data-table' => '.config-translation-entity-list', + 'autocomplete' => 'off', + 'title' => $filter['description'], + ), + ); + + $build['table'] = $table; + $build['table']['#attributes']['class'][] = 'config-translation-entity-list'; + $build['#attached']['library'][] = array('system', 'drupal.system.modules'); + + return $build; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label']['data'] = String::checkPlain($entity->label()); + $row['label']['class'] = 'table-filter-text-source'; + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = t('Label'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildOperations(EntityInterface $entity) { + $operations = parent::buildOperations($entity); + foreach (array_keys($operations['#links']) as $operation) { + // This is a translation UI for translators. Show the translation + // operation only. + if (!($operation == 'translate')) { + unset($operations['#links'][$operation]); + } + } + return $operations; + } + + /** + * Sorts an array by value. + * + * @param array $a + * First item for comparison. + * @param array $b + * Second item for comparison. + * + * @return int + * The comparison result for uasort(). + */ + public function sortRows($a, $b) { + return $this->sortRowsMultiple($a, $b, array('label')); + } + + /** + * Sorts an array by multiple criteria. + * + * @param array $a + * First item for comparison. + * @param array $b + * Second item for comparison. + * @param array $keys + * The array keys to sort on. + * + * @return int + * The comparison result for uasort(). + */ + public function sortRowsMultiple($a, $b, $keys) { + $key = array_shift($keys); + $a_value = (is_array($a) && isset($a[$key]['data'])) ? $a[$key]['data'] : ''; + $b_value = (is_array($b) && isset($b[$key]['data'])) ? $b[$key]['data'] : ''; + + if ($a_value == $b_value && !empty($keys)) { + return $this->sortRowsMultiple($a, $b, $keys); + } + + return strnatcasecmp($a_value, $b_value); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php new file mode 100644 index 0000000..bf52727 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php @@ -0,0 +1,196 @@ +get('entity.manager')->getStorageController($entity_type), + $container->get('module_handler'), + $container->get('entity.manager'), + $definition + ); + } + + /** + * Constructs a new EntityListController object. + * + * @param string $entity_type + * The type of entity to be listed. + * @param array $entity_info + * An array of entity info for the entity type. + * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage + * The entity storage controller class. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke hooks on. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * The entity manager. + * @param $definition + * The plugin definition of the config translation mapper. + */ + public function __construct($entity_type, array $entity_info, EntityStorageControllerInterface $storage, ModuleHandlerInterface $module_handler, EntityManager $entity_manager, array $definition) { + parent::__construct($entity_type, $entity_info, $storage, $module_handler); + $this->entityManager = $entity_manager; + $this->baseEntityType = $definition['base_entity_type']; + $this->baseEntityInfo = $this->entityManager->getDefinition($this->baseEntityType); + $this->baseEntityBundles = $this->entityManager->getBundleInfo($this->baseEntityType); + } + + /** + * {@inheritdoc} + */ + public function load() { + $entities = array(); + // It is not possible to use the standard load method, because this needs + // all field instance entities only for the given baseEntityType. + foreach (Field::fieldInfo()->getInstances($this->baseEntityType) as $fields) { + $entities = array_merge($entities, array_values($fields)); + } + return $entities; + } + + /** + * {@inheritdoc} + */ + public function getFilterLabels() { + $info = parent::getFilterLabels(); + $bundle = isset($this->baseEntityInfo['bundle_label']) ? $this->baseEntityInfo['bundle_label'] : t('Bundle'); + $bundle = Unicode::strtolower($bundle); + + $info['placeholder'] = $this->t('Enter field or @bundle', array('@bundle' => $bundle)); + $info['description'] = $this->t('Enter a part of the field or @bundle to filter by.', array('@bundle' => $bundle)); + + return $info; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = array( + 'data' => String::checkPlain($entity->label()), + 'class' => 'table-filter-text-source', + ); + + if ($this->displayBundle()) { + $bundle = $entity->bundle; + $row['bundle'] = array( + 'data' => String::checkPlain($this->baseEntityBundles[$bundle]['label']), + 'class' => 'table-filter-text-source', + ); + } + + $row['operations']['data'] = $this->buildOperations($entity); + + return $row; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = t('Field'); + if ($this->displayBundle()) { + $header['bundle'] = isset($this->baseEntityInfo['bundle_label']) ? $this->baseEntityInfo['bundle_label'] : t('Bundle'); + } + $header['operations'] = t('Operations'); + return $header; + } + + /** + * Controls the visibility of the bundle column on field instance list pages. + * + * @return bool + * Whenever the bundle is displayed or not. + */ + public function displayBundle() { + // The bundle key is explicitly defined in the entity definition. + if (isset($this->baseEntityInfo['bundle_keys']['bundle'])) { + return TRUE; + } + + // There is more than one bundle defined. + if (count($this->baseEntityBundles) > 1) { + return TRUE; + } + + // The defined bundle ones not match the entity type name. + if (!empty($this->baseEntityBundles) && !isset($this->baseEntityBundles[$this->baseEntityType])) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function sortRows($a, $b) { + return $this->sortRowsMultiple($a, $b, array('bundle', 'label')); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php new file mode 100644 index 0000000..1e47fda --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php @@ -0,0 +1,88 @@ +mapperManager = $mapper_manager; + $this->mapperDefinition = $this->mapperManager->getDefinition($config_translation_mapper); + $this->mapper = $this->mapperManager->createInstance($config_translation_mapper, $this->mapperDefinition); + } + + /** + * {inheritdoc} + */ + public static function create(ContainerInterface $container) { + $instance = new static( + $container->get('plugin.manager.config_translation'), + $container->get('request')->attributes->get('_raw_variables')->get('config_translation_mapper') + ); + return $instance; + } + + /** + * Provides the listing page for any entity type. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function listing() { + if (!$this->mapper) { + throw new NotFoundHttpException(); + } + $entity_type = $this->mapper->getType(); + // If the mapper, for example the mapper for field instances, has a custom + // list controller defined, use it. Other mappers, for examples the ones for + // node_type and block, fallback to the generic configuration translation + // list controller. + $class = isset($this->mapperDefinition['list_controller']) ? $this->mapperDefinition['list_controller'] : '\Drupal\config_translation\Controller\ConfigTranslationEntityListController'; + $controller = new $class($entity_type, $this->entityManager()->getDefinition($entity_type), $this->entityManager()->getStorageController($entity_type), $this->moduleHandler(), $this->entityManager(), $this->mapperDefinition); + $build = $controller->render(); + $build['#title'] = $this->mapper->getTypeLabel(); + return $build; + } +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php new file mode 100644 index 0000000..ef373ac --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php @@ -0,0 +1,148 @@ +mapperManager = $mapper_manager; + $this->mappers = $this->mapperManager->getMappers(); + } + + /** + * {inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.config_translation') + ); + } + + /** + * Retrieves the config translation mapper objects. + * + * @return \Drupal\config_translation\ConfigNamesMapper[] + * An array of configuration mappers. + */ + function getMappers() { + return $this->mappers; + } + + /** + * Builds the mappers as a renderable array for theme_table(). + * + * @return array + * Renderable array with config translation mappers. + */ + public function render() { + $build = array( + '#theme' => 'table', + '#header' => $this->buildHeader(), + '#rows' => array(), + ); + + $mappers = array(); + + foreach ($this->getMappers() as $mapper) { + if ($row = $this->buildRow($mapper)) { + $mappers[$mapper->getWeight()][] = $row; + } + } + + // Group by mapper weight and sort by label. + ksort($mappers); + foreach ($mappers as $weight => $mapper) { + usort($mapper, array($this, 'sortByLabel')); + $mappers[$weight] = $mapper; + } + + foreach ($mappers as $mapper) { + $build['#rows'] = array_merge($build['#rows'], $mapper); + } + + return $build; + } + + /** + * Builds a row for a mapper in the mapper listing. + * + * @param $mapper + * The mapper. + * @return array + * A render array structure of fields for this mapper. + */ + public function buildRow($mapper) { + $row['label'] = String::checkPlain($mapper->getTypeLabel()); + $row['operations']['data'] = $mapper->getOperations(); + return $row; + } + + /** + * Builds the header row for the mapper listing. + * + * @return array + * A render array structure of header strings. + */ + public function buildHeader() { + $row['Label'] = t('Label'); + $row['operations'] = t('Operations'); + return $row; + } + + /** + * Sorts a string array item by label key. + * + * @param array $a + * First item for comparison. + * @param array $b + * Second item for comparison. + * @param string $key + * The key to use in the comparison. + * + * @return int + * The comparison result for uasort(). + */ + public function sortByLabel($a, $b) { + $a_title = (is_array($a) && isset($a['label'])) ? $a['label'] : ''; + $b_title = (is_array($b) && isset($b['label'])) ? $b['label'] : ''; + + return strnatcasecmp($a_title, $b_title); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php new file mode 100644 index 0000000..e5f12a5 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php @@ -0,0 +1,38 @@ +t('Add @language translation for %label', array( + '%label' => $this->mapper->getTitle(), + '@language' => $this->language->name, + )); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + parent::submitForm($form, $form_state); + drupal_set_message($this->t('Successfully saved @language translation.', array('@language' => $this->language->name))); + } + +} 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..f8065e7 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php @@ -0,0 +1,134 @@ +configStorage = $config_storage; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.storage'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete the @language translation of %label?', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name)); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function getCancelRoute() { + return array( + 'route_name' => $this->mapper->getRouteName(), + 'route_parameters' => $this->mapper->getRouteParams(), + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'config_translation_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $mapper = NULL, Language $language = NULL) { + $this->mapper = $mapper; + $this->language = $language; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + foreach ($this->mapper->getNames() as $name) { + $this->configStorage->delete('locale.config.' . $this->language->id . '.' . $name); + } + + // Flush all persistent caches. + $this->moduleHandler->invokeAll('cache_flush'); + foreach (Cache::getBins() as $service_id => $cache_backend) { + if ($service_id != 'cache.menu') { + $cache_backend->deleteAll(); + } + } + + drupal_set_message($this->t('@language translation of %label was deleted', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name))); + $form_state['redirect'] = $this->mapper->getBasePath() . '/translate'; + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php new file mode 100644 index 0000000..ab688af --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php @@ -0,0 +1,37 @@ +t('Edit @language translation for %label', array( + '%label' => $this->mapper->getTitle(), + '@language' => $this->language->name, + )); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + parent::submitForm($form, $form_state); + drupal_set_message($this->t('Successfully updated @language translation.', array('@language' => $this->language->name))); + } +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php new file mode 100644 index 0000000..a00061b --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php @@ -0,0 +1,355 @@ +typedConfigManager = $typed_config_manager; + $this->localeStorage = $locale_storage; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.typed'), + $container->get('locale.storage'), + $container->get('module_handler') + ); + } + + /** + * {@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 $mapper + * The configuration mapper 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 $mapper = NULL, Language $language = NULL) { + $this->mapper = $mapper; + $this->language = $language; + $this->sourceLanguage = $this->mapper->getLanguageWithFallback(); + + // 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. + $this->baseConfigData = $this->mapper->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($this->language); + + $form['config_names'] = array( + '#type' => 'container', + '#tree' => TRUE, + ); + foreach ($this->mapper->getNames() as $name) { + $form['config_names'][$name] = array('#type' => 'container'); + $form['config_names'][$name] += $this->buildConfigForm($this->typedConfigManager->get($name), $this->config($name)->get(), $this->baseConfigData[$name]); + } + + $form['#attached']['library'][] = array('config_translation', 'drupal.config_translation.admin'); + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array('#type' => 'submit', '#value' => $this->t('Save translation')); + + // Leave the language context so that configuration accessed later in the + // request is displayed in the correct language. + config_context_leave(); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $form_values = $form_state['values']['config_names']; + + // For the form submission handling, use the override free context. + config_context_enter('config.context.free'); + + foreach ($this->mapper->getNames() as $name) { + // Set configuration values based on form submission and source values. + $base_config = $this->config($name); + $translation_config = $this->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[$name], !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(); + + $form_state['redirect'] = $this->mapper->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. + */ + 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' => $this->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) . ' ') : '') . $this->t($definition['label']), + '#collapsible' => TRUE, + '#collapsed' => $collapsed, + ) + $sub_build; + } + } + else { + $definition = $element->getDefinition(); + + // @todo Remove this once/if https://drupal.org/node/2100223 is in. + $definitions = array( + $definition['type'] => &$definition, + ); + $this->moduleHandler->alter('config_type_info', $definitions); + + // 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' => $this->t($definition['label']) . ' ('. $this->sourceLanguage->name . ')', + '#type' => 'item', + ); + + $definition += array('form_element_class' => '\Drupal\config_translation\FormElement\Textfield'); + + /** @var \Drupal\config_translation\FormElement\Element $form_element */ + $form_element = new $definition['form_element_class'](); + $build[$element_key]['translation'] = $form_element->getFormElement($definition, $this->language, $value); + } + } + 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); + } + } + } + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php new file mode 100644 index 0000000..c66f58c --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php @@ -0,0 +1,39 @@ +t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://userguide.icu-project.org/formatparse/datetime')); + } + else { + $description = $this->t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://php.net/manual/function.date.php')); + } + return array( + '#type' => 'textfield', + '#title' => $this->t($definition['label']) . ' (' . $language->name . ')', + '#description' => $description, + '#default_value' => $value, + '#attributes' => array('lang' => $language->id), + '#field_suffix' => '
 
', + '#ajax' => array( + 'callback' => 'config_translation_process_date_format_dateTimeLookup', + 'event' => 'keyup', + 'progress' => array('type' => 'throbber', 'message' => NULL), + ), + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php new file mode 100644 index 0000000..285d638 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php @@ -0,0 +1,56 @@ +translationManager()->translate($string, $args, $options); + } + + /** + * Returns the translation manager. + * + * @return \Drupal\Core\StringTranslation\TranslationInterface + * The translation manager. + */ + protected function translationManager() { + if (!$this->translationManager) { + $this->translationManager = $this->container()->get('string_translation'); + } + return $this->translationManager; + } + + /** + * Returns the service container. + * + * @return \Symfony\Component\DependencyInjection\ContainerInterface $container + * The service container. + */ + protected function container() { + return \Drupal::getContainer(); + } + + abstract public function getFormElement($definition, $language, $value); + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php new file mode 100644 index 0000000..9f4d67f --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php @@ -0,0 +1,32 @@ + 'textarea', + '#default_value' => $value, + '#title' => $this->t($definition['label']) . ' (' . $language->name . ')', + '#rows' => $rows, + '#attributes' => array('lang' => $language->id), + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php new file mode 100644 index 0000000..f5003d5 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php @@ -0,0 +1,26 @@ + 'textfield', + '#default_value' => $value, + '#title' => $this->t($definition['label']) . ' (' . $language->name . ')', + '#attributes' => array('lang' => $language->id), + ); + } + +} diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php new file mode 100644 index 0000000..95b387c --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php @@ -0,0 +1,64 @@ +mapperManager = $mapper_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('plugin.manager.config_translation') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions(array $base_plugin_definition) { + $mappers = $this->mapperManager->getMappers(); + foreach ($mappers as $mapper) { + foreach ($mapper->getRoutes() as $route_name => $route) { + $tab_name = $route_name . '_tab'; + $tab_root = str_replace('config_translation.item.', '', $tab_name); + $this->derivatives[$tab_name] = $base_plugin_definition; + $this->derivatives[$tab_name]['title'] = t('Translate @title', array('@title' => drupal_strtolower($mapper->getTypeName()))); + $this->derivatives[$tab_name]['route_name'] = $route_name; + $this->derivatives[$tab_name]['tab_root_id'] = $tab_root; + } + } + return $this->derivatives; + } + +} 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..fb8db49 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php @@ -0,0 +1,82 @@ +mapperManager = $mapper_manager; + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC] = 'routes'; + return $events; + } + + /** + * Generates dynamic routes for various configuration translations. + * + * 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. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route building event. + * + * @return \Symfony\Component\Routing\RouteCollection + * The route collection that contains the new dynamic route. + */ + public function routes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + + // Add configuration mappers. + $config_mappers = $this->mapperManager->getMappers(); + $this->generateRoutes($collection, $config_mappers); + } + + /** + * Generates routes for mappers. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection where the new dynamic routes are added. + * @param array $mappers + * An array of ConfigNamesMapper instances. + */ + protected function generateRoutes($collection, $mappers) { + foreach ($mappers as $mapper) { + foreach ($mapper->getRoutes() as $route_name => $route) { + $collection->add($route_name, $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..499d298 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUITest.php @@ -0,0 +1,492 @@ + 'Configuration Translation lists', + 'description' => 'Visit all lists.', + 'group' => 'Configuration Translation', + ); + } + + /** + * Admin user with all needed permissions. + * + * @var \Drupal\user\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 custom_block fields', + 'administer filters', + 'administer menu', + 'administer node fields', + 'administer permissions', + 'administer shortcuts', + 'administer site configuration', + 'administer taxonomy', + 'administer account settings', + '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->drupalPostForm('admin/structure/menu/add', $edit, t('Save')); + + // 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 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 type listing. + $this->drupalGet('admin/structure/block/custom-blocks/types'); + + $translate_link = 'admin/structure/block/custom-blocks/manage/' . $custom_block_type->id() . '/translate'; + // Test if the link to translate the custom block 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 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->drupalPostForm('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 field listing for the translate operation. + */ + function doFieldListTest() { + // Create a base content type. + $content_type = entity_create('node_type', array( + 'type' => Unicode::strtolower($this->randomName(16)), + 'name' => $this->randomName(), + )); + $content_type->save(); + + // Look at a few fields on a few entity types. + $pages = array( + array( + 'list' => 'admin/structure/types/manage/' . $content_type->id() . '/fields', + 'field' => 'node.' . $content_type->id() . '.body', + ), + array( + 'list' => 'admin/structure/block/custom-blocks/manage/basic/fields', + 'field' => 'custom_block.basic.body', + ), + ); + + foreach ($pages as $values) { + // Get fields listing. + $this->drupalGet($values['list']); + + $translate_link = $values['list'] . '/' . $values['field'] . '/translate'; + // Test if the link to translate the field 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 based on paths provided by the module. + + $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(); + $this->doFieldListTest(); + + // 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/ConfigTranslationOverviewTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php new file mode 100644 index 0000000..e5284e6 --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php @@ -0,0 +1,112 @@ + 'Configuration Translation Overview', + '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', + 'access contextual links', + 'administer views', + ); + // 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 config translation mapper page. + */ + function testMapperListPage() { + $this->drupalGet('admin/config/regional/config-translation'); + $this->assertLinkByHref('admin/config/regional/config-translation/config_test'); + $this->assertLinkByHref('admin/config/people/accounts/translate'); + + $labels = array( + '&$nxd~i0', + 'some "label" with quotes', + $this->randomString(), + ); + + foreach ($labels as $label) { + $test_entity = entity_create('config_test', array( + 'id' => 'config_test', + 'label' => $label, + )); + $test_entity->save(); + + $this->drupalGet('admin/config/regional/config-translation/config_test'); + $this->assertLinkByHref('admin/structure/config_test/manage/config_test/translate'); + $this->assertText(String::checkPlain($test_entity->label())); + + $entity_info = \Drupal::entityManager()->getDefinition($test_entity->entityType()); + $this->drupalGet('admin/structure/config_test/manage/config_test/translate'); + + $title = t('!label !entity_type', array('!label' => $test_entity->label(), '!entity_type' => Unicode::strtolower($entity_info['label']))); + $title = t('Translations for %label', array('%label' => $title)); + $this->assertRaw($title); + $this->assertRaw('' . t('Language') . ''); + + $this->drupalGet('admin/structure/config_test/manage/config_test'); + $this->assertLink(t('Translate @title', array('@title' => Unicode::strtolower($entity_info['label'])))); + } + } + +} 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..b032cfa --- /dev/null +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUITest.php @@ -0,0 +1,542 @@ + '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', + 'access contextual links', + 'administer views', + ); + // 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( + 'config_names[system.site][name][translation]' => $fr_site_name, + 'config_names[system.site][slogan][translation]' => $fr_site_slogan, + ); + + $this->drupalPostForm($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + $this->assertRaw(t('Successfully saved @language translation.', array('@language' => 'French'))); + + // 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('config_names[system.site][name][translation]', $fr_site_name); + $this->assertFieldByName('config_names[system.site][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( + 'config_names[system.site][name][translation]' => 'FR ' . $site_name, + 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan, + ); + // First time, no overrides, so just Add link. + $this->drupalPostForm($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. + $this->drupalGet($translation_base_url, $this->getUrlOptions('edit', 'fr')); + // Assert that the language configuration does not leak outside of the + // translation form into the actual site name and slogan. + $this->assertNoText('FR ' . $site_name); + $this->assertNoText('FR ' . $site_slogan); + $edit = array( + 'config_names[system.site][name][translation]' => $site_name, + 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan, + ); + $this->drupalPostForm(NULL, $edit, t('Save translation')); + $this->assertRaw(t('Successfully updated @language translation.', array('@language' => 'French'))); + $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. + $this->drupalGet($translation_base_url, $this->getUrlOptions('edit', 'fr')); + $this->assertNoText('FR ' . $site_slogan); + $edit = array( + 'config_names[system.site][name][translation]' => $site_name, + 'config_names[system.site][slogan][translation]' => $site_slogan, + ); + $this->drupalPostForm(NULL, $edit, t('Save translation')); + $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->drupalPostForm('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( + 'config_names[contact.category.feedback][label][translation]' => 'Website feedback - ' . $langcode, + 'config_names[contact.category.feedback][reply][translation]' => 'Thank you for your mail - ' . $langcode + ); + + // Save language specific version of form. + $this->drupalPostForm('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->drupalPostForm(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( + 'config_names[user.settings][anonymous][translation]' => 'Anonyme', + 'config_names[user.mail][status_blocked][status_blocked.subject][translation]' => 'Testing, your account is blocked.', + 'config_names[user.mail][status_blocked][status_blocked.body][translation]' => 'Testing account blocked body.' + ); + + $this->drupalPostForm('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() { + // Assert contextual link related to views. + $ids = array('views_ui:admin/structure/views/view:frontpage:location=page&name=frontpage&display_id=page_1'); + $response = $this->renderContextualLinks($ids, 'node'); + $this->assertResponse(200); + $json = drupal_json_decode($response); + $this->assertTrue(strpos($json[$ids[0]], t('Translate view')), 'Translate view contextual link added.'); + + $description = 'All content promoted to 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( + 'config_names[views.view.frontpage][description][translation]' => $description . " FR", + 'config_names[views.view.frontpage][label][translation]' => $human_readable_name . " FR", + 'config_names[views.view.frontpage][display][default][display.default.display_title][translation]' => $display_settings_master . " FR", + 'config_names[views.view.frontpage][display][default][display_options][display.default.display_options.title][translation]' => $display_options_master . " FR", + ); + $this->drupalPostForm($translation_base_url, $edit, t('Save translation'), $this->getUrlOptions('add', 'fr')); + $this->assertRaw(t('Successfully saved @language translation.', array('@language' => 'French'))); + + // 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('config_names[views.view.frontpage][description][translation]', $description . " FR"); + $this->assertFieldByName('config_names[views.view.frontpage][label][translation]', $human_readable_name . " FR"); + $this->assertFieldByName('config_names[views.view.frontpage][display][default][display.default.display_title][translation]', $display_settings_master . " FR"); + $this->assertFieldByName('config_names[views.view.frontpage][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->drupalPostForm('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( + 'config_names[user.settings][anonymous][translation]' => 'Anonyme', + ); + $this->drupalPostForm('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( + 'config_names[user.settings][anonymous][translation]' => 'Anonymous', + ); + $this->drupalPostForm('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->drupalPostForm('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->drupalPostForm('admin/config/regional/settings', $edit, t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.')); + + // Delete English language + $this->drupalPostForm('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->drupalPostForm('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 + ) + ); + } + + /** + * Get server-rendered contextual links for the given contextual link ids. + * + * @param array $ids + * An array of contextual link ids. + * @param string $current_path + * The Drupal path for the page for which the contextual links are rendered. + * + * @return string + * The response body. + */ + protected function renderContextualLinks($ids, $current_path) { + // Build POST values. + $post = array(); + for ($i = 0; $i < count($ids); $i++) { + $post['ids[' . $i . ']'] = $ids[$i]; + } + + // Serialize POST values. + foreach ($post as $key => $value) { + // Encode according to application/x-www-form-urlencoded + // Both names and values needs to be urlencoded, according to + // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1 + $post[$key] = urlencode($key) . '=' . urlencode($value); + } + $post = implode('&', $post); + + // Perform HTTP request. + return $this->curlExec(array( + CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => $current_path))), + CURLOPT_POST => TRUE, + CURLOPT_POSTFIELDS => $post, + CURLOPT_HTTPHEADER => array( + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded', + ), + )); + } + +} 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') . ''); + } + +} diff --git a/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig b/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig new file mode 100644 index 0000000..6bbee3d --- /dev/null +++ b/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig @@ -0,0 +1,24 @@ +{# +/** +* @file +* Default theme implementation for a form element in config_translation. +* +* Available variables: +* - element: Array that represents the element shown in the form. +* - source: The source of the translation. +* - translation: The translation for the target language. +* +* @see template_preprocess() +* @see template_preprocess_config_translation_manage_form_element() +* +* @ingroup themeable +*/ +#} +
+
+ {{ element.source }} +
+
+ {{ element.translation }} +
+
diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php new file mode 100644 index 0000000..205ac0e --- /dev/null +++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php @@ -0,0 +1,185 @@ + 'Configuration translation mapper manager', + 'description' => 'Tests the functionality provided by configuration translation mapper manager.', + 'group' => 'Configuration Translation', + ); + } + + public function setUp() { + $language = new Language(array('id' => 'en')); + $language_manager = $this->getMock('Drupal\Core\Language\LanguageManager'); + $language_manager->expects($this->once()) + ->method('getLanguage') + ->with(Language::TYPE_INTERFACE) + ->will($this->returnValue($language)); + $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $this->typedConfigManager = $this->getMockBuilder('Drupal\Core\Config\TypedConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $this->configMapperManager = new ConfigMapperManager( + $this->getMock('Drupal\Core\Cache\CacheBackendInterface'), + $language_manager, + $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'), + $entity_manager, + $this->typedConfigManager, + $this->getMock('Drupal\Core\StringTranslation\TranslationInterface') + ); + } + + /** + * Tests ConfigMapperManager::hasTranslatable(). + * + * @param \Drupal\Core\TypedData\TypedDataInterface $element + * The schema element to test. + * @param bool $expected + * The expected return value of ConfigMapperManager::hasTranslatable(). + * + * @dataProvider providerTestHasTranslatable + */ + public function testHasTranslatable(TypedDataInterface $element, $expected) { + $this->typedConfigManager + ->expects($this->once()) + ->method('get') + ->with('test') + ->will($this->returnValue($element)); + + $result = $this->configMapperManager->hasTranslatable('test'); + $this->assertSame($expected, $result); + } + + /** + * Provides data for ConfigMapperManager::testHasTranslatable() + * + * @return array + * An array of arrays, where each inner array contains the schema element + * to test as the first key and the expected result of + * ConfigMapperManager::hasTranslatable() as the second key. + */ + public function providerTestHasTranslatable() { + return array( + array($this->getElement(array()), FALSE), + array($this->getElement(array('aaa' => 'bbb')), FALSE), + array($this->getElement(array('translatable' => FALSE)), FALSE), + array($this->getElement(array('translatable' => TRUE)), TRUE), + array($this->getNestedElement(array( + $this->getElement(array()), + )), FALSE), + array($this->getNestedElement(array( + $this->getElement(array('translatable' => TRUE)), + )), TRUE), + array($this->getNestedElement(array( + $this->getElement(array('aaa' => 'bbb')), + $this->getElement(array('ccc' => 'ddd')), + $this->getElement(array('eee' => 'fff')), + )), FALSE), + array($this->getNestedElement(array( + $this->getElement(array('aaa' => 'bbb')), + $this->getElement(array('ccc' => 'ddd')), + $this->getElement(array('translatable' => TRUE)), + )), TRUE), + array($this->getNestedElement(array( + $this->getElement(array('aaa' => 'bbb')), + $this->getNestedElement(array( + $this->getElement(array('ccc' => 'ddd')), + $this->getElement(array('eee' => 'fff')), + )), + $this->getNestedElement(array( + $this->getElement(array('ggg' => 'hhh')), + $this->getElement(array('iii' => 'jjj')), + )), + )), FALSE), + array($this->getNestedElement(array( + $this->getElement(array('aaa' => 'bbb')), + $this->getNestedElement(array( + $this->getElement(array('ccc' => 'ddd')), + $this->getElement(array('eee' => 'fff')), + )), + $this->getNestedElement(array( + $this->getElement(array('ggg' => 'hhh')), + $this->getElement(array('translatable' => TRUE)), + )), + )), TRUE), + ); + } + + /** + * Returns a mocked schema element. + * + * @param array $definition + * The definition of the schema element. + * + * @return \Drupal\Core\Config\Schema\Element + * The mocked schema element. + */ + protected function getElement(array $definition) { + $element = $this->getMock('Drupal\Core\TypedData\TypedDataInterface'); + $element->expects($this->any()) + ->method('getDefinition') + ->will($this->returnValue($definition)); + return $element; + } + + /** + * Returns a mocked nested schema element. + * + * @param array $elements + * An array of simple schema elements. + * + * @return \Drupal\Core\Config\Schema\Mapping + * A nested schema element, containing the passed-in elements. + */ + protected function getNestedElement(array $elements) { + // ConfigMapperManager::findTranslatable() checks for the abstract class + // \Drupal\Core\Config\Schema\ArrayElement, but mocking that directly does + // not work. + $nested_element = $this->getMockBuilder('Drupal\Core\Config\Schema\Mapping') + ->disableOriginalConstructor() + ->getMock(); + $nested_element->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($elements))); + return $nested_element; + } + +} diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml new file mode 100644 index 0000000..0e772ba --- /dev/null +++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml @@ -0,0 +1,8 @@ +name: 'Configuration Translation Test' +type: module +package: Testing +version: VERSION +core: 8.x +hidden: true +dependencies: + - config_test diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module new file mode 100644 index 0000000..171f01d --- /dev/null +++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module @@ -0,0 +1,17 @@ +