Problem/Motivation

We use the locale_deploy module with a custom translation file (translations/custom/custom..po) that is meant to override core/contrib translations. When installing from existing configuration on a site whose default language is not English, these custom overrides are ignored — core's translation wins instead.

Cause: install_import_translations() (core/includes/install.core.inc) calls LocaleFetch::buildUpdateBatch() with no project list and no options. This imports every project, and PoDatabaseWriter runs with empty overwrite_options (first-wins): the first import of a string cannot be overwritten. Projects are imported in getAll() order (drupal first, locale_deploy.custom last), so core is locked in and the custom override is dropped. The subsequent last-wins pass in install_finish_translations() then finds every file already recorded in {locale_file} and re-imports nothing.

Regression from 11.3.x: there, install_import_translations() imported only core (drupal), deferring contrib/custom to the last-wins pass, so overrides applied correctly. The 11.4 locale batch→services refactor changed it to import all projects up front.

Steps to reproduce

1. Site default language de; locale + locale_deploy enabled; overwrite_not_customized: TRUE.
2. A translatable config label whose source string is translated by both core (drupal.de.po: Editor → Herausgeber) and the custom file (custom.de.po: Editor → Editor).
3. drush site:install --existing-config.
4. Expected: the custom override wins (Editor). Actual: core wins (Herausgeber), producing a spurious drush cex diff.

Proposed resolution

Limit the early import pass to core, restoring 11.3 behavior and letting install_finish_translations() import the rest with its last-wins options:

// install_import_translations()
return \Drupal::service(LocaleFetch::class)->buildUpdateBatch(['drupal']);

Remaining tasks

- Add a functional install test: install-from-config with a non-English default language where a contrib/custom project overrides a core string; assert the override wins.
- Confirm no regression for multilingual installs importing several projects.

User interface changes

None.

Introduced terminology

None.

API changes

None.

Data model changes

None.

Release notes snippet

Installing from existing configuration again lets contrib and custom interface translations override core translations. In 11.4.0 the installer imported all projects' translations in a single "first-wins" pass, causing core translations to take precedence over site overrides for non-English default languages.

Disclaimer: Issue was written with the help of claude.

Issue fork drupal-3609363

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

chr.fritsch created an issue. See original summary.

nicxvan’s picture

chr.fritsch’s picture

I already use the latest version of locale_deploy and also tried the weight fix you mentioned. For installing from configuration the problem still exists.

alexpott made their first commit to this issue’s fork.

alexpott’s picture

Compare 11.4.x

function install_import_translations(&$install_state) {
  install_download_additional_translations_operations($install_state);
  // If there is more than one language or the single one is not English, we
  // should import translations.
  $languages = \Drupal::languageManager()->getLanguages();
  if (count($languages) > 1 || !isset($languages['en'])) {
    return \Drupal::service(LocaleFetch::class)->buildUpdateBatch();
  }
}

to 11.3.x

function install_import_translations(&$install_state) {
  $module_handler = \Drupal::moduleHandler();
  $module_handler->loadInclude('locale', 'inc', 'locale.translation');

  // If there is more than one language or the single one is not English, we
  // should import translations.
  $batch_builder = (new BatchBuilder())
    ->setFile(\Drupal::service('extension.path.resolver')->getPath('module', 'locale') . '/locale.batch.inc');
  foreach (install_download_additional_translations_operations($install_state) as $operation) {
    $batch_builder->addOperation($operation[0], $operation[1]);
  }
  $languages = \Drupal::languageManager()->getLanguages();
  if (count($languages) > 1 || !isset($languages['en'])) {
    $batch_builder->addOperation(
      '_install_prepare_import',
      [array_keys($languages), $install_state['server_pattern']],
    );

    // Set up a batch to import translations for drupal core. Translation import
    // for contrib modules happens in install_import_translations_remaining.
    foreach ($languages as $language) {
      if (locale_translation_use_remote_source()) {
        $batch_builder->addOperation(
          'locale_translation_batch_fetch_download',
          ['drupal', $language->getId()],
        );
      }
      $batch_builder->addOperation(
        'locale_translation_batch_fetch_import',
        ['drupal', $language->getId(), []],
      );
    }

    $module_handler->loadInclude('locale', 'inc', 'locale.fetch');
    $batch_builder
      ->setTitle(t('Updating translations.'))
      ->setProgressMessage('')
      ->setErrorMessage(t('Error importing translation files'))
      ->setFinishCallback('locale_translation_batch_fetch_finished');
    return $batch_builder->toArray();
  }
}

The refactor expanded the scope fo what the method does...

return \Drupal::service(LocaleFetch::class)->buildUpdateBatch(); should be return \Drupal::service(LocaleFetch::class)->buildUpdateBatch(['drupal']);

berdir’s picture

Status: Active » Reviewed & tested by the community

Makes sense.

alexpott’s picture

I've managed to write a test for this. It took a while and even throwing some AI time at it did not help. Which goes to show that this code is convoluted and full of side effects.

alexpott’s picture

The test-only change is failing as expected... https://git.drupalcode.org/project/drupal/-/jobs/10825284

nicxvan’s picture

Test looks good too!

Thanks!

  • catch committed 7056932d on main
    fix: #3609363 Regression: install_import_translations() no longer lets...
catch’s picture

Version: main » 11.4.x-dev
Status: Reviewed & tested by the community » Fixed

This looks good. Painful test to write for a one-line fix...

Committed/pushed to main, 11.x and 11.4.x, thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

  • catch committed 10ee8884 on 11.4.x
    fix: #3609363 Regression: install_import_translations() no longer lets...

  • catch committed d75b4ddd on 11.x
    fix: #3609363 Regression: install_import_translations() no longer lets...