Did you know that before Drupal CMS 2.2, if you set up Drupal CMS in German and applied a recipe, its content types, views and roles stayed in English? Recipes are a big part of how Drupal CMS delivers features, so this was a sizable hole in the multilingual experience. Until a few months ago, recipes could not even be translated on localize.drupal.org. So we did a few things to make this possible ahead of DrupalCon Rotterdam. Here are the details.
Making localize.drupal.org offer recipes for translation
Before a site can download recipe translations, there need to be translations to download. And for that, localize.drupal.org needs to find the translatable strings in recipes. That is the job of the Translation Template Extractor (potx), which localize.drupal.org uses to parse every release of every project.
Extracting static strings from recipes was already done and merged in April this year. But in June when I looked again at Haven on localize.drupal.org, every release still had 0 strings. Oops! Turns out potx parsed config in config/install, config/optional and config/schema, but recipes ship their config directly in config/. So I opened and fixed Config inside recipes is not parsed at all in potx with comprehensive tests this time.
Once that was rolled out to localize.drupal.org, strings for site template recipes like Haven and Byte were there, ready for translators. But having translations there is only half of the story.
Locale only knows about modules and themes
The Interface Translation (Locale) module builds its list of translatable projects from installed modules and themes. It finds their name and version in their info file, and the original copy of their configuration in their config/install directory. Recipes have neither. They are not extensions, so Locale simply does not know about them.
Two things were missing for Locale to do its job for recipes:
- A list of which recipes were applied, and in which version. Without a project name and version, Locale cannot build the download URL for the translation file.
- The original translatable configuration the recipe shipped. Locale translates config by comparing the site's config with the original. Without it, nothing gets translated.
The second one is sneaky. Without it, even if the German translation file for Haven was downloaded and imported, "Blog post" would be translated to "Blogbeitrag" in the database, but the content type would still be called "Blog post".
Why not just look at configuration in the recipe directory? Because it is usually not available anymore. The core unpack plugin removes the recipe from Composer's data, and the project template does not commit the recipe directory. A month later someone adds French to the site and there is nothing to compare with.
Recipe Interface Translation Integration (recipe_locale) fixes these problems, and it is already part of Drupal CMS.
How it works
The module records both missing pieces at the moment a recipe is applied, while they are still there, and keeps them in configuration. That way they are exported and deployed with the site, and the production site that only imports config gets them too.
- When a recipe is applied, the module records it and every recipe it applied: the drupal.org project name, the Composer package name, the version and the recipe's name.
- It keeps a copy of only the translatable values of the configuration each recipe shipped, so Locale has an original to compare with long after the recipe directory is gone.
- It adds the recorded recipes to Locale's project list. From then on, Locale treats them like any module: it downloads their translations when they are applied, when a language is added and when cron checks for updates, and it translates their shipped configuration.
The Interface Translation module is optional, and may not be installed right away on your Drupal CMS site, so the installer adds this module to all sites in anticipation that you may need this data later. This way a site that adds Interface Translation later starts with the full list of recipes. Also if a recipe needs to be dropped from the list, there is a Recipe translation updates tab under Reports > Available translation updates for that.
Since we add it to all sites, the question arises: is this a lot of data to keep around? Not really. On fresh Drupal CMS installs with the Haven and Dashi site templates, it is about 90KB, which is 6-7% of the configuration export and 0.20% of the database. In return, roughly 900 strings become translatable that were otherwise out of reach. I'd say that is a good deal!
But, but, but... recipes should not be tracked!
I hear you. Recipes are supposed to be applied once and forgotten, as if someone did those site building steps manually and now owns the result. In practice though, recipes deliver sizable packages of features, especially in Drupal CMS. And there is no way to translate those packages without knowing what was applied.
Ok if they need to be tracked, what about Project Browser and Recipe Tracker, which already track recipes? Why not use those? Project Browser keeps only paths in non-deployable state, without versions. Recipe Tracker is not used by Drupal CMS and also does not handle the versions of unpacked recipes. Its config entity format is much heavier than what translation needs as well. Neither keeps the original translatable config, so both would need almost all of this module added either way.
Since this touches some contested design decisions, I wrote it all down. The module has a docs folder explaining each part, why it is needed and why it cannot be left out:
- Why track recipes at all answers the objections above, with a flowchart of what Locale needs.
- Recording applied recipes explains why the whole recipe tree is recorded and how the version is figured out, even after Composer forgot the recipe.
- Recipes as translation projects covers how recipes get on Locale's project list and when translations get downloaded.
- The configuration recipes ship goes deep on what is stored, how two recipes shipping the same config are handled, and how Locale reads the copies.
If you are interested in how Locale works under the hood, these are a good read even if you never apply a recipe.
What core could do instead
Most of this module is a workaround for two things core does not keep by design: a list of applied recipes and an original copy of the configuration they shipped. The version part, which Drupal CMS polyfills with a version key in the recipe's composer.json, is discussed in #3607162. The meta issue for making recipes translatable is #3488972. Translating recipe input prompts and config action values is planned in #3313863, which this module cannot do on its own. Feedback in those issues is very welcome!
Thanks!
This took quite a few people to come together. Thanks to Jose Reyero , Alex Pott, Jim Birch for the initial recipe translation fixes. Also Adam G-H for the brilliant ideas on both potx and this module. Thanks also to Christian López Espínola, Nathaniel Catchpole and Joachim Namyslo for shaping the plans in the meta issue, and to all the translators on localize.drupal.org who can now get to work on recipes. Last but not least, thanks to Acquia for supporting my time on this.
As with my recent work, I used an LLM heavily to research the problem space, come up with solutions and implement the module with its docs and tests.
Version 1.0.0 has been out since September 25 and is already in use on over 6,700 sites. If you run a multilingual site with recipes, give it a try and let me know how it goes in the issue queue. See you there!