Commit 650a94976b1 for woocommerce

commit 650a94976b1e31d4a0150b49665b4106edd2f128
Author: Luigi Teschio <gigitux@gmail.com>
Date:   Wed Oct 14 12:08:09 2026 +0200

    Fix extraction of bundled pattern translations (#69697)

    * Fix extraction of bundled pattern translations

    * Add changelog entry for pattern translation extraction

    * simplify test

    * rename file

    * fix unit test

diff --git a/plugins/woocommerce/bin/class-generate-pattern-translations.php b/plugins/woocommerce/bin/class-generate-pattern-translations.php
new file mode 100644
index 00000000000..f6e16cce98f
--- /dev/null
+++ b/plugins/woocommerce/bin/class-generate-pattern-translations.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Pattern header translation extraction.
+ *
+ * @package WooCommerce
+ */
+
+namespace Automattic\WooCommerce\Tools;
+
+use WP_CLI\I18n\FileDataExtractor;
+
+/**
+ * Generates extractable PHP strings from pattern headers.
+ *
+ * @internal
+ */
+class Generate_Pattern_Translations {
+
+	/**
+	 * Generates a PHP translation source without executing pattern files.
+	 *
+	 * @since 11.3.0
+	 *
+	 * @param string $plugin_directory The plugin directory containing patterns.
+	 * @return string
+	 * @throws \RuntimeException When no pattern files are found.
+	 */
+	public function generate( string $plugin_directory ): string {
+		$pattern_files = glob( $plugin_directory . '/patterns/*.php' );
+		if ( empty( $pattern_files ) ) {
+			throw new \RuntimeException( 'No pattern files found for translation extraction.' );
+		}
+		$strings = array();
+		foreach ( $pattern_files as $pattern_file ) {
+			$headers = FileDataExtractor::get_file_data(
+				$pattern_file,
+				array(
+					'Pattern title'       => 'Title',
+					'Pattern description' => 'Description',
+				)
+			);
+			foreach ( $headers as $context => $text ) {
+				if ( '' === $text ) {
+					continue;
+				}
+				$strings[] = sprintf(
+					"\t_x( %s, %s, 'woocommerce' ),\n",
+					var_export( $text, true ), // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- Serialize PHP string literals for the generated file.
+					var_export( $context, true ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- Serialize PHP string literals for the generated file.
+				);
+			}
+		}
+		sort( $strings, SORT_STRING );
+
+		return "<?php\n/**\n * Pattern metadata strings for translation extraction.\n *\n * Generated from pattern headers by `composer generate-pattern-translations`.\n * This file is not loaded at runtime.\n *\n * @package WooCommerce\n */\n\ndefined( 'ABSPATH' ) || exit;\n\nreturn array(\n"
+			. implode( '', $strings )
+			. ");\n";
+	}
+}
diff --git a/plugins/woocommerce/bin/generate-pattern-translations.php b/plugins/woocommerce/bin/generate-pattern-translations.php
new file mode 100644
index 00000000000..9e9e7f82e34
--- /dev/null
+++ b/plugins/woocommerce/bin/generate-pattern-translations.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Generates pattern metadata strings for translation scanners.
+ *
+ * @package WooCommerce
+ */
+
+use Automattic\WooCommerce\Tools\Generate_Pattern_Translations;
+
+require_once __DIR__ . '/composer/wp/vendor/autoload.php';
+require_once __DIR__ . '/class-generate-pattern-translations.php';
+
+$generator = new Generate_Pattern_Translations();
+$source    = $generator->generate( dirname( __DIR__ ) );
+
+// This build script writes a local generated file without loading WordPress.
+// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+if ( false === file_put_contents( __DIR__ . '/../i18n/patterns.php', $source ) ) {
+	throw new \RuntimeException( 'Could not write pattern translation strings.' );
+}
diff --git a/plugins/woocommerce/changelog/fix-bundled-pattern-title-translations b/plugins/woocommerce/changelog/fix-bundled-pattern-title-translations
new file mode 100644
index 00000000000..0ca1f67e411
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-bundled-pattern-title-translations
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Make bundled pattern names available for translation in the editor.
diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json
index fbb62799c4a..8c9caa1bf2a 100644
--- a/plugins/woocommerce/composer.json
+++ b/plugins/woocommerce/composer.json
@@ -146,7 +146,11 @@
 		"phpcbf": [
 			"phpcbf -p"
 		],
+		"generate-pattern-translations": [
+			"@php bin/generate-pattern-translations.php"
+		],
 		"makepot-audit": [
+			"@generate-pattern-translations",
 			"wp --allow-root i18n make-pot . --include=\"woocommerce.php,assets,i18n,includes,lib,packages,patterns,src,templates\" --slug=woocommerce"
 		],
 		"makepot": [
diff --git a/plugins/woocommerce/i18n/patterns.php b/plugins/woocommerce/i18n/patterns.php
new file mode 100644
index 00000000000..252f3d246f1
--- /dev/null
+++ b/plugins/woocommerce/i18n/patterns.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Pattern metadata strings for translation extraction.
+ *
+ * Generated from pattern headers by `composer generate-pattern-translations`.
+ * This file is not loaded at runtime.
+ *
+ * @package WooCommerce
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+return array(
+	_x( 'Centered Header Menu', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Entire Site', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Image Gallery', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Minimal Left Image', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Modern Black', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Split Right Image', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon Store Only', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon with Header and Footer', 'Pattern title', 'woocommerce' ),
+	_x( 'Coming Soon', 'Pattern title', 'woocommerce' ),
+	_x( 'Default Coming Soon', 'Pattern title', 'woocommerce' ),
+	_x( 'Distraction Free Header', 'Pattern title', 'woocommerce' ),
+	_x( 'Essential Header', 'Pattern title', 'woocommerce' ),
+	_x( 'Footer with 3 Menus', 'Pattern title', 'woocommerce' ),
+	_x( 'Footer with Simple Menu', 'Pattern title', 'woocommerce' ),
+	_x( 'Large Footer', 'Pattern title', 'woocommerce' ),
+	_x( 'Large Header', 'Pattern title', 'woocommerce' ),
+	_x( 'Minimal Header', 'Pattern title', 'woocommerce' ),
+	_x( 'No Products Found - Clear Filters', 'Pattern title', 'woocommerce' ),
+	_x( 'No Products Found', 'Pattern title', 'woocommerce' ),
+	_x( 'Product Search', 'Pattern title', 'woocommerce' ),
+	_x( 'Related Products', 'Pattern title', 'woocommerce' ),
+);
diff --git a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
index 5e813650ab8..5b33f43ff05 100644
--- a/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
+++ b/plugins/woocommerce/src/Blocks/Patterns/PatternRegistry.php
@@ -136,11 +136,11 @@ class PatternRegistry {
 			}
 		}

-		// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
-		$pattern_data['title'] = translate_with_gettext_context( wp_strip_all_tags( html_entity_decode( (string) $pattern_data['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ), 'Pattern title', 'woocommerce' );
+		// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
+		$pattern_data['title'] = _x( wp_strip_all_tags( html_entity_decode( (string) $pattern_data['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ), 'Pattern title', 'woocommerce' );
 		if ( ! empty( $pattern_data['description'] ) ) {
-			// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.LowLevelTranslationFunction
-			$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', 'woocommerce' );
+			// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
+			$pattern_data['description'] = _x( $pattern_data['description'], 'Pattern description', 'woocommerce' );
 		}

 		// A pattern is registrable as long as it provides either inline content
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Patterns/GeneratePatternTranslationsTest.php b/plugins/woocommerce/tests/php/src/Blocks/Patterns/GeneratePatternTranslationsTest.php
new file mode 100644
index 00000000000..9a07e937ca6
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/Patterns/GeneratePatternTranslationsTest.php
@@ -0,0 +1,62 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\Patterns;
+
+use Automattic\WooCommerce\Tools\Generate_Pattern_Translations;
+use Gettext\Translations;
+use WP_CLI\I18n\PhpCodeExtractor;
+
+/**
+ * Tests extraction independently of the WP-CLI mocks used by other test classes.
+ *
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ */
+class GeneratePatternTranslationsTest extends \WC_Unit_Test_Case {
+	/**
+	 * The System Under Test.
+	 *
+	 * @var Generate_Pattern_Translations
+	 */
+	private $sut;
+
+	/**
+	 * Loads the translation tooling.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		require_once WC_ABSPATH . 'bin/composer/wp/vendor/autoload.php';
+		require_once WC_ABSPATH . 'bin/class-generate-pattern-translations.php';
+		$this->sut = new Generate_Pattern_Translations();
+	}
+
+	/**
+	 * @testdox The generated source is current and exposes every bundled pattern title to plugin string extraction.
+	 */
+	public function test_bundled_pattern_titles_are_extractable(): void {
+		$source = $this->sut->generate( WC_ABSPATH );
+		// The generated file must stay in sync so WordPress.org can scan release ZIPs.
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+		$this->assertSame( $source, file_get_contents( WC_ABSPATH . 'i18n/patterns.php' ), 'Run composer generate-pattern-translations after changing pattern headers.' );
+		$translations = new Translations();
+		$translations->setDomain( 'woocommerce' );
+		PhpCodeExtractor::fromString(
+			$source,
+			$translations,
+			array(
+				'file' => 'i18n/patterns.php',
+			)
+		);
+
+		$pattern_files = glob( WC_ABSPATH . 'patterns/*.php' );
+		$this->assertNotEmpty( $pattern_files );
+		foreach ( $pattern_files as $pattern_file ) {
+			$metadata = get_file_data( $pattern_file, array( 'title' => 'Title' ) );
+			$this->assertNotFalse(
+				$translations->find( 'Pattern title', $metadata['title'] ),
+				sprintf( 'The title in %s must be available for translation.', basename( $pattern_file ) )
+			);
+		}
+	}
+}