<?php
/**
* Class TranslationsLoader
*
* @package WooCommerce\Payments
*/
namespace WCPay\Internal\PluginManagement;
use DateTime;
use WCPay\Internal\Logger;
use WCPay\Internal\Proxy\HooksProxy;
/**
* A class for managing WordPress translations loading for WooPayments.
*
* Unlike for other WordPress.org plugins, WooPayments translations are loaded from https://translate.wordpress.com.
*
* @codeCoverageIgnore -- The most of the code interacts with global WordPress methods to wrangle translations. Not that much to test here, so excluding it from test coverage.
*/
class TranslationsLoader {
/**
* Logger instance.
*
* @var Logger
*/
private $logger;
/**
* HooksProxy instance.
*
* @var HooksProxy
*/
private $hooks_proxy;
/**
* Initializes all dependencies.
*
* @param Logger $logger Logger instance.
* @param HooksProxy $hooks_proxy Hooks proxy instance.
*/
public function __construct( Logger $logger, HooksProxy $hooks_proxy ) {
$this->logger = $logger;
$this->hooks_proxy = $hooks_proxy;
}
/**
* Hooks into WordPress plugin update process to load plugin translations from translate.wordpress.com.
*/
public function init_hooks() {
$this->hooks_proxy->add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'load_wcpay_translations' ] );
}
/**
* Hooks into auto-update process to load plugin translations from translate.wordpress.com.
*
* Runs in a cron thread, or in a visitor thread if triggered
* by _maybe_update_plugins(), or in an auto-update thread.
*
* @template T
* @param T $transient The update_plugins transient object.
*
* @return T The same or a modified version of the transient.
*/
public function load_wcpay_translations( $transient ) {
try {
if ( is_object( $transient ) ) {
$translations = $this->get_translations_update_data();
$merged_translations = array_merge( isset( $transient->translations ) ? $transient->translations : [], $translations );
$transient->translations = $merged_translations;
}
} catch ( \Exception $ex ) {
$this->logger->error( 'Error with loading WooPayments translations from WordPress.com. Reason: ' . $ex->getMessage() );
return $transient;
}
return $transient;
}
/**
* Get translations updates information.
*
* @return array Update data {product_id => data}
* @throws \Exception If something goes wrong with fetching info about translation packages from WordPress.com.
*/
public function get_translations_update_data() {
$installed_translations = wp_get_installed_translations( 'plugins' );
$locales = array_values( get_available_languages() );
if ( empty( $locales ) ) {
return [];
}
// Use the same timeout values as Woo Core https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/includes/admin/helper/class-wc-helper-updater.php#L257.
$timeout = wp_doing_cron() ? 30 : 3;
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugin_data = get_plugin_data( WCPAY_PLUGIN_FILE );
/**
* Note: TextDomain could differ from the plugin slug, but WordPress uses TextDomain to load translations.
*/
$plugin_name = $plugin_data['TextDomain'];
$request_body = [
'locales' => $locales,
'plugins' => [],
];
$request_body['plugins'][ $plugin_name ] = [
'version' => WCPAY_VERSION_NUMBER,
];
$raw_response = wp_remote_post(
'https://translate.wordpress.com/api/translations-updates/woocommerce',
[
'body' => wp_json_encode( $request_body ),
'headers' => [ 'Content-Type: application/json' ],
'timeout' => $timeout,
]
);
$response_code = wp_remote_retrieve_response_code( $raw_response );
if ( 200 !== $response_code ) {
$this->logger->debug(
sprintf( 'Raw response: %s', var_export( $raw_response, true ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- That's debug message which will only be logged when debuging is enabled.
);
throw new \Exception(
sprintf( 'Request failed. HTTP response code: %s', $response_code )
);
}
$response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
if ( array_key_exists( 'success', $response ) && false === $response['success'] ) {
// The shape of response is not known, so more specific error message can't be provided in exception. Logging the response body for debuggin purposes.
$this->logger->debug(
sprintf( 'Unexpected response body: %s', var_export( $response, true ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export -- That's debug message which will only be logged when debuging is enabled.
);
throw new \Exception( 'Unexpected response body.' );
}
$language_packs = $response['data'][ $plugin_name ];
$translations = [];
foreach ( $language_packs as $language_pack ) {
// Maybe we have this language pack already installed so lets check revision date.
if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) {
$installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] );
$new_translation_revision_time = new DateTime( $language_pack['last_modified'] );
// Skip if translation language pack is not newer than what is installed already.
if ( $new_translation_revision_time <= $installed_translation_revision_time ) {
continue;
}
}
$translations[] = [
'type' => 'plugin',
'slug' => $plugin_name,
'language' => $language_pack['wp_locale'],
'version' => $language_pack['version'],
'updated' => $language_pack['last_modified'],
'package' => $language_pack['package'],
'autoupdate' => true,
];
}
return $translations;
}
}