In #2098119: Replace config context system with baked-in locale support and single-event based overrides we baked language support into the heart of the configuration system. This was so we could efficiently preload language configuration overrides. Language overrides are using configuration with name format with language.config.XX.configuration_object_name in order to sort the overrides per langcode (XX is the langcode). Configuration translation access is managed with language module and config factory, writing is in locale module and config translation.

This approach still raises a number of problems for us:

  • Config file explosion leading to unmanageable configuration stored in files - even if active moves to the DB we still want to have a sensible file structure in staging or a git repository.
  • Filename length - we need to keep this less than 255 adding this whooping great prefix on the front does not help
  • Because the language override configuration objects have different names schema don't automatically apply #2168609: Move config translations (language.config.[langcode]) in to new location
  • Depending on how the language configuration is accessed configuration events sometime do fire and sometimes don't. In the configuration translation UI it is accessed both directly though configuration storage and through the config factory.
  • Because language is special cased it is hard for other modules to do the same, domain language module would not be able to override the language values, because they are baked in with highest priority. Also there is no real implementation of overrides (outside of tets), so contrib has no real example to follow.

Proposed resolution

Language configuration overrides should have exactly the same name as the configuration they are overriding. They should be subject to schema validation. They just need to reside in a different directory from active configuration. The patch moves them to a subdirectory language/XX. Language override configuration should only be accessible through a LanguageStorage service. It should not involve the configuration factory since it is not overridable itself.

The patch completely removes all reference to language from the configuration system. Instead we add a CompilerPass to add objects with implement the ConfigOverrideInterface to the factory. During a loadMultiple the override objects are asked for overrides. All module overrides use the same mechanism (the event has been removed). The compiler pass is better than the events, because on English only sites, then no events are fired, and the compiler pass requires explicit registration, so that will only be added/invoked if needed, not all the time. Reading is performance critical, so this is good. Also, if there is no language, we don't need to handle the special case NULL language is ConfigFactory.

The result is a simpler config factory, replaceable config overrides, cleaner config directories, a real implementation of the config override system, there will only be one way to do overrides and the schema checking will be applicable.

Remaining tasks

  • Cache key length is still an issue, the prefixing is still happening there.
  • Make config imports also import the overrides.
  • Consider implementing a LanguageConfigOverride class that is similar to Config separate, so it can provide CRUD but no language override for example.

User interface changes


API changes

(more to detail in writing)

#24 9-24-interdiff.txt79.82 KBalexpott
#24 2201437.24.patch135.48 KBalexpott
PASSED: [[SimpleTest]]: [PHP 5.4 MySQL] 65,255 pass(es). View
#20 Config overrides and language.png89.71 KBGábor Hojtsy
#9 3-9-interdiff.patch17.92 KBalexpott
FAILED: [[SimpleTest]]: [MySQL] Unable to apply patch 3-9-interdiff.patch. Unable to apply patch. See the log in the details link for more information. View
#9 2201437.9.patch105.46 KBalexpott
PASSED: [[SimpleTest]]: [MySQL] 64,613 pass(es). View
#3 2201437.3.patch101.89 KBalexpott
FAILED: [[SimpleTest]]: [MySQL] 64,524 pass(es), 3 fail(s), and 1 exception(s). View


Gábor Hojtsy’s picture

Issue tags: +sprint, +language-config
sun’s picture

Note that I already made very good progress over in:

#2190723: Add a KeyValueStore\FileStorage to replace e.g. ConfigStorage

In essence, it replaces the custom storage engines of the configuration system with regular key/value store implementations.

In the key/value architecture, a "subdirectory" is just simply a "collection".

The most important aspect: Stop coupling the config storage to the filesystem or a specific serialization format.

All of these will work to instantiate storage service for language-specific overrides:

