Commit 23b89ba1eb9 for woocommerce
commit 23b89ba1eb9846b8671e4d850b2d3aff3e663cbe
Author: Mike Jolley <mike.jolley@me.com>
Date: Wed Nov 11 11:38:39 2026 +0000
Use the filtered price format for currency position in blocks (#69710)
* Fix blocks ignoring the filtered price format for currency position
* Add changelog entry for blocks currency position fix
* Ignore markup when detecting currency symbol spacing
* Use strip_tags when detecting currency symbol spacing
* Update CurrencySymbol @since for the next release
diff --git a/plugins/woocommerce/changelog/42250-blocks-filtered-currency-position b/plugins/woocommerce/changelog/42250-blocks-filtered-currency-position
new file mode 100644
index 00000000000..468e1792c22
--- /dev/null
+++ b/plugins/woocommerce/changelog/42250-blocks-filtered-currency-position
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Show the currency symbol in the Mini-Cart, Cart and Checkout blocks where extensions place it with the woocommerce_price_format filter.
diff --git a/plugins/woocommerce/src/Blocks/Assets/AssetDataRegistry.php b/plugins/woocommerce/src/Blocks/Assets/AssetDataRegistry.php
index 6c764549488..5f31c9a268f 100644
--- a/plugins/woocommerce/src/Blocks/Assets/AssetDataRegistry.php
+++ b/plugins/woocommerce/src/Blocks/Assets/AssetDataRegistry.php
@@ -6,6 +6,7 @@ use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
+use Automattic\WooCommerce\Internal\Utilities\CurrencySymbol;
use Automattic\WooCommerce\Internal\Utilities\PriceSeparators;
use Exception;
use InvalidArgumentException;
@@ -120,8 +121,8 @@ class AssetDataRegistry {
return [
'code' => $currency,
'precision' => wc_get_price_decimals(),
- 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ) ),
- 'symbolPosition' => get_option( 'woocommerce_currency_pos' ),
+ 'symbol' => CurrencySymbol::get_symbol(),
+ 'symbolPosition' => CurrencySymbol::get_position(),
'decimalSeparator' => PriceSeparators::get_decimal(),
'thousandSeparator' => PriceSeparators::get_thousand(),
'priceFormat' => html_entity_decode( get_woocommerce_price_format() ),
diff --git a/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php b/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
index def8db66c38..798b89630eb 100644
--- a/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
+++ b/plugins/woocommerce/src/Blocks/Utils/BlocksSharedState.php
@@ -7,6 +7,7 @@ namespace Automattic\WooCommerce\Blocks\Utils;
use InvalidArgumentException;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
+use Automattic\WooCommerce\Internal\Utilities\CurrencySymbol;
use Automattic\WooCommerce\Internal\Utilities\PriceSeparators;
/**
@@ -142,8 +143,8 @@ class BlocksSharedState {
'currency' => array(
'code' => $currency,
'precision' => wc_get_price_decimals(),
- 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ) ),
- 'symbolPosition' => get_option( 'woocommerce_currency_pos' ),
+ 'symbol' => CurrencySymbol::get_symbol(),
+ 'symbolPosition' => CurrencySymbol::get_position(),
'decimalSeparator' => PriceSeparators::get_decimal(),
'thousandSeparator' => PriceSeparators::get_thousand(),
'priceFormat' => html_entity_decode( get_woocommerce_price_format() ),
diff --git a/plugins/woocommerce/src/Internal/Utilities/CurrencySymbol.php b/plugins/woocommerce/src/Internal/Utilities/CurrencySymbol.php
new file mode 100644
index 00000000000..e9094bda835
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Utilities/CurrencySymbol.php
@@ -0,0 +1,59 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\Utilities;
+
+use Automattic\WooCommerce\Enums\CurrencyPosition;
+
+/**
+ * Provides the currency symbol and its position for non-HTML consumers (client settings payloads, APIs).
+ *
+ * The position is read from the filtered price format rather than the `woocommerce_currency_pos`
+ * option, so extensions that filter `woocommerce_price_format` (e.g. currency switchers) apply here too.
+ *
+ * @since 11.4.0
+ */
+class CurrencySymbol {
+
+ /**
+ * Get the store currency symbol with HTML entities decoded.
+ *
+ * ENT_HTML5 matches how browsers parse the entity in HTML output, as in PriceSeparators.
+ *
+ * @return string
+ */
+ public static function get_symbol(): string {
+ return html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 );
+ }
+
+ /**
+ * Get the currency symbol position, as one of the CurrencyPosition values.
+ *
+ * Only visible whitespace between the symbol and the amount in the price format counts as a space,
+ * so markup is ignored and entities such as ` ` are decoded first.
+ * Falls back to the option when the format is missing either placeholder.
+ *
+ * @return string
+ */
+ public static function get_position(): string {
+ $format = (string) get_woocommerce_price_format();
+ $symbol_pos = strpos( $format, '%1$s' );
+ $amount_pos = strpos( $format, '%2$s' );
+
+ if ( false === $symbol_pos || false === $amount_pos ) {
+ return (string) get_option( 'woocommerce_currency_pos' );
+ }
+
+ $placeholder_length = strlen( '%1$s' );
+ $between = (string) substr( $format, min( $symbol_pos, $amount_pos ) + $placeholder_length, abs( $amount_pos - $symbol_pos ) - $placeholder_length );
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- wp_strip_all_tags() trims, which would drop a plain-space separator.
+ $visible = html_entity_decode( strip_tags( $between ), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 );
+ $has_space = 1 === preg_match( '/[\s\p{Zs}]/u', $visible );
+
+ if ( $symbol_pos < $amount_pos ) {
+ return $has_space ? CurrencyPosition::LEFT_SPACE : CurrencyPosition::LEFT;
+ }
+
+ return $has_space ? CurrencyPosition::RIGHT_SPACE : CurrencyPosition::RIGHT;
+ }
+}
diff --git a/plugins/woocommerce/src/StoreApi/Formatters/CurrencyFormatter.php b/plugins/woocommerce/src/StoreApi/Formatters/CurrencyFormatter.php
index 0d4f5a3961b..44393fe920f 100644
--- a/plugins/woocommerce/src/StoreApi/Formatters/CurrencyFormatter.php
+++ b/plugins/woocommerce/src/StoreApi/Formatters/CurrencyFormatter.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\StoreApi\Formatters;
use Automattic\WooCommerce\Enums\CurrencyPosition;
+use Automattic\WooCommerce\Internal\Utilities\CurrencySymbol;
use Automattic\WooCommerce\Internal\Utilities\PriceSeparators;
/**
@@ -18,8 +19,8 @@ class CurrencyFormatter implements FormatterInterface {
* @return array
*/
public function format( $value, array $options = [] ) {
- $position = get_option( 'woocommerce_currency_pos' );
- $symbol = html_entity_decode( get_woocommerce_currency_symbol() );
+ $position = CurrencySymbol::get_position();
+ $symbol = CurrencySymbol::get_symbol();
$prefix = '';
$suffix = '';
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Assets/AssetDataRegistry.php b/plugins/woocommerce/tests/php/src/Blocks/Assets/AssetDataRegistry.php
index d7c23efea32..4398fa42fdd 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Assets/AssetDataRegistry.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Assets/AssetDataRegistry.php
@@ -43,6 +43,25 @@ class AssetDataRegistry extends \WP_UnitTestCase {
$this->assertSame( ',', $currency_data['decimalSeparator'] );
}
+ /**
+ * @testdox Currency symbol position follows the filtered price format.
+ */
+ public function test_currency_data_symbol_position_uses_filtered_price_format() {
+ update_option( 'woocommerce_currency_pos', 'left' );
+ add_filter(
+ 'woocommerce_price_format',
+ function () {
+ return '%2$s %1$s';
+ }
+ );
+
+ $method = new \ReflectionMethod( $this->registry, 'get_currency_data' );
+ $method->setAccessible( true );
+ $currency_data = $method->invoke( $this->registry );
+
+ $this->assertSame( 'right_space', $currency_data['symbolPosition'] );
+ }
+
public function test_add_data() {
$this->registry->add( 'test', 'foo' );
$this->assertEquals( [ 'test' => 'foo' ], $this->registry->get() );
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Formatters/TestCurrencyFormatter.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Formatters/TestCurrencyFormatter.php
index 065c59c8478..67dd9359830 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Formatters/TestCurrencyFormatter.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Formatters/TestCurrencyFormatter.php
@@ -49,4 +49,23 @@ class TestCurrencyFormatter extends \WP_UnitTestCase {
$this->assertSame( "\u{00A0}", $value['currency_thousand_separator'] );
$this->assertSame( ',', $value['currency_decimal_separator'] );
}
+
+ /**
+ * @testdox Currency prefix and suffix follow the filtered price format.
+ */
+ public function test_format_uses_filtered_price_format() {
+ update_option( 'woocommerce_currency_pos', 'left' );
+ add_filter(
+ 'woocommerce_price_format',
+ function () {
+ return '%2$s %1$s';
+ }
+ );
+
+ $value = $this->mock_formatter->format( [] );
+ $symbol = $value['currency_symbol'];
+
+ $this->assertSame( '', $value['currency_prefix'], 'A right-positioned price format should not add a prefix' );
+ $this->assertSame( ' ' . $symbol, $value['currency_suffix'], 'A right-positioned price format should add a spaced suffix' );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
index c99b1a66081..2c83029c7de 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/BlocksSharedStateTest.php
@@ -74,6 +74,25 @@ class BlocksSharedStateTest extends \WC_Unit_Test_Case {
$this->assertSame( ',', $currency_data['currency']['decimalSeparator'] );
}
+ /**
+ * @testdox Currency symbol position follows the filtered price format.
+ */
+ public function test_currency_data_symbol_position_uses_filtered_price_format(): void {
+ update_option( 'woocommerce_currency_pos', 'left' );
+ add_filter(
+ 'woocommerce_price_format',
+ function () {
+ return '%2$s %1$s';
+ }
+ );
+
+ $method = new \ReflectionMethod( BlocksSharedState::class, 'get_currency_data' );
+ $method->setAccessible( true );
+ $currency_data = $method->invoke( null );
+
+ $this->assertSame( 'right_space', $currency_data['currency']['symbolPosition'] );
+ }
+
/**
* @testdox nonOptimisticProperties is empty when no filter is registered.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/CurrencySymbolTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/CurrencySymbolTest.php
new file mode 100644
index 00000000000..ec472b7d959
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/CurrencySymbolTest.php
@@ -0,0 +1,118 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Utilities;
+
+use Automattic\WooCommerce\Enums\CurrencyPosition;
+use Automattic\WooCommerce\Internal\Utilities\CurrencySymbol;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the CurrencySymbol class.
+ */
+class CurrencySymbolTest extends WC_Unit_Test_Case {
+
+ /**
+ * @testdox Should decode HTML entities in the currency symbol.
+ * @testWith ["EUR", "€"]
+ * ["USD", "$"]
+ * ["AED", "د.إ"]
+ *
+ * @param string $currency The store currency.
+ * @param string $expected The expected decoded symbol.
+ */
+ public function test_symbol_is_decoded( string $currency, string $expected ): void {
+ update_option( 'woocommerce_currency', $currency );
+
+ $this->assertSame( $expected, CurrencySymbol::get_symbol(), "The {$currency} symbol should be decoded to plain text" );
+ }
+
+ /**
+ * @testdox Should decode a filtered currency symbol the way browsers parse it.
+ * @testWith ["zł", "zł"]
+ * ["$", "$"]
+ * ["'", "'"]
+ *
+ * @param string $filtered The filtered symbol.
+ * @param string $expected The expected decoded symbol.
+ */
+ public function test_filtered_symbol_is_decoded( string $filtered, string $expected ): void {
+ add_filter(
+ 'woocommerce_currency_symbol',
+ function () use ( $filtered ) {
+ return $filtered;
+ }
+ );
+
+ $this->assertSame( $expected, CurrencySymbol::get_symbol(), "A symbol filtered to '{$filtered}' should decode to '{$expected}'" );
+ }
+
+ /**
+ * @testdox Should match the currency position option when the price format is not filtered.
+ * @testWith ["left"]
+ * ["right"]
+ * ["left_space"]
+ * ["right_space"]
+ *
+ * @param string $position The stored currency position.
+ */
+ public function test_matches_option_when_price_format_is_not_filtered( string $position ): void {
+ update_option( 'woocommerce_currency_pos', $position );
+
+ $this->assertSame( $position, CurrencySymbol::get_position(), "The '{$position}' option should be returned unchanged" );
+ }
+
+ /**
+ * @testdox Should derive the position from a filtered price format.
+ * @dataProvider filtered_price_format_data
+ *
+ * @param string $format The filtered price format.
+ * @param string $expected The expected symbol position.
+ */
+ public function test_derives_position_from_filtered_price_format( string $format, string $expected ): void {
+ update_option( 'woocommerce_currency_pos', CurrencyPosition::LEFT );
+ add_filter(
+ 'woocommerce_price_format',
+ function () use ( $format ) {
+ return $format;
+ }
+ );
+
+ $this->assertSame( $expected, CurrencySymbol::get_position(), "The '{$format}' price format should map to '{$expected}'" );
+ }
+
+ /**
+ * Data provider of filtered price formats and their expected symbol positions.
+ *
+ * @return array
+ */
+ public function filtered_price_format_data(): array {
+ return array(
+ 'right' => array( '%2$s%1$s', CurrencyPosition::RIGHT ),
+ 'right with nbsp' => array( '%2$s %1$s', CurrencyPosition::RIGHT_SPACE ),
+ 'right with space' => array( '%2$s %1$s', CurrencyPosition::RIGHT_SPACE ),
+ 'left with nbsp' => array( '%1$s %2$s', CurrencyPosition::LEFT_SPACE ),
+ 'right with code appended' => array( '%2$s %1$sPLN', CurrencyPosition::RIGHT_SPACE ),
+ 'left with code prepended' => array( 'USD%1$s%2$s', CurrencyPosition::LEFT ),
+ 'left with empty markup' => array( '%1$s<span></span>%2$s', CurrencyPosition::LEFT ),
+ 'right with spaced markup' => array( '%2$s<span> </span>%1$s', CurrencyPosition::RIGHT_SPACE ),
+ 'left with dash' => array( '%1$s-%2$s', CurrencyPosition::LEFT ),
+ 'left with quoted bracket' => array( '%1$s<span title="x > y"></span>%2$s', CurrencyPosition::LEFT ),
+ );
+ }
+
+ /**
+ * @testdox Should fall back to the currency position option when the price format has no symbol placeholder.
+ */
+ public function test_falls_back_to_option_without_symbol_placeholder(): void {
+ update_option( 'woocommerce_currency_pos', CurrencyPosition::RIGHT );
+ add_filter(
+ 'woocommerce_price_format',
+ function () {
+ return '%2$s';
+ }
+ );
+
+ $this->assertSame( CurrencyPosition::RIGHT, CurrencySymbol::get_position(), 'A format without the symbol should not change the position' );
+ }
+}