diff --git a/config_translation.admin.inc b/config_translation.admin.inc deleted file mode 100644 index 34842e8..0000000 --- a/config_translation.admin.inc +++ /dev/null @@ -1,439 +0,0 @@ -getConfigGroup($path_arg); - - drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH); - - // It is possible the original language this configuration was saved with is - // not on the system. For example, the configuration shipped in English but - // the site has no English configured. Represent the original language in the - // table even if it is not currently configured. - $languages = language_list(); - $original_langcode = $group->getLangcode(); - if (!isset($languages[$original_langcode])) { - $language_name = language_name($original_langcode); - if ($original_langcode == 'en') { - $language_name = t('Built-in English'); - } - // Create a dummy language object for this listing only. - $languages[$original_langcode] = new Language(array('langcode' => $original_langcode, 'name' => $language_name)); - } - - $path = $group->getBasePath(); - $header = array(t('Language'), t('Operations')); - $page = array(); - $page['languages'] = array( - '#type' => 'table', - '#header' => $header, - ); - foreach ($languages as $language) { - if ($language->langcode == $original_langcode) { - $page['languages'][$language->langcode]['language'] = array( - '#markup' => '' . t('@language (original)', array('@language' => $language->name)) . '' - ); - - // @todo: the user translating might as well not have access to - // edit the original configuration. They will get a 403 for this - // link when clicked. Do we know better? - $operations = array(); - $operations['edit'] = array( - 'title' => t('Edit'), - 'href' => $path, - ); - $page['languages'][$language->langcode]['operations'] = array( - '#type' => 'operations', - '#links' => $operations, - ); - } - else { - $page['languages'][$language->langcode]['language'] = array( - '#markup' => $language->name, - ); - $operations = array(); - - // Check if translation exist for this language. - if (!$group->hasTranslation($language)) { - $operations['add'] = array( - 'title' => t('Add'), - 'href' => $path . '/translate/add/' . $language->langcode, - ); - } - else { - $operations['edit'] = array( - 'title' => t('Edit'), - 'href' => $path . '/translate/edit/' . $language->langcode, - ); - $operations['delete'] = array( - 'title' => t('Delete'), - 'href' => $path . '/translate/delete/' . $language->langcode, - ); - } - $page['languages'][$language->langcode]['operations'] = array( - '#type' => 'operations', - '#links' => $operations, - ); - } - } - return $page; -} - -/** - * Configuration page wrapper for translation editing form. - * - * @param string $action - * Action identifier, either 'add' or 'edit'. Used to provide proper - * labeling on the screen. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper - * Configuration mapper. - * @param mixed $path_arg - * Path argument from the menu system (entity id or loaded entity for - * configuration entities, NULL otherwise). - * @param \Drupal\Core\Language\Language $language - * A language object. - */ -function config_translation_item_translate_page($action, ConfigMapperInterface $mapper, $path_arg, $language) { - // Get configuration group for this mapper. - $group = $mapper->getConfigGroup($path_arg); - - $replacement = array( - '%label' => $group->getTitle(), - '@language' => strtolower($language->name), - ); - switch ($action) { - case 'add': - drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH); - break; - case 'edit': - drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH); - break; - } - - // Make sure we are in the override free config context. For example, - // visiting the configuration page in another language would make those - // language overrides active by default. But we need the original values. - config_context_enter('config.context.free'); - - // Get base language configuration to display in the form before entering - // into the language context for the form. This avoids repetitively going - // in and out of the language context to get original values later. - $base_config = $group->getConfigData(); - - // Leave override free context. - config_context_leave(); - - // Enter context for the translation target language requested and generate - // form with translation data in that language. - config_translation_enter_context($language); - return drupal_get_form('config_translation_form', $group, $language, $base_config); -} - -/** - * Build configuration form with metadata and values. - * - * @param array $form - * An associative array containing the structure of the form. - * @param array $form_state - * An associative array containing the current state of the form. - * @param \Drupal\config_translation\ConfigGroupMapper $group - * Configuration name group. - * @param \Drupal\Core\Language\Language $language - * A language object. - * @param array $base_config - * An array of base language configuration data keyed by configuration names. - * - * @return array - * An associative array containing the structure of the form. - */ -function config_translation_form($form, &$form_state, ConfigGroupMapper $group, $language, $base_config = array()) { - $form['group'] = array( - '#type' => 'value', - '#value' => $group, - ); - $form['language'] = array( - '#type' => 'value', - '#value' => $language, - ); - foreach ($group->getNames() as $id => $name) { - $form[$id] = array( - '#type' => 'container', - '#tree' => TRUE, - ); - $form[$id] += config_translation_build_form(config_typed()->get($name), config($name)->get(), $base_config[$name]); - } - - $form['actions']['#type'] = 'actions'; - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translation')); - return $form; -} - -/** - * Submit handler to save config translation. - */ -function config_translation_form_submit(&$form, &$form_state) { - $form_values = $form_state['values']; - $language = $form_values['language']; - $group = $form_values['group']; - - // For the form submission handling, use the override free context. - config_context_enter('config.context.free'); - - foreach ($group->getNames() as $id => $name) { - // Set config values based on form submission and original values. - $base_config = config($name); - $translation_config = config('locale.config.' . $language->langcode . '.' . $name); - $locations = locale_storage()->getLocations(array('type' => 'configuration', 'name' => $name)); - - config_translation_set_config($language, $base_config, $translation_config, $form_values[$id], !empty($locations)); - - // If no overrides, delete language specific config file. - $saved_config = $translation_config->get(); - if (empty($saved_config)) { - $translation_config->delete(); - } - else { - $translation_config->save(); - } - } - - config_context_leave(); - - drupal_set_message(t('Updated @language configuration translations successfully.', array('@language' => $language->name))); - $form_state['redirect'] = $group->getBasePath() . '/translate'; -} - -/** - * Sets configuration based on a nested form value array. - * - * @param Language $language - * Language object. - * @param \Drupal\Core\Config\Config $base_config - * Base language configuration values instance. - * @param \Drupal\Core\Config\Config $translation_config - * Translation configuration instance. Values from $config_values will be set - * in this instance. - * @param array $config_values - * A simple one dimensional or recursive array: - * - simple: - * array(name => 'French site name') - * - recursive: - * cancel_confirm => array( - * cancel_confirm.subject => 'Subject' - * cancel_confirm.body => '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. - */ -function config_translation_set_config(Language $language, Config $base_config, Config $translation_config, array $config_values, $shipped_config = FALSE) { - foreach ($config_values as $key => $value) { - if (is_array($value)) { - // Traverse into this level in the configuration. - config_translation_set_config($language, $base_config, $translation_config, $value, $shipped_config); - } - else { - - // If the config file being translated was originally shipped, we should - // update the locale translation storage. The string should already be - // there, but we make sure to check. - if ($shipped_config && $source_string = locale_storage()->findString(array('source' => $base_config->get($key)))) { - - // Get the translation for this original source string from locale. - $conditions = array( - 'lid' => $source_string->lid, - 'language' => $language->langcode, - ); - $translations = locale_storage()->getTranslations($conditions + array('translated' => TRUE)); - // If we got a translation, take that, otherwise create a new one. - $translation = reset($translations) ?: locale_storage()->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->setString($value) - ->setCustomized() - ->save(); - } - } - - // Save value, if different from original configuration. If same as - // original configuration, remove override. - if ($base_config->get($key) !== $value) { - $translation_config->set($key, $value); - } - else { - $translation_config->clear($key); - } - } - } -} - -/** - * Formats configuration schema as a form tree. - * - * @param $schema - * Schema definition of configuration. - * @param $config - * Configuration object of requested language. - * @param $base_config - * Configuration object of base language. - * @param bool $collapsed - * Flag to set collapsed state. - * @param string|NULL $base_key - * Base configuration key. - * - * @return array - * An associative array containing the structure of the form. - * - * @todo - * Make this conditional on the translatable schema property from - * http://drupal.org/node/1905152 - currently hardcodes label and text. - */ -function config_translation_build_form($schema, $config, $base_config, $collapsed = FALSE, $base_key = '') { - $build = array(); - foreach ($schema as $key => $element) { - $element_key = implode('.', array_filter(array($base_key, $key))); - $definition = $element->getDefinition() + array('label' => t('N/A')); - if ($element instanceof Element) { - // Build sub-structure and include it with a wrapper in the form - // if there are any translatable elements there. - $sub_build = config_translation_build_form($element, $config[$key], $base_config[$key], TRUE, $key); - if (!empty($sub_build)) { - $build[$key] = array( - '#type' => 'details', - '#title' => t($definition['label']), - '#collapsible' => TRUE, - '#collapsed' => $collapsed, - ) + $sub_build; - } - } - else { - $type = $element->getType(); - switch ($type) { - case 'label': - $type = 'textfield'; - break; - case 'text': - $type = 'textarea'; - break; - default: - continue(2); - break; - } - $value = $config[$key]; - $description = format_string('Source string: !source_string', array('!source_string' => nl2br($base_config[$key]) ?: t('(Empty)'))); - $build[$element_key] = array( - '#type' => $type, - '#title' => t($definition['label']), - '#default_value' => $value, - '#description' => $description, - ); - } - } - return $build; -} - -/** - * Creates the form for confirmation of deleting a translation. - * - * @param array $form - * An associative array containing the structure of the form. - * @param array $form_state - * An associative array containing the current state of the form. - * @param \Drupal\config_translation\ConfigMapperInterface $mapper - * Configuration mapper. - * @param mixed $path_arg - * Path argument from the menu system (entity id or loaded entity for - * configuration entities, NULL otherwise). - * @param \Drupal\Core\Language\Language $language - * A language object. - * - * @return array - * An associative array containing the structure of the form. - * - * @see config_translation_item_delete_form_submit(). - * @ingroup forms - */ -function config_translation_item_delete_form($form, &$form_state, ConfigMapperInterface $mapper, $path_arg, $language) { - // Get configuration group for this mapper. - $group = $mapper->getConfigGroup($path_arg); - - $form['group'] = array( - '#type' => 'value', - '#value' => $group, - ); - $form['language'] = array( - '#type' => 'value', - '#value' => $language, - ); - return confirm_form($form, - t('Are you sure you want to delete the @language translation of %label?', array('%label' => $group->getTitle(), '@language' => $language->name)), - $group->getBasePath() . '/translate', - t('This cannot be undone.'), - t('Delete'), - t('Cancel') - ); -} - -/** - * Form submission handler for config_translation_item_delete_form(). - * - * @see config_translation_item_delete_form(). - */ -function config_translation_item_delete_form_submit($form, &$form_state) { - $form_values = $form_state['values']; - $group = $form_values['group']; - $language = $form_values['language']; - - $storage = drupal_container()->get('config.storage'); - foreach ($group->getNames() as $name) { - $storage->delete('locale.config.' . $language->langcode . '.' . $name); - } - // @todo: do we need to flush caches? The config change may affect page display. - // drupal_flush_all_caches(); - - drupal_set_message(t('@language translation of %label was deleted', array('%label' => $group->getTitle(), '@language' => $language->name))); - $form_state['redirect'] = $group->getBasePath() . '/translate'; -} - -/** - * Use a dummy user to get overrides for a specific language. - * - * See http://drupal.org/node/1905152 for an upcoming API to access - * translations easier through typed data. - * - * @param \Drupal\Core\Language\Language $language - * A language object. - */ -function config_translation_enter_context($language) { - $user_config_context = config_context_enter('Drupal\user\UserConfigContext'); - $account = new User(array('preferred_langcode' => $language->langcode), 'user'); - $user_config_context->setAccount($account); -} diff --git a/config_translation.module b/config_translation.module index 5c13e9b..58d1e1c 100644 --- a/config_translation.module +++ b/config_translation.module @@ -7,6 +7,7 @@ use Drupal\Core\Config\Schema\Element; use Drupal\Core\Language\Language; +use Drupal\user\Plugin\Core\Entity\User; use Drupal\config_translation\ConfigGroupMapper; use Drupal\config_translation\ConfigEntityMapper; use Drupal\config_translation\ConfigMapperInterface; @@ -59,41 +60,25 @@ function config_translation_menu() { } $items[$path . '/translate'] = array( 'title' => 'Translate', - 'page callback' => 'config_translation_item_overview_page', - 'page arguments' => array($group, $group->getPathIdIndex()), - 'access callback' => 'config_translation_config_name_access', - 'access arguments' => array($group, $group->getPathIdIndex()), + 'route_name' => $group->getRouterName(), 'type' => $group->getMenuType(), 'weight' => 100, - 'file' => 'config_translation.admin.inc', ); $depth = substr_count($path, '/'); $items[$path . '/translate/add/%language'] = array( 'title' => 'Translate', - 'page callback' => 'config_translation_item_translate_page', - 'page arguments' => array('add', $group, $group->getPathIdIndex(), $depth + 3), - 'access callback' => 'config_translation_config_name_access', - 'access arguments' => array($group, $group->getPathIdIndex(), $depth + 3), + 'route_name' => $group->getRouterName() . '_add', 'type' => MENU_CALLBACK, - 'file' => 'config_translation.admin.inc', ); $items[$path . '/translate/edit/%language'] = array( 'title' => 'Translate', - 'page callback' => 'config_translation_item_translate_page', - 'page arguments' => array('edit', $group, $group->getPathIdIndex(), $depth + 3), - 'access callback' => 'config_translation_config_name_access', - 'access arguments' => array($group, $group->getPathIdIndex(), $depth + 3), + 'route_name' => $group->getRouterName() . '_edit', 'type' => MENU_CALLBACK, - 'file' => 'config_translation.admin.inc', ); $items[$path . '/translate/delete/%language'] = array( 'title' => 'Delete', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('config_translation_item_delete_form', $group, $group->getPathIdIndex(), $depth + 3), - 'access callback' => 'config_translation_config_name_access', - 'access arguments' => array($group, $group->getPathIdIndex(), $depth + 3), + 'route_name' => $group->getRouterName() . '_delete', 'type' => MENU_CALLBACK, - 'file' => 'config_translation.admin.inc', ); } @@ -254,36 +239,36 @@ function config_translation_config_translation_group_info() { $items = array(); // Block. - $items[] = new ConfigEntityMapper('admin/structure/block/manage/%block', 4, 'block', t('@label block'), 'block.block'); + $items[] = new ConfigEntityMapper('admin/structure/block/manage/{block}', 4, 'block', t('@label block'), 'block.block'); // Custom block. - $items[] = new ConfigEntityMapper('admin/structure/custom-blocks/manage/%custom_block_type', 4, 'custom_block_type', t('@label custom block type'), 'custom_block.type'); + $items[] = new ConfigEntityMapper('admin/structure/custom-blocks/manage/{custom_block_type}', 4, 'custom_block_type', t('@label custom block type'), 'custom_block.type'); // Contact. - $items[] = new ConfigEntityMapper('admin/structure/contact/manage/%contact_category', 4, 'contact_category', t('@label contact category'), 'contact.category'); + $items[] = new ConfigEntityMapper('admin/structure/contact/manage/{contact_category}', 4, 'contact_category', t('@label contact category'), 'contact.category'); // Filter. - $items[] = new ConfigEntityMapper('admin/config/content/formats/%filter_format', 4, 'filter_format', t('@label text format'), 'filter.format', MENU_LOCAL_TASK, TRUE); + $items[] = new ConfigEntityMapper('admin/config/content/formats/{filter_format}', 4, 'filter_format', t('@label text format'), 'filter.format', MENU_LOCAL_TASK, TRUE); // Menu. - $items[] = new ConfigEntityMapper('admin/structure/menu/manage/%menu', 4, 'menu', t('@label menu'), 'menu.menu'); + $items[] = new ConfigEntityMapper('admin/structure/menu/manage/{menu}', 4, 'menu', t('@label menu'), 'menu.menu'); // Shortcut. - $items[] = new ConfigEntityMapper('admin/config/user-interface/shortcut/manage/%shortcut_set', 5, 'shortcut', t('@label shortcut set'), 'shortcut.set'); + $items[] = new ConfigEntityMapper('admin/config/user-interface/shortcut/manage/{shortcut_set}', 5, 'shortcut', t('@label shortcut set'), 'shortcut.set'); // System. $items[] = new ConfigGroupMapper('admin/config/development/maintenance', t('System maintenance'), array('system.maintenance'), TRUE); $items[] = new ConfigGroupMapper('admin/config/system/site-information', t('Site information'), array('system.site'), MENU_LOCAL_TASK, TRUE); // Taxonomy. - $items[] = new ConfigEntityMapper('admin/structure/taxonomy/%taxonomy_vocabulary', 3, 'taxonomy_vocabulary', t('@label vocabulary'), 'taxonomy.vocabulary'); + $items[] = new ConfigEntityMapper('admin/structure/taxonomy/{taxonomy_vocabulary}', 3, 'taxonomy_vocabulary', t('@label vocabulary'), 'taxonomy.vocabulary'); // User. $items[] = new ConfigGroupMapper('admin/config/people/accounts', t('Account settings'), array('user.settings', 'user.mail')); - $items[] = new ConfigEntityMapper('admin/people/roles/edit/%user_role', 4, 'user_role', t('@label user role'), 'user.role', MENU_LOCAL_TASK, TRUE); + $items[] = new ConfigEntityMapper('admin/people/roles/edit/{user_role}', 4, 'user_role', t('@label user role'), 'user.role', MENU_LOCAL_TASK, TRUE); // Views. - $items[] = new ConfigEntityMapper('admin/structure/views/view/%', 4, 'view', t('@label view'), 'views.view', MENU_CALLBACK); + $items[] = new ConfigEntityMapper('admin/structure/views/view/{view}', 4, 'view', t('@label view'), 'views.view', MENU_CALLBACK); return $items; } @@ -437,3 +422,18 @@ function config_translation_page_alter(&$page) { } } } + +/** + * Use a dummy user to get overrides for a specific language. + * + * See http://drupal.org/node/1905152 for an upcoming API to access + * translations easier through typed data. + * + * @param \Drupal\Core\Language\Language $language + * A language object. + */ +function config_translation_enter_context($language) { + $user_config_context = config_context_enter('Drupal\user\UserConfigContext'); + $account = new User(array('preferred_langcode' => $language->langcode), 'user'); + $user_config_context->setAccount($account); +} diff --git a/lib/Drupal/config_translation/Access/ConfigNameCheck.php b/lib/Drupal/config_translation/Access/ConfigNameCheck.php new file mode 100644 index 0000000..b0bda1a --- /dev/null +++ b/lib/Drupal/config_translation/Access/ConfigNameCheck.php @@ -0,0 +1,51 @@ +getRequirements()); + } + + /** + * Implements \Drupal\Core\Access\AccessCheckInterface::access(). + */ + public function access(Route $route, Request $request) { + $mapper = $route->getDefault('mapper'); + $entity = $request->attributes->get($mapper->getType()); + + // Get configuration group for this mapper. + $group = $mapper->getConfigGroup($entity); + $group_language = language_load($group->getLangcode()); + + // Only allow access to translate configuration, if proper permissions are + // granted, the configuration has translatable pieces, the source language + // and target language are not locked, and the target language is not the + // original submission language. Although technically config can be overlayed + // with translations in the same language, that is logically not a good idea. + return ( + user_access('translate configuration') && + $group->hasSchema() && + $group->hasTranslatable() && + !$group_language->locked && + (empty($language) || (!$language->locked && $language->langcode != $group_language->langcode)) + ); + } + +} diff --git a/lib/Drupal/config_translation/ConfigEntityMapper.php b/lib/Drupal/config_translation/ConfigEntityMapper.php index 75222ee..1f5d431 100644 --- a/lib/Drupal/config_translation/ConfigEntityMapper.php +++ b/lib/Drupal/config_translation/ConfigEntityMapper.php @@ -64,6 +64,14 @@ class ConfigEntityMapper implements ConfigMapperInterface { */ private $add_edit_tab = FALSE; + + /** + * Router name of the new routing system. + * + * @var bool + */ + private $router_name; + /** * Constructor for configuration group. * @@ -93,6 +101,7 @@ class ConfigEntityMapper implements ConfigMapperInterface { $this->config_prefix = $config_prefix; $this->menu_type = $menu_type; $this->add_edit_tab = $add_edit_tab; + $this->setRouteName(); } /** @@ -154,4 +163,24 @@ class ConfigEntityMapper implements ConfigMapperInterface { return new ConfigGroupMapper($base_path, $title, $names, $this->menu_type, $this->add_edit_tab); } + /** + * {@inheritdoc} + */ + public function setRouteName() { + $this->router_name = 'config_translation.item.' . str_replace(array('/', '-'), array('.', '_'), $this->base_path); + } + + /** + * {@inheritdoc} + */ + public function getRouterName() { + return $this->router_name; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->entity_type; + } } diff --git a/lib/Drupal/config_translation/ConfigGroupMapper.php b/lib/Drupal/config_translation/ConfigGroupMapper.php index 3367d31..4c305c0 100644 --- a/lib/Drupal/config_translation/ConfigGroupMapper.php +++ b/lib/Drupal/config_translation/ConfigGroupMapper.php @@ -50,6 +50,13 @@ class ConfigGroupMapper implements ConfigMapperInterface { private $add_edit_tab = FALSE; /** + * Router name of the new routing system. + * + * @var bool + */ + private $router_name; + + /** * Constructor for configuration group. * * @param string $base_path @@ -71,6 +78,7 @@ class ConfigGroupMapper implements ConfigMapperInterface { $this->names = $names; $this->menu_type = $menu_type; $this->add_edit_tab = $add_edit_tab; + $this->setRouteName(); } /** @@ -213,4 +221,24 @@ class ConfigGroupMapper implements ConfigMapperInterface { $this->names[] = $name; } + /** + * {@inheritdoc} + */ + public function setRouteName() { + $this->router_name = 'config_translation.item.' . str_replace(array('/', '-'), array('.', '_'), $this->base_path); + } + + /** + * {@inheritdoc} + */ + public function getRouterName() { + return $this->router_name; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return NULL; + } } diff --git a/lib/Drupal/config_translation/ConfigMapperInterface.php b/lib/Drupal/config_translation/ConfigMapperInterface.php index dba6e62..1ba60b3 100644 --- a/lib/Drupal/config_translation/ConfigMapperInterface.php +++ b/lib/Drupal/config_translation/ConfigMapperInterface.php @@ -52,4 +52,25 @@ interface ConfigMapperInterface { */ public function getConfigGroup($arg = NULL); + /** + * Helper to provide type of the group. + * + * @return mixed + */ + public function getType(); + + /** + * Creates unique route name for the group. + * + * @return mixed + */ + public function setRouteName(); + + /** + * Returns group name. + * + * @return mixed + */ + public function getRouterName(); + } diff --git a/lib/Drupal/config_translation/ConfigTranslationBundle.php b/lib/Drupal/config_translation/ConfigTranslationBundle.php new file mode 100644 index 0000000..0dac459 --- /dev/null +++ b/lib/Drupal/config_translation/ConfigTranslationBundle.php @@ -0,0 +1,28 @@ +register('config_translation.subscriber', 'Drupal\config_translation\Routing\RouteSubscriber') + ->addTag('event_subscriber'); + + $container->register('config_translation.access_check', 'Drupal\config_translation\Access\ConfigNameCheck') + ->addTag('access_check'); + } +} diff --git a/lib/Drupal/config_translation/Controller/ConfigTranslationController.php b/lib/Drupal/config_translation/Controller/ConfigTranslationController.php index 75f2a97..e7e1b69 100644 --- a/lib/Drupal/config_translation/Controller/ConfigTranslationController.php +++ b/lib/Drupal/config_translation/Controller/ConfigTranslationController.php @@ -7,10 +7,13 @@ namespace Drupal\config_translation\Controller; -use Drupal\Core\ControllerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\ControllerInterface; use Drupal\config_translation\ConfigGroupMapper; +use Drupal\config_translation\ConfigMapperInterface; use Drupal\Core\Config\Config; +use Drupal\config_translation\Form\ConfigTranslationManageForm; /** * Controller providing page callbacks for the config translation interface. @@ -18,7 +21,7 @@ use Drupal\Core\Config\Config; class ConfigTranslationController implements ControllerInterface { /** - * @inheritdoc + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static($container->get('database')); @@ -38,7 +41,7 @@ class ConfigTranslationController implements ControllerInterface { '#empty' => t('No translatable configuration found.'), ); - $config_groups = config_translation_get_groups(); + $config_groups = self::getTranslationGroups(); foreach ($config_groups as $group) { if ($group instanceof ConfigGroupMapper && $group->hasSchema() && $group->hasTranslatable()) { // Figure out language code and name for this configuration. Shipped @@ -71,4 +74,171 @@ class ConfigTranslationController implements ControllerInterface { } return $page; } + + /** + * Get group definitions from hooks and make it possible to alter groups. + * + * Configuration groups are used to get multiple configuration names used for + * one specific configuration form together. If contributed modules alter a + * form adding in additional settings stored elsewhere, the list of names + * can be expanded. + */ + public static function getTranslationGroups() { + $config_groups = \Drupal::ModuleHandler()->invokeAll('config_translation_group_info'); + + // Create an array of path indexed groups for easier altering. + $path_indexed_groups = array(); + foreach ($config_groups as $group) { + $path_indexed_groups[$group->getBasePath()] = $group; + } + \Drupal::ModuleHandler()->alter('config_translation_group_info', $path_indexed_groups); + return $path_indexed_groups; + } + + /** + * Language translations overview page for a configuration name. + * + * @param ConfigMapperInterface $mapper + * Configuration mapper. + * @param mixed $path_arg + * Path argument from the menu system (entity id or loaded entity for + * configuration entities, NULL otherwise). + * + * @return array + * Page render array. + */ + public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper) { + // Get configuration group for this mapper. + $entity = $request->attributes->get($mapper->getType()); + $group = $mapper->getConfigGroup($entity); + drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH); + + // It is possible the original language this configuration was saved with is + // not on the system. For example, the configuration shipped in English but + // the site has no English configured. Represent the original language in the + // table even if it is not currently configured. + $languages = language_list(); + $original_langcode = $group->getLangcode(); + if (!isset($languages[$original_langcode])) { + $language_name = language_name($original_langcode); + if ($original_langcode == 'en') { + $language_name = t('Built-in English'); + } + // Create a dummy language object for this listing only. + $languages[$original_langcode] = new Language(array('langcode' => $original_langcode, 'name' => $language_name)); + } + + $path = $group->getBasePath(); + $header = array(t('Language'), t('Operations')); + $page = array(); + $page['languages'] = array( + '#type' => 'table', + '#header' => $header, + ); + foreach ($languages as $language) { + if ($language->langcode == $original_langcode) { + $page['languages'][$language->langcode]['language'] = array( + '#markup' => '' . t('@language (original)', array('@language' => $language->name)) . '' + ); + + // @todo: the user translating might as well not have access to + // edit the original configuration. They will get a 403 for this + // link when clicked. Do we know better? + $operations = array(); + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path, + ); + $page['languages'][$language->langcode]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + else { + $page['languages'][$language->langcode]['language'] = array( + '#markup' => $language->name, + ); + $operations = array(); + + // Check if translation exist for this language. + if (!$group->hasTranslation($language)) { + $operations['add'] = array( + 'title' => t('Add'), + 'href' => $path . '/translate/add/' . $language->langcode, + ); + } + else { + $operations['edit'] = array( + 'title' => t('Edit'), + 'href' => $path . '/translate/edit/' . $language->langcode, + ); + $operations['delete'] = array( + 'title' => t('Delete'), + 'href' => $path . '/translate/delete/' . $language->langcode, + ); + } + $page['languages'][$language->langcode]['operations'] = array( + '#type' => 'operations', + '#links' => $operations, + ); + } + } + return $page; + } + + /** + * @param Request $request + * @param string $action + * Action identifier, either 'add' or 'edit'. Used to provide proper + * labeling on the screen. + * @param ConfigMapperInterface $mapper + * Configuration mapper. + * @param mixed $path_arg + * Path argument from the menu system (entity id or loaded entity for + * configuration entities, NULL otherwise). + * @param Language $language + * A language object. + * + * @return array|mixed + */ + function itemTranslatePage(Request $request, $action, ConfigMapperInterface $mapper) { + // Get configuration group for this mapper. + $entity = $request->attributes->get($mapper->getType()); + $group = $mapper->getConfigGroup($entity); + $langcode = $request->attributes->get('langcode'); + if ($langcode) { + $language = language_load($langcode); + } + $replacement = array( + '%label' => $group->getTitle(), + '@language' => strtolower($language->name), + ); + switch ($action) { + case 'add': + drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH); + break; + case 'edit': + drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH); + break; + } + + // Make sure we are in the override free config context. For example, + // visiting the configuration page in another language would make those + // language overrides active by default. But we need the original values. + config_context_enter('config.context.free'); + + // Get base language configuration to display in the form before entering + // into the language context for the form. This avoids repetitively going + // in and out of the language context to get original values later. + $base_config = $group->getConfigData(); + + // Leave override free context. + config_context_leave(); + + // Enter context for the translation target language requested and generate + // form with translation data in that language. + config_translation_enter_context($language); + return drupal_get_form(new ConfigTranslationManageForm(), $group, $language, $base_config); + } + } diff --git a/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php b/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php new file mode 100644 index 0000000..06f5fec --- /dev/null +++ b/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php @@ -0,0 +1,98 @@ + $this->group->getTitle(), '@language' => $this->language->name)); + + } + + /** + * {@inheritdoc} + */ + protected function getConfirmText() { + return t('Delete'); + } + + + /** + * {@inheritdoc} + */ + protected function getCancelPath() { + return $this->group->getBasePath() . '/translate'; + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'config_translation_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, ConfigMapperInterface $mapper = NULL) { + // Get configuration group for this mapper. + /* + // @TODO: find a way to get Request object here. + $entity = $request->attributes->get($mapper->getType()); + $this->group = $mapper->getConfigGroup($entity); + $langcode = $request->attributes->get('langcode'); + $this->language = language_load($langcode); + */ + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + + $form_values = $form_state['values']; + + $storage = drupal_container()->get('config.storage'); + foreach ($this->group->getNames() as $name) { + $storage->delete('locale.config.' . $this->language->langcode . '.' . $name); + } + // @todo: do we need to flush caches? The config change may affect page display. + // drupal_flush_all_caches(); + + drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->group->getTitle(), '@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + +} diff --git a/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php b/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php new file mode 100644 index 0000000..fe95dcc --- /dev/null +++ b/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php @@ -0,0 +1,234 @@ +group, $this->language, $this->base_config) = $form_state['build_info']['args']; + + foreach ($this->group->getNames() as $id => $name) { + $form[$id] = array( + '#type' => 'container', + '#tree' => TRUE, + ); + $form[$id] += $this->buildConfigForm(config_typed()->get($name), config($name)->get(), $this->base_config[$name]); + } + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save translation')); + return $form; + } + + /** + * @inheritdoc + */ + public function validateForm(array &$form, array &$form_state) { + + } + + /** + * @inheritdoc + */ + public function submitForm(array &$form, array &$form_state) { + $form_values = $form_state['values']; + + // For the form submission handling, use the override free context. + config_context_enter('config.context.free'); + + foreach ($this->group->getNames() as $id => $name) { + // Set config values based on form submission and original values. + $base_config = config($name); + $translation_config = config('locale.config.' . $this->language->langcode . '.' . $name); + $locations = locale_storage()->getLocations(array('type' => 'configuration', 'name' => $name)); + + $this->setConfig($this->language, $base_config, $translation_config, $form_values[$id], !empty($locations)); + + // If no overrides, delete language specific config file. + $saved_config = $translation_config->get(); + if (empty($saved_config)) { + $translation_config->delete(); + } + else { + $translation_config->save(); + } + } + + config_context_leave(); + + drupal_set_message(t('Updated @language configuration translations successfully.', array('@language' => $this->language->name))); + $form_state['redirect'] = $this->group->getBasePath() . '/translate'; + } + + + /** + * Formats configuration schema as a form tree. + * + * @param $schema + * Schema definition of configuration. + * @param $config + * Configuration object of requested language. + * @param $base_config + * Configuration object of base language. + * @param bool $collapsed + * Flag to set collapsed state. + * @param string|NULL $base_key + * Base configuration key. + * + * @return array + * An associative array containing the structure of the form. + * + * @todo + * Make this conditional on the translatable schema property from + * http://drupal.org/node/1905152 - currently hardcodes label and text. + */ + protected function buildConfigForm($schema, $config, $base_config, $collapsed = FALSE, $base_key = '') { + $build = array(); + foreach ($schema as $key => $element) { + $element_key = implode('.', array_filter(array($base_key, $key))); + $definition = $element->getDefinition() + array('label' => t('N/A')); + if ($element instanceof Element) { + // Build sub-structure and include it with a wrapper in the form + // if there are any translatable elements there. + $sub_build = $this->buildConfigForm($element, $config[$key], $base_config[$key], TRUE, $key); + if (!empty($sub_build)) { + $build[$key] = array( + '#type' => 'details', + '#title' => t($definition['label']), + '#collapsible' => TRUE, + '#collapsed' => $collapsed, + ) + $sub_build; + } + } + else { + $type = $element->getType(); + switch ($type) { + case 'label': + $type = 'textfield'; + break; + case 'text': + $type = 'textarea'; + break; + default: + continue(2); + break; + } + $value = $config[$key]; + $description = format_string('Source string: !source_string', array('!source_string' => nl2br($base_config[$key]) ?: t('(Empty)'))); + $build[$element_key] = array( + '#type' => $type, + '#title' => t($definition['label']), + '#default_value' => $value, + '#description' => $description, + ); + } + } + return $build; + } + + /** + * Sets configuration based on a nested form value array. + * + * @param Language $language + * Language object. + * @param \Drupal\Core\Config\Config $base_config + * Base language configuration values instance. + * @param \Drupal\Core\Config\Config $translation_config + * Translation configuration instance. Values from $config_values will be set + * in this instance. + * @param array $config_values + * A simple one dimensional or recursive array: + * - simple: + * array(name => 'French site name') + * - recursive: + * cancel_confirm => array( + * cancel_confirm.subject => 'Subject' + * cancel_confirm.body => '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)) { + // Traverse into this level in the configuration. + $this->setConfig($language, $base_config, $translation_config, $value, $shipped_config); + } + else { + + // If the config file being translated was originally shipped, we should + // update the locale translation storage. The string should already be + // there, but we make sure to check. + if ($shipped_config && $source_string = locale_storage()->findString(array('source' => $base_config->get($key)))) { + + // Get the translation for this original source string from locale. + $conditions = array( + 'lid' => $source_string->lid, + 'language' => $language->langcode, + ); + $translations = locale_storage()->getTranslations($conditions + array('translated' => TRUE)); + // If we got a translation, take that, otherwise create a new one. + $translation = reset($translations) ?: locale_storage()->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->setString($value) + ->setCustomized() + ->save(); + } + } + + // Save value, if different from original configuration. If same as + // original configuration, remove override. + if ($base_config->get($key) !== $value) { + $translation_config->set($key, $value); + } + else { + $translation_config->clear($key); + } + } + } + } +} diff --git a/lib/Drupal/config_translation/Routing/RouteSubscriber.php b/lib/Drupal/config_translation/Routing/RouteSubscriber.php new file mode 100644 index 0000000..3c5d3bd --- /dev/null +++ b/lib/Drupal/config_translation/Routing/RouteSubscriber.php @@ -0,0 +1,82 @@ +getRouteCollection(); + $config_groups = ConfigTranslationController::getTranslationGroups(); + foreach ($config_groups as $group) { + $path = $group->getBasePath(); + $depth = substr_count($path, '/'); + + $route = new Route($path . '/translate', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemOverviewPage', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouterName(), $route); + + + $route = new Route($path . '/translate/add/{langcode}', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemTranslatePage', + 'action' => 'add', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouterName() . '_add', $route); + + $route = new Route($path . '/translate/edit/{langcode}', array( + '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemTranslatePage', + 'action' => 'edit', + 'mapper' => $group, + ),array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouterName() . '_edit', $route); + + $route = new Route($path . '/translate/delete/{langcode}', array( + '_form' => '\Drupal\config_translation\Form\ConfigTranslationDeleteForm', + 'mapper' => $group, + ), array( + '_config_translation_config_name_access' => 'TRUE', + )); + $collection->add($group->getRouterName() . '_delete', $route); + } + } +}