$language_store = new FileStorage('langcode=de', new Yaml(), config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
$language_store = new FileStorage('langcode=de', new Json(), config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
$language_store = new DatabaseStorage('langcode=de', new PhpSerialize(), Database::getConnection(), 'config');
$language_store = new DatabaseStorage('langcode=de', new Xml(), Database::getConnection(), 'config');

# And quite possibly in tests:
$language_store = new MemoryStorage('langcode=de');
alexpott’s picture

101.89 KB
FAILED: [[SimpleTest]]: [MySQL] 64,524 pass(es), 3 fail(s), and 1 exception(s). View

Patch attached move language configuration into subdirectories and implements what is currently proposed in the issue summary.

However just before uploading I discovered that cache cids are also limited to 255 characters as well so the reuse of cache_config and add a prefix to the cache key will encounter the same issues as adding the prefix to the filename. So we either need to ensure config names are less then a certain length or use separate cache bins.

@sun and #2 we need to be able to listAll - until key value stores have that built in an assured then it's not a solution.

alexpott’s picture

Status: Active » Needs review
alexpott’s picture

Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, 3: 2201437.3.patch, failed testing.

Gábor Hojtsy’s picture

Priority: Normal » Critical
Issue tags: +beta blocker

All right, elevating to critical beta blocker in light of #2168609: Move config translations (language.config.[langcode]) in to new location. The sheer extent of API changes suggested is clearly beta blocking. Will close down #2168609: Move config translations (language.config.[langcode]) in to new location as duplicate.

Gábor Hojtsy’s picture

Interesting stuff. Here are some code review comments. I'll keep digesting the changes for a while, there are lots of changes and I don't fully understand yet how all pieces fit.

  1. +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php
    @@ -392,4 +312,30 @@ static function getSubscribedEvents() {
    +  public function getSortedConfigFactoryOverrides() {
    +    if (!isset($this->sortedConfigFactoryOverrides)) {
    +      // Sort the negotiators according to priority.
    +      krsort($this->configFactoryOverrides);
    +      // Merge nested negotiators from $this->configFactoryOverrides into
    +      // $this->sortedConfigFactoryOverrides.
    +      $this->sortedConfigFactoryOverrides = array();
    +      foreach ($this->configFactoryOverrides as $overrides) {
    +        $this->sortedConfigFactoryOverrides = array_merge($this->sortedConfigFactoryOverrides, $overrides);
    +      }
    +    }
    +    return $this->sortedConfigFactoryOverrides;


  2. +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverridePass.php
    @@ -0,0 +1,33 @@
    +  public function process(ContainerBuilder $container) {
    +    $manager = $container->getDefinition('config.factory');
    +    foreach ($container->findTaggedServiceIds('config.factory.override') as $id => $attributes) {
    +      $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
    +      $manager->addMethodCall('addOverride', array(new Reference($id), $priority));
    +    }
    +  }

    Woah, so these would be now services instead of event listeners? (That at least answers my puzzled mind from above where I've seen you removed the module override event).

  3. +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
    @@ -72,14 +72,14 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter
    +  public function installDefaultConfig($type, $extension_name) {
    @@ -131,6 +131,11 @@ public function installDefaultConfig($type, $name) {
    +      $config_override->install($type, $extension_name);
    +++ b/core/lib/Drupal/Core/Config/ConfigManager.php
    @@ -128,6 +128,11 @@ function uninstall($type, $name) {
    +      $config_override->uninstall($type, $name);

    I see why you changed $name to $extension_name to avoid confusion with config names. But this is not consistently done in the uninstall code. And nonetheless seems unrelated?

  4. +++ b/core/lib/Drupal/Core/Config/FileStorage.php
    @@ -241,4 +241,15 @@ public function deleteAll($prefix = '') {
    +   * Sets rhe filesystem path for configuration objects.


  5. +++ b/core/lib/Drupal/Core/CoreServiceProvider.php
    @@ -75,6 +76,9 @@ public function register(ContainerBuilder $container) {
    +    // Add the compiler pass that will process the tagged config factory
    +    // override services.
    +    $container->addCompilerPass(new ConfigFactoryOverridePass());

    So why do these need to be services now compiled to the container?

  6. +++ b/core/lib/Drupal/Core/Datetime/Date.php
    @@ -215,11 +215,14 @@ protected function t($string, array $args = array(), array $options = array()) {
    -      // Enter a language specific context so the right date format is loaded.
    -      $original_language = $this->configFactory->getLanguage();
    -      $this->configFactory->setLanguage(new Language(array('id' => $langcode)));
    +      if ($this->languageManager->isConfigurable()) {
    +        $original_language = $this->languageManager->getConfigOverrideLanguage();
    +        $this->languageManager->setConfigOverrideLanguage(new Language(array('id' => $langcode)));
    +      }
           $this->dateFormats[$format][$langcode] = $this->dateFormatStorage->load($format);
    -      $this->configFactory->setLanguage($original_language);
    +      if ($this->languageManager->isConfigurable()) {
    +        $this->languageManager->setConfigOverrideLanguage($original_language);
    +      }

    This is distinctively harder to do / more ugly looking.

  7. +++ b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php
    @@ -168,4 +168,12 @@ public function getFallbackCandidates($langcode = NULL, array $context = array()
    +   * Determines if thr language manager is configurable or not.


  8. +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php
    @@ -141,9 +136,9 @@ public function buildForm(array $form, array &$form_state, Request $request = NU
    +    $this->configStorage->setLangcode($this->language->id);

    Woo, its own storage, fancy :)

  9. +++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
    @@ -285,13 +281,12 @@ public function testContactConfigEntityTranslation() {
    -      $language_config_name = \Drupal::configFactory()->getLanguageConfigName($langcode, '');
    -      $config_parsed = $file_storage->read($language_config_name);
    +      $config = \Drupal::languageManager()->config('', language_load($langcode));

    Fancy! This actually looks more understandable.

  10. +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php
    @@ -0,0 +1,131 @@
    +   * @todo maybe this should be done somewhere else?
    +   */
    +  public function uninstall($type, $name) {

    Not sure where else would be possible?

  11. +++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php
    @@ -372,4 +408,39 @@ public function getLanguageSwitchLinks($type, $path) {
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function isConfigurable() {
    +    return TRUE;
    +  }
    +++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php
    @@ -17,6 +17,11 @@
    +   * TRUE if the LanguageManager is configurable. Default implementation is FALSE.
    +   */
    +  const CONFIGURABLE = TRUE;

    Why both a const and a method?!

alexpott’s picture

Status: Needs work » Needs review
105.46 KB
PASSED: [[SimpleTest]]: [MySQL] 64,613 pass(es). View
17.92 KB
FAILED: [[SimpleTest]]: [MySQL] Unable to apply patch 3-9-interdiff.patch. Unable to apply patch. See the log in the details link for more information. View
  1. c&p fail although this shows where the pattern of adding tagged services to other services during a compiler pass is from - it is what we do for theme negotiators.
  2. Yes and this means that the language.config_factory_override service is now overridable and replaceable which means a multilingual domain module can do something sane.
  3. This is so we can use $extension_name when calling the install() method on any config override objects. Happy to make the change on ConfigManager::uninstall too or fix change the other usage of $name in ConfigInstaller::install() to be $config_name - which is what I've done in the attached patch.
  4. fixed
  5. This is how config overrides are discovered - see and This has the added benefit that the event system is not involved at all in configuration overriding and therefore not involved in configuration reading.
  6. Well atm in HEAD this is actually a bug since we shouldn't be setting the language on the ConfigFactory unless we have a ConfigurableLanguageManager but one way of making this nicer is to add getConfigOverrideLanguage and setConfigOverrideLanguage to the default LanguageManager but just do nothing in the default implementations. I've implemented this in the latest patch.
  7. fixed
  8. :)
  9. :)
  10. Me neither at the moment
  11. fixed

So whilst writing the following comment:

   * Gets the cache key for a configuration object name.
   * In the default implementation of CachedStorage this simply returns the
   * name. In more complex implementations this allows different versions of
   * the same configuration object to cached. Since the maximum length of a
   * configuration object name is 250 characters this should never add more than
   * 5 characters to the cache key.
   * @see \Drupal\language\Config\LanguageStorage::getCacheKey()
   * @param string $name
   *   The configuration object name.
   * @return string
   *   The cache key.
  protected function getCacheKey($name) {

I realised that of course langcodes can be longer than 5 characters. I think we need to reduce Config::MAX_NAME_LENGTH which at the moment in 250. The question of course is to what. @sun's work on moving config storage to a key value store will help here since a collection is not part of the key. The alternative to do implementing either of the former solutions is to have a cache bin for each language :(

Status: Needs review » Needs work

The last submitted patch, 9: 3-9-interdiff.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
Gábor Hojtsy’s picture

It would be great to see a before/after architecture comparison (IMHO would be great as figures). If nobody else comes around to this, I may be able to do this late this week (I'll hopefully be able to sync up in person with Alex Pott too in London in the meantime).

xjm’s picture

Issue tags: +Naming things is hard
beejeebus’s picture

this doubles the storage calls for multi-lingual sites. i have no words.

alexpott’s picture

But adding an arbitrary prefix to the configuration object name to denote overrides breaks schema and will fall foul of the max config name length. Let alone the impracticalities of dumping everything into the same folder. We can not have everything. If you want to run a high performance multilingual site you are going to need to cache your configuration in something other than the DB. And in fact I would argue the same for a high performance fullstop.

catch’s picture

I'm not that bothered about the extra cache i/o from language overrides. The main problem we have with cache i/o in CMI is pre-loading of configuration objects. So at the moment this might mean 2 x 25 cache hits which is pretty bad. With pre-loading implemented we're looking at more like 2 x 1 or 2 x 5 or whatever, which is going to be better than the monolingual case now.

It'll also be better than i18n language overrides in Drupal 7 where there's a parallel variables table iirc + locale/t() integration for configuration entities now - or at least not worse.

Gábor Hojtsy’s picture

It is definitely better than Drupal 7 and also more flexible than current Drupal 8 with support for swapping out by eg. language domain or other alternate implementations as needed.

vijaycs85’s picture

Gábor Hojtsy’s picture

Issue summary: View changes

Just had a discussion about this at DrupalCamp London. Here are the raw notes:


  • Config overrides baked into config factory
  • Prefixed with language.config.$langcode.*
  • Manage with config translation and locale module (write)
  • We enable to do config translation reading with language module
  • In config translation we use config() (fire config events), and sometimes access through storage


  • Config length 250, any config file which is close there will not be translatable
  • We cannot apply schema because they get a different name, values should match in type
  • Language override is not a complete representation (but not REAL config)
  • In itself cannot be overridden although we treat it as config, its a proto-config, does not exist on its own

Current patch

  • 1. Config in language is not special
  • 2. Instead of event listeners, the overrides are a compiler pass
  • no special handling needed for language in config factory (no NULL language)
  • we would invoke the event all the time, this way it needs to explicitly register
  • reading config fires no events anymore, performance critical to read quicker
  • because the override is a service, it can be overriden (eg. domain language module, group language module)
  • we could do this without changing the language storage (could be separate patch)
  • 3. Introduces use of the override event in core, good example for contrib
  • 4. Question is should we store in config? => store in subdirectory
  • Solves the number of files stored, which would slow down listings, it would be uncontrollable growth
  • Nicer to work with
  • Could apply schema
  • Problem: we still use the config object to read it, but should not
  • 5. Performance concern is this will need to access config once more on the page, but changing storage (eg. non MySQL cache backend) would speed it up


  • Simpler config factory
  • Replaceable language overrides
  • Cleaner config directories
  • Full testing / real implementation of the config override system
  • Only one way to do overrides
  • Schema checking is now natively possible


  • Cache keys are still 255 chars, and we still prefix the cache key with langcode, has the same issue. Langcode may be longer than 5 chars. => Question is how much, eg. domains may be longer, but you don’t need to use the same cache.
  • Importing does not work at the moment. The import method is there, not implemented.
  • Implement a LanguageConfigOverride class that is similar to Config separate, so it can provide CRUD but no language override for example :D Config would be more complex.

Also updated issue summary with better versions of the summaries (not just copied the raw notes :D).

Gábor Hojtsy’s picture

Here is a visual summary to help us move forward. Also adding to the issue summary. Editable at

Gábor Hojtsy’s picture

Issue summary: View changes
Gábor Hojtsy’s picture

Issue summary: View changes
beejeebus’s picture

i don't like this patch, i'd prefer we address the limitations listed in #19 in smaller patches. however, i'm not gonna work on that, so i'm happy to help this land.


alexpott’s picture

Issue summary: View changes
135.48 KB
PASSED: [[SimpleTest]]: [PHP 5.4 MySQL] 65,255 pass(es). View
79.82 KB

So I've spent the weekend working on this and scope creeps.

Aims of the patch

  1. Ensure that schema can be use for language overrides. This is important since this means that code can rely on translated configuration to be the same data type as untranslated config. We have to do this.
  2. Use a sub directory for language overrides. This is important because language overrides can multiple the number of configuration objects by the amount of languages enabled.

Aim 2 has impacts on Configuration synchronisation since how can we know about the new storage directories if language is not yet enabled? Furthermore, even if language is enabled the current ConfigImporter it not built to handle multiple storages.

So the question is could we go back to the patch in #2168609: Move config translations (language.config.[langcode]) in to new location? I don't think so because of the issues with the length of config object name - the only way around it might be shorten the max length and special case language config. However langcodes can be up to 7 characters long so the max needs to be shortened to 227 and the config prefix needs to be checked for in ValidateName and max allowed increased to 250. However in my opinion this type of solution is exactly the type of thinking that has got us into this mess. Special casing and hacking language into CMI is going to lead more of this.

Way forward? (each step could be a separate issue)

  1. Split the Config class into a couple of base classes so that the functionality can be reused - the attached patch does this and discovered an issue with ThemeSettings not using the same merge method as Config.
  2. Convert the language overrides and module overrides into a compiler pass. Doing this will allow customisation to language overrides for multi domain, provide a single way of implementing overrides, and is a (minor) performance optimisation since the event dispatcher is not longer invoked during a configuration read
  3. Enable configuration importer to work with multiple storages (staging and active).
  4. Move language configuration to a new storage.
Gábor Hojtsy’s picture

I think this starts to reach into way too many areas. We should maybe start by breaking out the compiler pass and the wrapping of overrides into its own issue, get that in and then continue forward... The extent of changes in this patch and the reach it has far and wide makes it hard to believe people would agree on all fronts at once :/

Gábor Hojtsy’s picture

Title: Config overrides and language » [META] Config overrides and language
Status: Needs review » Postponed

We agreed on breaking this up. Here is one of the items: #2215503: Use services instead of event subscribers for config module overrides. I believe Eli-T is working on another part.

Eli-T’s picture

xjm’s picture

Status: Postponed » Active

Since this is a meta, let's make it active so we can get continued architectural input here.

Gábor Hojtsy’s picture

I'm hard at work on #2219499: Generalize language config override responsibility which will break out the language override features. It will NOT introduce its own storage, so diffs, deployment/install/uninstall/sync are still not affected. There is considerable code here still to handle that, so this issue will still not be fully solved, but that is a well rounded problem space we can cover.

Gábor Hojtsy’s picture

#2219499: Generalize language config override responsibility now has the config language refactoring and its only a 70k patch. Much nicer to review :) Feedback welcome there.

jessebeach’s picture

Title: [META] Config overrides and language » [META-1] Config overrides and language
Gábor Hojtsy’s picture

The final(?) sub-issue then is #2224887: Language configuration overrides should have their own storage :) We still have some code from @alexpott above to put in there :)

Gábor Hojtsy’s picture

Gábor Hojtsy’s picture

Status: Active » Closed (duplicate)

#2224887: Language configuration overrides should have their own storage has all the remainder of this one, so closing this down as duplicate. Will update the issue summary there to carry over all remaining info that applies.

Gábor Hojtsy’s picture

Issue tags: -sprint

Yay for only one issue left.