diff --git a/readme.txt b/readme.txt index 44231e01..07bb4efb 100644 --- a/readme.txt +++ b/readme.txt @@ -286,6 +286,7 @@ Please make sure you make a backup of your database before updating any version * Improved: the WooCommerce add-to-cart, remove-from-cart, checkout and purchase events now include a currency property, so they can be broken down by the currency they were placed in. The purchase event uses the currency the order was placed in. * Fixed: in "domain per language" mode, the proxy endpoint and the locally cached tracker script were loaded from the default domain, which could cause CORS errors on the other language domains. * Fixed: Plugin Tokens for language domains whose key contains an underscore (e.g., TranslatePress' nl_NL) were stored under a stripped key, which meant they were never read back. +* Fixed: goal, event and funnel names (e.g. "Woo Complete Purchase") were translatable, so a translation update could start sending events under new names, splitting each goal's data and stopping revenue from being recorded. They're now always sent in English. On French sites, where these names were translated in September 2026, events are sent to the original goals again; goals created with the French names can be removed from your Plausible dashboard. * Tested with WP 7.1. = 2.6.1 = diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 412dfd39..72afb5b3 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -89,12 +89,16 @@ class Provisioning { public function __construct( $client = null ) { $this->client = $client; + /** + * These names identify the goals in Plausible Analytics and must match the names the events are sent under, so + * they're deliberately not translatable. @see https://github.com/plausible/wordpress/issues/326 + */ $this->custom_event_goals = [ - EnhancedMeasurements::FOUR_O_FOUR => __( '404', 'plausible-analytics' ), - EnhancedMeasurements::CLOAKED_AFFILIATE_LINKS => __( 'Cloaked Link: Click', 'plausible-analytics' ), - EnhancedMeasurements::FORM_COMPLETIONS => __( 'WP Form Completions', 'plausible-analytics' ), - EnhancedMeasurements::QUERY_PARAMS => __( 'WP Query Parameters', 'plausible-analytics' ), - EnhancedMeasurements::SEARCH_QUERIES => __( 'WP Search Queries', 'plausible-analytics' ), + EnhancedMeasurements::FOUR_O_FOUR => '404', + EnhancedMeasurements::CLOAKED_AFFILIATE_LINKS => 'Cloaked Link: Click', + EnhancedMeasurements::FORM_COMPLETIONS => 'WP Form Completions', + EnhancedMeasurements::QUERY_PARAMS => 'WP Query Parameters', + EnhancedMeasurements::SEARCH_QUERIES => 'WP Search Queries', ]; $this->init(); diff --git a/src/Admin/Provisioning/Integrations.php b/src/Admin/Provisioning/Integrations.php index d2d58c0a..b71eed8a 100644 --- a/src/Admin/Provisioning/Integrations.php +++ b/src/Admin/Provisioning/Integrations.php @@ -174,7 +174,7 @@ private function maybe_dismantle_outdated_funnel( $funnel_name, $view_product_pa $first_step = (string) ( $steps[0]['goal']['display_name'] ?? '' ); - // Only a view-product step is checked: other steps are named after the (translatable) event goals. + // Only the view-product step is checked: it's the only step whose goal changes, with the languages served. if ( strpos( $first_step, 'Visit ' ) !== 0 || $first_step === sprintf( 'Visit %s', $view_product_path ) ) { return $all_ids; } diff --git a/src/Admin/Provisioning/Integrations/EDD.php b/src/Admin/Provisioning/Integrations/EDD.php index 2ba35df1..da3809ec 100644 --- a/src/Admin/Provisioning/Integrations/EDD.php +++ b/src/Admin/Provisioning/Integrations/EDD.php @@ -58,7 +58,8 @@ public function maybe_create_edd_funnel( $old_settings, $settings ) { $edd = new Integrations\EDD( false ); - $this->integrations->create_integration_funnel( $edd->event_goals, __( 'EDD Purchase Funnel', 'plausible-analytics' ), $edd->post_type ); + // Plausible Analytics identifies funnels by name, so it's deliberately not translatable, like the goal names. + $this->integrations->create_integration_funnel( $edd->event_goals, 'EDD Purchase Funnel', $edd->post_type ); } /** diff --git a/src/Admin/Provisioning/Integrations/WooCommerce.php b/src/Admin/Provisioning/Integrations/WooCommerce.php index 55155af4..20d56d51 100644 --- a/src/Admin/Provisioning/Integrations/WooCommerce.php +++ b/src/Admin/Provisioning/Integrations/WooCommerce.php @@ -59,7 +59,8 @@ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) { $woocommerce = new Integrations\WooCommerce( false ); - $this->integrations->create_integration_funnel( $woocommerce->event_goals, __( 'Woo Purchase Funnel', 'plausible-analytics' ), $woocommerce->post_type ); + // Plausible Analytics identifies funnels by name, so it's deliberately not translatable, like the goal names. + $this->integrations->create_integration_funnel( $woocommerce->event_goals, 'Woo Purchase Funnel', $woocommerce->post_type ); } /** diff --git a/src/Admin/Upgrades.php b/src/Admin/Upgrades.php index 70e95145..dbe6ae07 100644 --- a/src/Admin/Upgrades.php +++ b/src/Admin/Upgrades.php @@ -432,13 +432,17 @@ private function upgrade_to_260() { } /** - * After updating to 2.6.2, (re)create the Custom Properties and — for multilingual ecommerce installs — the - * integration goals, so existing installs pick up the currency Custom Property and the per-language - * Pageview goals (e.g. /es/producto*) without having to save their settings first. + * After updating to 2.6.2, (re)create the goals, the Custom Properties and the integration funnels, so existing + * installs pick them up without having to save their settings first: * - * The Custom Properties apply to every WooCommerce/EDD install with Ecommerce Revenue enabled. The localized - * Pageview goals only apply when a multilingual plugin (WPML, with or without WooCommerce Multilingual & - * Multicurrency, or TranslatePress) is active. + * - Goal and funnel names are no longer translated (@see https://github.com/plausible/wordpress/issues/326). Sites + * whose goals were created under translated names (e.g. French, since September 2026) now send their events under + * the English names, so those goals, including the Revenue goal, must exist. + * - The currency Custom Property applies to every WooCommerce/EDD install with Ecommerce Revenue enabled. + * - When a multilingual plugin (WPML, with or without WooCommerce Multilingual & Multicurrency, or TranslatePress) + * is active, the view-product goals are localized per language (e.g. /es/producto*). + * + * Goals and funnels are created with get-or-create requests, so existing ones are left unchanged. * * This runs on init (@see Upgrades::__construct()), so the multilingual plugin's languages are available: WPML loads * them on plugins_loaded and TranslatePress stores them in an option. When there are none (e.g. WPML's setup hasn't @@ -452,33 +456,33 @@ private function upgrade_to_260() { * @codeCoverageIgnore because all we'd be doing is testing the Plugins API. */ public function upgrade_to_262() { + $provisioning = new Provisioning(); + $settings = Helpers::get_settings(); + + /** + * (Re)create the goals of the enabled Enhanced Measurements under their (untranslated) names. Bails when no Plugin + * Token is entered yet; the goals are created as soon as one is. + * + * @see Provisioning::maybe_provision_on_connect() + */ + $provisioning->maybe_create_goals( [], $settings ); + $is_ecommerce = \Plausible\Analytics\WP\Integrations::is_wc_active() || \Plausible\Analytics\WP\Integrations::is_edd_active(); if ( $is_ecommerce && EnhancedMeasurements::is_enabled( EnhancedMeasurements::ECOMMERCE_REVENUE ) ) { - $provisioning = new Provisioning(); - $settings = Helpers::get_settings(); - - /** - * The currency Custom Property (@see Provisioning::CUSTOM_PROPERTIES) applies to every ecommerce install, - * so (re)create the Custom Properties whether or not a multilingual plugin is active. Bails when no Plugin - * Token is entered yet; the properties are created as soon as one is. - * - * @see Provisioning::maybe_provision_on_connect() Creates the goals and custom properties as soon as a - * Plugin Token is entered. - */ + // The currency Custom Property (@see Provisioning::CUSTOM_PROPERTIES) applies to every ecommerce install. $provisioning->maybe_create_custom_properties( [], $settings ); /** - * The localized Pageview goals only matter when a multilingual plugin is active and serves languages. When - * it doesn't serve any yet (e.g. WPML's setup hasn't been completed), skip the funnels rather than - * provisioning the default language's path only; they're (re)created with the right paths on the next - * settings save. + * (Re)create the funnels and their goals. When a multilingual plugin is active but doesn't serve any languages + * yet (e.g. WPML's setup hasn't been completed), skip them rather than provisioning the default language's + * path only; they're (re)created with the right paths on the next settings save. * * @see Provisioning\Integrations\WooCommerce::init() Both funnels are (re)created on * update_option_plausible_analytics_settings, i.e. when the Plugin Token is saved. * @see Provisioning\Integrations\EDD::init() */ - if ( Helpers::get_multilang_plugin() && ! empty( Helpers::get_active_languages() ) ) { + if ( ! Helpers::get_multilang_plugin() || ! empty( Helpers::get_active_languages() ) ) { $integrations = new Integrations( $provisioning ); ( new Provisioning\Integrations\WooCommerce( $integrations ) )->maybe_create_woocommerce_funnel( [], $settings ); diff --git a/src/Helpers.php b/src/Helpers.php index 483516db..2b6fcec9 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -67,6 +67,10 @@ public static function get_active_languages() { * language) the store's base currency is used, and Plausible converts foreign-currency purchases into * it automatically. * + * The base currency is read from WooCommerce's setting instead of get_woocommerce_currency(), because multicurrency + * plugins filter the latter to the currency of the current request (e.g. WCML returns the visitor's currency in + * AJAX requests), which would make the goal's currency depend on whichever request happens to provision it. + * * @since 2.6.2 * * @param string $domain_key 'default' or a WPML language code (e.g. 'es'). @@ -74,7 +78,7 @@ public static function get_active_languages() { * @return string ISO 4217 currency code. */ public static function get_currency_for_language( $domain_key = 'default' ) { - $base = Integrations::is_edd_active() ? edd_get_currency() : get_woocommerce_currency(); + $base = Integrations::is_edd_active() ? edd_get_currency() : (string) get_option( 'woocommerce_currency', 'USD' ); // A default currency per language is a WCML (WooCommerce + WPML) concept only. if ( Integrations::is_edd_active() || static::get_multilang_plugin() !== static::MULTILANG_PLUGIN_WPML ) { diff --git a/src/Integrations/EDD.php b/src/Integrations/EDD.php index b20d3937..954c4ed9 100644 --- a/src/Integrations/EDD.php +++ b/src/Integrations/EDD.php @@ -38,13 +38,17 @@ public function __construct( $init = true ) { $uri = '/' . $uri; } + /** + * These names identify the goals in Plausible Analytics, so they must never change: they're deliberately not + * translatable, because a translation (update) would start sending events under new names, splitting each + * goal's data and leaving e.g. the Revenue goal behind. @see https://github.com/plausible/wordpress/issues/326 + */ $this->event_goals = [ - // translators: %s: Product page URI pattern. - 'view-product' => sprintf( __( 'Visit %s*', 'plausible-analytics' ), $uri ), - 'add-to-cart' => __( 'EDD Add to Cart', 'plausible-analytics' ), - 'remove-from-cart' => __( 'EDD Remove from Cart', 'plausible-analytics' ), - 'checkout' => __( 'EDD Start Checkout', 'plausible-analytics' ), - 'purchase' => __( 'EDD Complete Purchase', 'plausible-analytics' ), + 'view-product' => sprintf( 'Visit %s*', $uri ), + 'add-to-cart' => 'EDD Add to Cart', + 'remove-from-cart' => 'EDD Remove from Cart', + 'checkout' => 'EDD Start Checkout', + 'purchase' => 'EDD Complete Purchase', ]; $this->init( $init ); diff --git a/src/Integrations/FormSubmit.php b/src/Integrations/FormSubmit.php index 534a2cd7..61b1d9fb 100644 --- a/src/Integrations/FormSubmit.php +++ b/src/Integrations/FormSubmit.php @@ -65,7 +65,8 @@ public function add_js() { wp_localize_script( 'plausible-form-submit-integration', 'plausible_analytics_i18n', - [ 'form_completions' => __( 'WP Form Completions', 'plausible-analytics' ), ] + // The goal's name, which is deliberately not translatable. @see Provisioning::__construct() + [ 'form_completions' => 'WP Form Completions', ] ); wp_enqueue_script( 'plausible-form-submit-integration' ); @@ -109,7 +110,7 @@ private function track_submission( $uri ) { $proxy = new Proxy( false ); $proxy->do_request( - __( 'WP Form Completions', 'plausible-analytics' ), + 'WP Form Completions', null, null, [ 'path' => $uri ] diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 4297ceb3..cf46e5cc 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -41,13 +41,17 @@ public function __construct( $init = true ) { $uri = '/' . $uri; } + /** + * These names identify the goals in Plausible Analytics, so they must never change: they're deliberately not + * translatable, because a translation (update) would start sending events under new names, splitting each + * goal's data and leaving e.g. the Revenue goal behind. @see https://github.com/plausible/wordpress/issues/326 + */ $this->event_goals = [ - // translators: %s: Product page URI pattern. - 'view-product' => sprintf( __( 'Visit %s*', 'plausible-analytics' ), $uri ), - 'add-to-cart' => __( 'Woo Add to Cart', 'plausible-analytics' ), - 'remove-from-cart' => __( 'Woo Remove from Cart', 'plausible-analytics' ), - 'checkout' => __( 'Woo Start Checkout', 'plausible-analytics' ), - 'purchase' => __( 'Woo Complete Purchase', 'plausible-analytics' ), + 'view-product' => sprintf( 'Visit %s*', $uri ), + 'add-to-cart' => 'Woo Add to Cart', + 'remove-from-cart' => 'Woo Remove from Cart', + 'checkout' => 'Woo Start Checkout', + 'purchase' => 'Woo Complete Purchase', ]; $this->init( $init ); diff --git a/tests/integration/Admin/ProvisioningTest.php b/tests/integration/Admin/ProvisioningTest.php index 667b739d..ca9ef06d 100644 --- a/tests/integration/Admin/ProvisioningTest.php +++ b/tests/integration/Admin/ProvisioningTest.php @@ -16,6 +16,33 @@ use function Brain\Monkey\Functions\when; class ProvisioningTest extends TestCase { + /** + * The goal names must match the names the events are sent under, so a translation must never change them. + * + * @see https://github.com/plausible/wordpress/issues/326 + * @return void + * @throws \ReflectionException + */ + public function testCustomEventGoalsAreNotTranslated() { + $translate = function ( $translation, $text, $domain ) { + return $domain === 'plausible-analytics' ? "Traduit : $text" : $translation; + }; + + add_filter( 'gettext', $translate, 10, 3 ); + + try { + $goals = new \ReflectionProperty( Provisioning::class, 'custom_event_goals' ); + $goals->setAccessible( true ); + + $this->assertEquals( + [ '404', 'Cloaked Link: Click', 'WP Form Completions', 'WP Query Parameters', 'WP Search Queries' ], + array_values( $goals->getValue( new Provisioning( false ) ) ) + ); + } finally { + remove_filter( 'gettext', $translate ); + } + } + /** * @see Provisioning::create_goal_request() * @return void diff --git a/tests/integration/HelpersTest.php b/tests/integration/HelpersTest.php index 10de9191..002711e4 100644 --- a/tests/integration/HelpersTest.php +++ b/tests/integration/HelpersTest.php @@ -462,13 +462,15 @@ public function testGetActiveLanguagesTranslatePressSkipsUnpublishedLanguages() /** * A language's pinned WCML default currency should be used; anything else ("Keep", stored as false, 0 or '0') - * should fall back to the store's base currency. + * should fall back to the store's base currency setting, not the (filtered) currency of the current request. * * @see Helpers::get_currency_for_language() * @return void */ public function testGetCurrencyForLanguage() { - when( 'get_woocommerce_currency' )->justReturn( 'USD' ); + // Multicurrency plugins filter this to the currency of the current request. + when( 'get_woocommerce_currency' )->justReturn( 'JPY' ); + update_option( 'woocommerce_currency', 'USD' ); $plugin = function () { return Helpers::MULTILANG_PLUGIN_WPML; @@ -511,6 +513,7 @@ public function testGetCurrencyForLanguage() { remove_filter( 'plausible_analytics_multilang_plugin', $plugin ); remove_filter( 'wpml_default_language', $default ); delete_option( '_wcml_settings' ); + delete_option( 'woocommerce_currency' ); } } diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php index 00604fda..85a736f9 100644 --- a/tests/integration/Integrations/WooCommerceTest.php +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -12,6 +12,37 @@ #[AllowDynamicProperties] class WooCommerceTest extends TestCase { + /** + * The event names identify the goals in Plausible, so a translation must never change them. + * + * @see https://github.com/plausible/wordpress/issues/326 + * @return void + */ + public function testEventGoalsAreNotTranslated() { + when( 'wc_get_permalink_structure' )->justReturn( [ 'product_base' => 'product' ] ); + + $translate = function ( $translation, $text, $domain ) { + return $domain === 'plausible-analytics' ? "Traduit : $text" : $translation; + }; + + add_filter( 'gettext', $translate, 10, 3 ); + + try { + $this->assertEquals( + [ + 'view-product' => 'Visit /product*', + 'add-to-cart' => 'Woo Add to Cart', + 'remove-from-cart' => 'Woo Remove from Cart', + 'checkout' => 'Woo Start Checkout', + 'purchase' => 'Woo Complete Purchase', + ], + ( new WooCommerce( false ) )->event_goals + ); + } finally { + remove_filter( 'gettext', $translate ); + } + } + /** * @see WooCommerce::track_entered_checkout() * @return void