-
Notifications
You must be signed in to change notification settings - Fork 366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom translation updates (GlotPress) #3211
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
<?php | ||
/** | ||
* Main Traduttore Registry library. | ||
* | ||
* @since 1.0.0 | ||
* | ||
* Copyright (c) 2018-2020 required (email: info@required.com) | ||
* | ||
* This program is free software; you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License, version 2 or, at | ||
* your discretion, any later version, as published by the Free | ||
* Software Foundation. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program; if not, write to the Free Software | ||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | ||
*/ | ||
|
||
namespace PMPro\Required\Traduttore_Registry; | ||
|
||
use DateTime; | ||
|
||
const TRANSIENT_KEY_PLUGIN = 'traduttore-registry-plugins'; | ||
const TRANSIENT_KEY_THEME = 'traduttore-registry-themes'; | ||
|
||
/** | ||
* Adds a new project to load translations for. | ||
* | ||
* @since 1.0.0 | ||
* | ||
* @param string $type Project type. Either plugin or theme. | ||
* @param string $slug Project directory slug. | ||
* @param string $api_url Full GlotPress API URL for the project. | ||
*/ | ||
function add_project( $type, $slug, $api_url ) { | ||
if ( ! has_action( 'init', __NAMESPACE__ . '\register_clean_translations_cache' ) ) { | ||
add_action( 'init', __NAMESPACE__ . '\register_clean_translations_cache', 9999 ); | ||
} | ||
|
||
/** | ||
* Short-circuits translations API requests for private projects. | ||
*/ | ||
add_filter( | ||
'translations_api', | ||
static function ( $result, $requested_type, $args ) use ( $type, $slug, $api_url ) { | ||
if ( $type . 's' === $requested_type && $slug === $args['slug'] ) { | ||
return get_translations( $type, $args['slug'], $api_url ); | ||
} | ||
|
||
return $result; | ||
}, | ||
10, | ||
3 | ||
); | ||
|
||
/** | ||
* Filters the translations transients to include the private plugin or theme. | ||
* | ||
* @see wp_get_translation_updates() | ||
*/ | ||
add_filter( | ||
'site_transient_update_' . $type . 's', | ||
static function ( $value ) use ( $type, $slug, $api_url ) { | ||
if ( ! $value ) { | ||
$value = new \stdClass(); | ||
} | ||
|
||
if ( ! isset( $value->translations ) ) { | ||
$value->translations = []; | ||
} | ||
|
||
$translations = get_translations( $type, $slug, $api_url ); | ||
|
||
if ( ! isset( $translations['translations'] ) ) { | ||
return $value; | ||
} | ||
|
||
$installed_translations = get_installed_translations( $type ); | ||
$locales = get_available_locales(); | ||
|
||
/** This filter is documented in wp-includes/update.php */ | ||
$locales = apply_filters( $type . 's_update_check_locales', $locales ); | ||
$active_locales = array_unique( $locales ); | ||
|
||
foreach ( (array) $translations['translations'] as $translation ) { | ||
if ( ! \in_array( $translation['language'], $active_locales, true ) ) { | ||
continue; | ||
} | ||
|
||
if ( $translation['updated'] && isset( $installed_translations[ $slug ][ $translation['language'] ] ) ) { | ||
$local = sanitize_date( $installed_translations[ $slug ][ $translation['language'] ]['PO-Revision-Date'] ); | ||
$remote = new DateTime( $translation['updated'] ); | ||
|
||
if ( $local >= $remote ) { | ||
continue; | ||
} | ||
} | ||
|
||
$translation['type'] = $type; | ||
$translation['slug'] = $slug; | ||
|
||
$value->translations[] = $translation; | ||
} | ||
|
||
return $value; | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Registers actions for clearing translation caches. | ||
* | ||
* @since 1.1.0 | ||
*/ | ||
function register_clean_translations_cache() { | ||
$clear_plugin_translations = static function() { | ||
clean_translations_cache( 'plugin' ); | ||
}; | ||
$clear_theme_translations = static function() { | ||
clean_translations_cache( 'theme' ); | ||
}; | ||
|
||
add_action( 'set_site_transient_update_plugins', $clear_plugin_translations ); | ||
add_action( 'delete_site_transient_update_plugins', $clear_plugin_translations ); | ||
|
||
add_action( 'set_site_transient_update_themes', $clear_theme_translations ); | ||
add_action( 'delete_site_transient_update_themes', $clear_theme_translations ); | ||
} | ||
|
||
/** | ||
* Clears existing translation cache for a given type. | ||
* | ||
* @since 1.1.0 | ||
* | ||
* @param string $type Project type. Either plugin or theme. | ||
*/ | ||
function clean_translations_cache( $type ) { | ||
$transient_key = constant( __NAMESPACE__ . '\TRANSIENT_KEY_' . strtoupper( $type ) ); | ||
$translations = get_site_transient( $transient_key ); | ||
|
||
if ( ! \is_object( $translations ) ) { | ||
return; | ||
} | ||
|
||
/* | ||
* Don't delete the cache if the transient gets changed multiple times | ||
* during a single request. Set cache lifetime to maximum 15 seconds. | ||
*/ | ||
$cache_lifespan = 15; | ||
$time_not_changed = isset( $translations->_last_checked ) && ( time() - $translations->_last_checked ) > $cache_lifespan; | ||
|
||
if ( ! $time_not_changed ) { | ||
return; | ||
} | ||
|
||
delete_site_transient( $transient_key ); | ||
} | ||
|
||
/** | ||
* Gets the translations for a given project. | ||
* | ||
* @since 1.0.0 | ||
* | ||
* @param string $type Project type. Either plugin or theme. | ||
* @param string $slug Project directory slug. | ||
* @param string $url Full GlotPress API URL for the project. | ||
* @return array Translation data. | ||
*/ | ||
function get_translations( $type, $slug, $url ) { | ||
$transient_key = constant( __NAMESPACE__ . '\TRANSIENT_KEY_' . strtoupper( $type ) ); | ||
$translations = get_site_transient( $transient_key ); | ||
|
||
if ( ! \is_object( $translations ) ) { | ||
$translations = new \stdClass(); | ||
} | ||
|
||
|
||
if ( isset( $translations->{$slug} ) && \is_array( $translations->{$slug} ) ) { | ||
return $translations->{$slug}; | ||
} | ||
|
||
$result = json_decode( wp_remote_retrieve_body( wp_remote_get( $url, [ 'timeout' => 2 ] ) ), true ); | ||
|
||
if ( ! \is_array( $result ) ) { | ||
// Cache an empty result in case of a failure | ||
// and retry on next update check. | ||
$result = []; | ||
} | ||
|
||
$translations->{$slug} = $result; | ||
$translations->_last_checked = time(); | ||
|
||
set_site_transient( $transient_key, $translations ); | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Sanitizes a date string. | ||
* | ||
* DateTime fails to parse date strings that contain brackets, such as | ||
* “Tue Dec 22 2015 12:52:19 GMT+0100 (West-Europa)”, which appears in | ||
* PO-Revision-Date headers. Sanitization ensures such date headers are | ||
* parsed correctly into DateTime instances. | ||
* | ||
* @since 2.1.0 | ||
* | ||
* @param string $date_string The date string to sanitize. | ||
* @return \DateTime Date from string if parsable, otherwise the Unix epoch. | ||
*/ | ||
function sanitize_date( $date_string ) { | ||
$date_and_timezone = explode( '(', $date_string ); | ||
$date_no_timezone = trim( $date_and_timezone[0] ); | ||
|
||
try { | ||
$date = new DateTime( $date_no_timezone ); | ||
} catch ( \Exception $e ) { | ||
return new DateTime( '1970-01-01' ); | ||
} | ||
|
||
return $date; | ||
} | ||
|
||
/** | ||
* Gets the installed translations. | ||
* | ||
* Results are cached. | ||
* | ||
* @since 2.2.0 | ||
* | ||
* @see wp_get_installed_translations() | ||
* | ||
* @param string $type Project type. Either plugin or theme. | ||
* @return array Translation data. | ||
*/ | ||
function get_installed_translations( $type ) { | ||
static $cache = []; | ||
|
||
if ( ! isset( $cache[ $type ] ) ) { | ||
$cache[ $type ] = wp_get_installed_translations( $type . 's' ); | ||
} | ||
|
||
return $cache[ $type ]; | ||
} | ||
|
||
/** | ||
* Gets all available and installed locales. | ||
* | ||
* Results are cached. | ||
* | ||
* @since 2.2.0 | ||
* | ||
* @see get_available_languages() | ||
* | ||
* @return array List of installed locales. | ||
*/ | ||
function get_available_locales() { | ||
static $cache = null; | ||
|
||
if ( ! isset( $cache ) ) { | ||
$cache = array_values( get_available_languages() ); | ||
} | ||
|
||
return $cache; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,3 +56,41 @@ function pmpro_translate_billing_period($period, $number = 1) | |
return __("Years", 'paid-memberships-pro' ); | ||
} | ||
} | ||
|
||
/** | ||
* Handle translation updates from our own translation server. | ||
* @since TBD | ||
*/ | ||
function pmpro_check_for_translations() { | ||
// Run it only on a PMPro page in the admin. | ||
if ( ! current_user_can( 'update_plugins' ) ) { | ||
return; | ||
} | ||
|
||
$is_pmpro_admin = ! empty( $_REQUEST['page'] ) && strpos( $_REQUEST['page'], 'pmpro' ) !== false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do a similar check here: https://github.com/strangerstudios/paid-memberships-pro/blob/dev/includes/admin.php#L327-L331 Maybe we should have a function in includes/admin.php or adminpages/functions.php for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of having an admin function that checks is a pmpro setting page. We're starting to do this more. |
||
$is_update_or_plugins_page = strpos( $_SERVER['REQUEST_URI'], 'update-core.php' ) !== false || strpos( $_SERVER['REQUEST_URI'], 'plugins.php' ) !== false; | ||
|
||
// Only run this check when we're in the PMPro Page or plugins/update page to save some resources. | ||
if ( ! $is_pmpro_admin && ! $is_update_or_plugins_page ) { | ||
return; | ||
} | ||
|
||
$pmpro_add_ons = pmpro_getAddOns(); | ||
foreach( $pmpro_add_ons as $add_on ) { | ||
// Skip if the plugin isn't active. | ||
if ( ! pmpro_is_plugin_active( $add_on['plugin'] ) ) { | ||
continue; | ||
} | ||
|
||
$plugin_slug = $add_on['Slug']; | ||
|
||
// This uses the Traduttore plugin to check for translations for locales etc. | ||
PMPro\Required\Traduttore_Registry\add_project( | ||
'plugin', | ||
$plugin_slug, | ||
'https://translate.strangerstudios.com/api/translations/' . $plugin_slug | ||
); | ||
} | ||
|
||
} | ||
add_action( 'admin_init', 'pmpro_check_for_translations' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we document how the caching works? Maybe WP core is doing it's own caching too.
How often will the site hit our servers? Ideally we don't want the admin pages to be hitting our servers every time they load a plugins page or our settings pages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can get way more aggressive with the cache. I'd be okay to bump it up by a day or two (we have a way to manually download the translation as well or they could delete the transient to ping the server when needed).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spent some time looking into this. TLDR: our
clean_translations_cache()
here that clears caches older than 15 seconds should only be run twice daily (by default) on the coreset_site_transient_update_plugins
hook or whenever plugins are manually rechecked or a plugin is updated triggering thedelete_site_transient_update_plugins
hook.So in a sense, we do have some built-in caching from WP. It looks like the 15 second delay is really just there to prevent updating translations for a plugin multiple times on a single page load