Commit 7ad15ccb8ec for woocommerce

commit 7ad15ccb8ec82fe095739be93ffe4ebc22736fcb
Author: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com>
Date:   Tue Nov 10 19:37:58 2026 +0530

    Fix fatal error when combining translation files made with wp i18n make-json (#70751)

    * Combine user translation chunks that use the "messages" domain

    `wp i18n make-json` writes its strings under locale_data.messages, not
    locale_data.woocommerce. With two or more such files for a locale,
    combine_user_translation_chunks() passed null to array_merge(), and the
    TypeError ended the request: activating WooCommerce from wp-admin showed
    the critical error page, and the combined file was never written. Merge
    under the key the files use.

    * Read each translation chunk's own key when combining user files

    A later file using the other key (make-json vs Loco Translate) merged as
    an empty array and its strings were lost. Initialise $domain_key before
    the loop, and add tests for make-json files and both mixed orders.

diff --git a/plugins/woocommerce/changelog/fix-user-translation-chunks-messages-domain b/plugins/woocommerce/changelog/fix-user-translation-chunks-messages-domain
new file mode 100644
index 00000000000..e0d43f3756a
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-user-translation-chunks-messages-domain
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Avoid a fatal error when combining translation files made with `wp i18n make-json`, which keep their strings under the "messages" domain.
diff --git a/plugins/woocommerce/src/Internal/Admin/Translations.php b/plugins/woocommerce/src/Internal/Admin/Translations.php
index d2c15e6c3b4..fdc7e727714 100644
--- a/plugins/woocommerce/src/Internal/Admin/Translations.php
+++ b/plugins/woocommerce/src/Internal/Admin/Translations.php
@@ -138,6 +138,7 @@ class Translations {
 		// the filesystem object should be hooked up.
 		global $wp_filesystem;
 		$combined_translation_data = array();
+		$domain_key                = 'messages';

 		foreach ( $json_i18n_filenames as $json_filename ) {
 			if ( ! $wp_filesystem->is_readable( $json_filename ) ) {
@@ -161,14 +162,18 @@ class Translations {
 				continue;
 			}

+			// `wp i18n make-json` keeps strings under "messages"; Loco Translate uses the text domain.
+			$chunk_domain_key = isset( $chunk_data['locale_data']['woocommerce'] ) ? 'woocommerce' : 'messages';
+
 			if ( empty( $combined_translation_data ) ) {
 				// Use the first translation file as the base structure.
 				$combined_translation_data = $chunk_data;
+				$domain_key                = $chunk_domain_key;
 			} else {
 				// Combine all messages from all chunk files.
-				$combined_translation_data['locale_data']['woocommerce'] = array_merge(
-					$combined_translation_data['locale_data']['woocommerce'],
-					$chunk_data['locale_data']['woocommerce']
+				$combined_translation_data['locale_data'][ $domain_key ] = array_merge(
+					$combined_translation_data['locale_data'][ $domain_key ] ?? array(),
+					$chunk_data['locale_data'][ $chunk_domain_key ] ?? array()
 				);
 			}
 		}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/TranslationsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/TranslationsTest.php
index ce054d580ba..a0d92202675 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/TranslationsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/TranslationsTest.php
@@ -130,6 +130,96 @@ class TranslationsTest extends WC_Unit_Test_Case {
 		$this->created_files[] = $path;
 	}

+	/**
+	 * Writes a chunk translation JSON file in a user-generated format: `wp i18n make-json`
+	 * keeps strings under "messages", Loco Translate under the text domain.
+	 *
+	 * @param string $md5_name   Fake md5 part of the filename.
+	 * @param string $source     Source file path stored in the JSON.
+	 * @param string $domain_key Key the messages are stored under in locale_data.
+	 * @param array  $messages   Translation messages.
+	 */
+	private function create_user_chunk_json( $md5_name, $source, $domain_key, $messages ) {
+		$data = array(
+			'translation-revision-date' => '2026-01-01 00:00:00+0000',
+			'generator'                 => 'WP-CLI/2.12.0',
+			'source'                    => $source,
+			'domain'                    => $domain_key,
+			'locale_data'               => array(
+				$domain_key => array_merge(
+					array(
+						'' => array(
+							'domain' => $domain_key,
+							'lang'   => 'de',
+						),
+					),
+					$messages
+				),
+			),
+		);
+
+		$path = $this->lang_dir . 'woocommerce-de_DE-' . $md5_name . '.json';
+		file_put_contents( $path, wp_json_encode( $data ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+		$this->created_files[] = $path;
+	}
+
+	/**
+	 * Builds the combined translation file and returns its decoded contents.
+	 *
+	 * @return array Combined translation data.
+	 */
+	private function build_combined_file(): array {
+		if ( ! function_exists( 'get_filesystem_method' ) ) {
+			require_once ABSPATH . 'wp-admin/includes/file.php';
+		}
+		\WP_Filesystem();
+		$build = new \ReflectionMethod( Translations::class, 'build_and_save_translations' );
+		$build->setAccessible( true );
+		$build->invoke( $this->sut, $this->lang_dir, 'woocommerce', 'de_DE' );
+
+		return json_decode( file_get_contents( $this->lang_dir . 'woocommerce-de_DE-' . WC_ADMIN_APP . '.json' ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+	}
+
+	/**
+	 * Key combinations of the two user-generated files, in the order they are read.
+	 *
+	 * @return array[]
+	 */
+	public function user_chunk_key_provider(): array {
+		return array(
+			'make-json, make-json' => array( 'messages', 'messages' ),
+			'Loco, make-json'      => array( 'woocommerce', 'messages' ),
+			'make-json, Loco'      => array( 'messages', 'woocommerce' ),
+		);
+	}
+
+	/**
+	 * @testdox Should combine user-generated chunk files whichever key each one uses.
+	 * @dataProvider user_chunk_key_provider
+	 *
+	 * @param string $first_key  Key used by the file read first.
+	 * @param string $second_key Key used by the file read second.
+	 */
+	public function test_combines_user_generated_chunks( string $first_key, string $second_key ): void {
+		$this->create_user_chunk_json(
+			'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+			WC_ADMIN_DIST_JS_FOLDER . 'app/index.js',
+			$first_key,
+			array( 'Show' => array( 'Anzeigen' ) )
+		);
+		$this->create_user_chunk_json(
+			'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+			WC_ADMIN_DIST_JS_FOLDER . 'chunks/analytics-report-products.js',
+			$second_key,
+			array( 'Compare Products' => array( 'Produkte vergleichen' ) )
+		);
+
+		$messages = $this->build_combined_file()['locale_data'][ $first_key ];
+
+		$this->assertSame( array( 'Anzeigen' ), $messages['Show'], 'Strings from the first file should be kept' );
+		$this->assertSame( array( 'Produkte vergleichen' ), $messages['Compare Products'], 'Strings from the second file should be merged in' );
+	}
+
 	/**
 	 * @testdox Should leave the translation file untouched for other script handles.
 	 */