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..6b75adf 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',
);
}
@@ -234,14 +219,14 @@ function config_translation_exists($name, Language $language) {
* can be expanded.
*/
function config_translation_get_groups() {
- $config_groups = drupal_container()->get('module_handler')->invokeAll('config_translation_group_info');
+ $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_container()->get('module_handler')->alter('config_translation_group_info', $path_indexed_groups);
+ Drupal::ModuleHandler()->alter('config_translation_group_info', $path_indexed_groups);
return $path_indexed_groups;
}
@@ -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..3d8d7fd
--- /dev/null
+++ b/lib/Drupal/config_translation/Access/ConfigNameCheck.php
@@ -0,0 +1,54 @@
+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);
+
+ $langcode = $request->attributes->get('langcode');
+ if ($langcode) {
+ $group_language = language_load($langcode);
+ }
+
+ // Only allow access to translate configuration, if proper permissions are
+ // granted, the configuration has translatable pieces, the source language
+ // and target language are not locked, and the target language is not the
+ // original submission language. Although technically config can be overlayed
+ // with translations in the same language, that is logically not a good idea.
+ return (
+ user_access('translate configuration') &&
+ $group->hasSchema() &&
+ $group->hasTranslatable() &&
+ !$group_language->locked &&
+ (empty($language) || (!$language->locked && $language->langcode != $group_language->langcode))
+ );
+ }
+
+}
diff --git a/lib/Drupal/config_translation/ConfigEntityMapper.php b/lib/Drupal/config_translation/ConfigEntityMapper.php
index 75222ee..c90ea24 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,26 @@ class ConfigEntityMapper implements ConfigMapperInterface {
return new ConfigGroupMapper($base_path, $title, $names, $this->menu_type, $this->add_edit_tab);
}
+ /**
+ * {@inheritdoc}
+ */
+ public function setRouteName() {
+ $search = array('/', '-', '{', '}');
+ $replace = array('.', '_', '_', '_');
+ $this->router_name = 'config_translation.item.' . str_replace($search, $replace, $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..b55689a 100644
--- a/lib/Drupal/config_translation/Controller/ConfigTranslationController.php
+++ b/lib/Drupal/config_translation/Controller/ConfigTranslationController.php
@@ -7,10 +7,15 @@
namespace Drupal\config_translation\Controller;
-use Drupal\Core\ControllerInterface;
+use Drupal\Core\TypedData\Type\Language;
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;
+use Drupal\config_translation\Form\ConfigTranslationDeleteForm;
/**
* Controller providing page callbacks for the config translation interface.
@@ -18,7 +23,7 @@ use Drupal\Core\Config\Config;
class ConfigTranslationController implements ControllerInterface {
/**
- * @inheritdoc
+ * {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('database'));
@@ -71,4 +76,183 @@ class ConfigTranslationController implements ControllerInterface {
}
return $page;
}
+
+ /**
+ * Language translations overview page for a configuration name.
+ *
+ ** @param Request $request
+ * Page request object.
+ * @param ConfigMapperInterface $mapper
+ * Configuration mapper.
+ *
+ * @return array
+ * Page render array.
+ */
+ public function itemOverviewPage(Request $request, ConfigMapperInterface $mapper) {
+ $group = $this->getConfigGroup($request, $mapper);
+ drupal_set_title(t('Translations for %label', array('%label' => $group->getTitle())), PASS_THROUGH);
+
+ // It is possible the original language this configuration was saved with is
+ // not on the system. For example, the configuration shipped in English but
+ // the site has no English configured. Represent the original language in the
+ // table even if it is not currently configured.
+ $languages = language_list();
+ $original_langcode = $group->getLangcode();
+ if (!isset($languages[$original_langcode])) {
+ $language_name = language_name($original_langcode);
+ if ($original_langcode == 'en') {
+ $language_name = t('Built-in English');
+ }
+ // Create a dummy language object for this listing only.
+ $languages[$original_langcode] = new Language(array('langcode' => $original_langcode, 'name' => $language_name));
+ }
+
+ $path = $group->getBasePath();
+ $header = array(t('Language'), t('Operations'));
+ $page = array();
+ $page['languages'] = array(
+ '#type' => 'table',
+ '#header' => $header,
+ );
+ foreach ($languages as $language) {
+ if ($language->langcode == $original_langcode) {
+ $page['languages'][$language->langcode]['language'] = array(
+ '#markup' => '' . t('@language (original)', array('@language' => $language->name)) . ''
+ );
+
+ // @todo: the user translating might as well not have access to
+ // edit the original configuration. They will get a 403 for this
+ // link when clicked. Do we know better?
+ $operations = array();
+ $operations['edit'] = array(
+ 'title' => t('Edit'),
+ 'href' => $path,
+ );
+ $page['languages'][$language->langcode]['operations'] = array(
+ '#type' => 'operations',
+ '#links' => $operations,
+ );
+ }
+ else {
+ $page['languages'][$language->langcode]['language'] = array(
+ '#markup' => $language->name,
+ );
+ $operations = array();
+
+ // Check if translation exist for this language.
+ if (!$group->hasTranslation($language)) {
+ $operations['add'] = array(
+ 'title' => t('Add'),
+ 'href' => $path . '/translate/add/' . $language->langcode,
+ );
+ }
+ else {
+ $operations['edit'] = array(
+ 'title' => t('Edit'),
+ 'href' => $path . '/translate/edit/' . $language->langcode,
+ );
+ $operations['delete'] = array(
+ 'title' => t('Delete'),
+ 'href' => $path . '/translate/delete/' . $language->langcode,
+ );
+ }
+ $page['languages'][$language->langcode]['operations'] = array(
+ '#type' => 'operations',
+ '#links' => $operations,
+ );
+ }
+ }
+ return $page;
+ }
+
+ /**
+ * Renders translation item manage form.
+ *
+ * @param Request $request
+ * Page request object.
+ * @param string $action
+ * Action identifier, either 'add' or 'edit'. Used to provide proper
+ * labeling on the screen.
+ * @param ConfigMapperInterface $mapper
+ * Configuration mapper.
+ *
+ * @return array|mixed
+ */
+ public function itemTranslatePage(Request $request, $action, ConfigMapperInterface $mapper) {
+ $group = $this->getConfigGroup($request, $mapper);
+ $language = $this->getLanguage($request, $mapper);
+
+ $replacement = array(
+ '%label' => $group->getTitle(),
+ '@language' => strtolower($language->name),
+ );
+ switch ($action) {
+ case 'add':
+ drupal_set_title(t('Add @language translation for %label', $replacement), PASS_THROUGH);
+ break;
+ case 'edit':
+ drupal_set_title(t('Edit @language translation for %label', $replacement), PASS_THROUGH);
+ break;
+ }
+
+ // Make sure we are in the override free config context. For example,
+ // visiting the configuration page in another language would make those
+ // language overrides active by default. But we need the original values.
+ config_context_enter('config.context.free');
+
+ // Get base language configuration to display in the form before entering
+ // into the language context for the form. This avoids repetitively going
+ // in and out of the language context to get original values later.
+ $base_config = $group->getConfigData();
+
+ // Leave override free context.
+ config_context_leave();
+
+ // Enter context for the translation target language requested and generate
+ // form with translation data in that language.
+ config_translation_enter_context($language);
+ return drupal_get_form(new ConfigTranslationManageForm(), $group, $language, $base_config);
+ }
+
+ /**
+ * Item delete form.
+ *
+ * @param Request $request
+ * @param ConfigMapperInterface $mapper
+ * @return array|mixed
+ */
+ public function itemDeletePage(Request $request, ConfigMapperInterface $mapper) {
+ return drupal_get_form(new ConfigTranslationDeleteForm(), $this->getConfigGroup($request, $mapper), $this->getLanguage($request, $mapper));
+ }
+
+ /**
+ * Helper to get config group.
+ *
+ * @param Request $request
+ * @param ConfigMapperInterface $mapper
+ *
+ * @return ConfigGroupMapper
+ */
+ protected function getConfigGroup(Request $request, ConfigMapperInterface $mapper) {
+ // Get configuration group for this mapper.
+ $entity = $request->attributes->get($mapper->getType());
+ return $mapper->getConfigGroup($entity);
+ }
+
+ /**
+ * Helper to get language object.
+ *
+ * @param Request $request
+ * @param ConfigMapperInterface $mapper
+ *
+ * @return bool|\Drupal\core\Language\Language
+ */
+ protected function getLanguage(Request $request, ConfigMapperInterface $mapper) {
+ $langcode = $request->attributes->get('langcode');
+ if ($langcode) {
+ return language_load($langcode);
+ }
+ return FALSE;
+ }
+
}
diff --git a/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php b/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php
new file mode 100644
index 0000000..3a4beed
--- /dev/null
+++ b/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php
@@ -0,0 +1,91 @@
+ $this->group->getTitle(), '@language' => $this->language->name));
+
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getConfirmText() {
+ return t('Delete');
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getCancelPath() {
+ return $this->group->getBasePath() . '/translate';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormID() {
+ return 'config_translation_delete_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state, ConfigMapperInterface $group = NULL, $language = NULL) {
+ $this->group = $group;
+ $this->language = $language;
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+
+ $form_values = $form_state['values'];
+
+ $storage = drupal_container()->get('config.storage');
+ foreach ($this->group->getNames() as $name) {
+ $storage->delete('locale.config.' . $this->language->langcode . '.' . $name);
+ }
+ // @todo: do we need to flush caches? The config change may affect page display.
+ // drupal_flush_all_caches();
+
+ drupal_set_message(t('@language translation of %label was deleted', array('%label' => $this->group->getTitle(), '@language' => $this->language->name)));
+ $form_state['redirect'] = $this->group->getBasePath() . '/translate';
+ }
+
+}
diff --git a/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php b/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php
new file mode 100644
index 0000000..908577d
--- /dev/null
+++ b/lib/Drupal/config_translation/Form/ConfigTranslationManageForm.php
@@ -0,0 +1,236 @@
+group = $group;
+ $this->language = $language;
+ $this->base_config = $base_config;
+
+ foreach ($this->group->getNames() as $id => $name) {
+ $form[$id] = array(
+ '#type' => 'container',
+ '#tree' => TRUE,
+ );
+ $form[$id] += $this->buildConfigForm(config_typed()->get($name), config($name)->get(), $this->base_config[$name]);
+ }
+
+ $form['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..81fdd21
--- /dev/null
+++ b/lib/Drupal/config_translation/Routing/RouteSubscriber.php
@@ -0,0 +1,82 @@
+getRouteCollection();
+ $config_groups = config_translation_get_groups();
+ 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(
+ '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemDeletePage',
+ 'mapper' => $group,
+ ),array(
+ '_config_translation_config_name_access' => 'TRUE',
+ ));
+ $collection->add($group->getRouterName() . '.delete', $route);
+ }
+ }
+}