Skip to content
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

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions includes/lib/glotpress-helper.php
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: [email protected])
*
* 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 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;
}
39 changes: 39 additions & 0 deletions includes/localization.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<?php
function pmpro_load_textdomain() {
$locale = apply_filters( 'plugin_locale', get_user_locale(), 'paid-memberships-pro' );
Expand Down Expand Up @@ -56,3 +57,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;
$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.
\Required\Traduttore_Registry\add_project(
'plugin',
$plugin_slug,
'https://translate.strangerstudios.com/api/translations/' . $plugin_slug
);
}

}
add_action( 'admin_init', 'pmpro_check_for_translations' );
1 change: 1 addition & 0 deletions paid-memberships-pro.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
require_once( PMPRO_DIR . '/includes/sessions.php' ); // start/close PHP session vars

require_once( PMPRO_DIR . '/includes/localization.php' ); // localization functions
require_once( PMPRO_DIR . '/includes/lib/glotpress-helper.php' ); // handles translation updates logic from our own server.
require_once( PMPRO_DIR . '/includes/lib/name-parser.php' ); // parses "Jason Coleman" into firstname=>Jason, lastname=>Coleman
require_once( PMPRO_DIR . '/includes/functions.php' ); // misc functions used by the plugin
require_once( PMPRO_DIR . '/includes/updates.php' ); // database and other updates
Expand Down