I am porting www.example.com, the German version is at de.example.com, the French is at fr.example.com and so forth. I could enter these domains in admin/config/regional/language/detection/url but I would like them to be subdomains: if one visits www.example.test then (www,de,fr).example.test should work, and if one visits www.example.local then (www,de,fr).example.local should be the translated domains and so forth. It seems I need to make the results of $config = $this->config->get('language.negotiation')->get('url'); dynamic. I am at a complete loss at how to implement this:

  1. I could write an entirely new plugin based on LanguageNegotiationUrl. If I started with a copy of it, I could drop processInbound and half of getLangcode but the rest would need to stay -- and given how little change needs to happen, that's a lot of code to keep in sync with core.

  2. I could patch LanguageNegotiationUrl factoring out the four $config['domains'] interactions into a method and then extend accordingly. Much less maintenance pain than the previous one but at the same time, it's a core patch. Of course, composer-patch applies them after updates but still, a core patch for this?

  3. I could write a config override which overrides language.negotiation but it seems the overrider doesn't get the data loaded. So I'd need to get the domain map from somewhere else, for example by re-reading it from the config storage. That's really, really wasteful.

  4. I could write a "super saiyan config override" by decorating the config factory service so when the language.negotiation is get() then do something with it, like put the dynamic overrides in setModuleOverride. OK, that's not a lot of code, great, the proxy script will write most of it, even better, but now all config calls are a hair more complex and a hair slower.

Any clean ideas I missed...?

  • I wouldn't rewrite or patch core code. You could add your own path processor with a higher priority to handle the domain stuff before the path reaches the language negotiation. – 4k4 Oct 2 '18 at 6:54
  • Intriguing. Care to expand this into an answer? From what I see in LanguageNegotiationUrl::processOutbound, the domain is not available separately, all that code is busy reconstructing base_url. – Smartsheet eng Oct 2 '18 at 7:04
  • have you investigated drupal.org/project/domain – bleen Oct 2 '18 at 15:59

This sounds to me like the perfect use case for environment specific configuration. No runtime changes required. Just like you would swap the email addresses in your environments that are used to send emails and would swap the API keys in dev/test environments, you would swap this configuration there as well. You could use something like https://www.drupal.org/project/config_split for example.

  1. config_split I have multple problems with: one is philosophical, having multiple branches of config sounds like a good way to complicate deployments where you need to make sure the right config gets to live. The other is practical, the team is big and fluid and everyone needing to configure the domains to their preference is not ideal.
  2. If I resolve to not touch the actual negotiating code (which would be very highly desirable) then the path processor is a no-go because LanguageNegotiationUrl::getLangcode calls $request->getHost(). And Request::getHost is not a simple getter of a property (tellingly there's no setHost) rather it calculates the host every time from various headers so this manipulation would need to replicate and fight this logic unsetting this , setting that... This is another no-go.

I ended with writing a class proxying ConfigFactory and a service provider injecting this right into the negotiatior service:

public function alter(ContainerBuilder $container) {
  if ($container->hasDefinition('language_negotiator') && $_ENV['SMARTSHEET_SITE_ENVIRONMENT '] !== 'prod)') {
    $definition = $container->getDefinition('language_negotiator');
    if ($definition->getClass() === 'Drupal\language\LanguageNegotiator') {
      $definition->replaceArgument(2, new Reference('sd8.language_config_factory'));

and then the SubdomainConfigFactory does this:

public function get($name) {
  $config = $this->configFactory->get($name);
  if ($name === 'language.negotiation') {
    $domainSuffix = preg_replace('/^[^.]+/', '', $this->requestStack->getCurrentRequest()->getHost());
    $override['url']['domains'] = [];
    foreach ($config->get('url.domains') as $langcode => $domain) {
      $override['url']['domains'][$langcode] = strtok($domain, '.') . $domainSuffix;
  return $config;

This is very far from ideal and I feel there must be a nicer way to work with dynamic config and I will post if I find out how.


This can simply be overridden from your settings.php or settings.local.php.

$config['language.negotiation']['url']['domains']['en'] = 'www.example.test';
$config['language.negotiation']['url']['domains']['de'] = 'de.example.test';
$config['language.negotiation']['url']['domains']['fr'] = 'fr.example.test';

Which means, that you need to set them manually in every environment once. Each of your coworkers needs to adjust their settings.php or settings.local.php according their needs.


You could add a custom path processor. To handle the domain path part before language negotiation you need to choose a priority inbound >300 and outbound <100, because these are the priorities of the language path processor:

    class: Drupal\mymodule\PathProcessor\PathProcessorCustom
      - { name: path_processor_inbound, priority: 310 }
      - { name: path_processor_outbound, priority: 90 }
    arguments: ['@config.factory']

For more information see Do new routes in Drupal 8 *require* a static first portion?

  • Well, this only describes how to define a path processor, that's OK. But what do you actually do with it? A significant portion of LanguageNegotiationUrl::processOutbound seems to be just that -- reconstructing base_url. And if I am my own processor then I actually need to implement a LanguageNegotiationUrl::processInbound and so I do not readily see how would this end up with less code than doing option #1 from the question. (I would also like to point out I have asked the exact same in the comment on the question.) – Smartsheet eng Oct 2 '18 at 8:57
  • If you set the priority <100 your outbound path processor runs after LanguageNegotiationUrl::processOutbound or any other language negotiation plugin and you only need to change the top level domain in $options['base_url']. – 4k4 Oct 2 '18 at 11:32
  • I am a bit wary of re-parsing $options['base_url'] but I guess something like packagist.org/packages/spatie/url would make it more solid -- and if the domains configured are the production domains then it could be ran only on non-production. – Smartsheet eng Oct 2 '18 at 15:53
  • My answer is about a better way than 1 - 4 of your question to override the result of path processing in language negotiation. It's not about for which cases this is useful. If you want to do this only for local dev, then you can override the language config in settings.local.php: $config['language.negotiation']['url']['domains']['de'] = .... – 4k4 Oct 2 '18 at 17:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.