wenpai.org/wp-content/plugins/plat-gp-customizations/inc/cli/class-stats.php
2024-11-14 02:29:46 +08:00

1754 lines
61 KiB
PHP

<?php
/**
* This WP-CLI command has to be in the sandbox in the
* /home/wporg/public_html/wp-content/plugins/wporg-gp-customizations/inc/cli folder
*
* To execute this command, you need to use this text in the CLI:
*
* wp wporg-translate show-stats --url=translate.wenpai.org
*/
namespace WordPressdotorg\GlotPress\Customizations\CLI;
use DateTime;
use GP_Locale;
use WP_CLI;
use WP_Query;
use WordPressdotorg\GlotPress\Routes\Plugin;
use function WordPressdotorg\Locales\get_locales;
use function WordPressdotorg\I18nTeams\Locales\get_locales_data;
class Stats {
/**
* Number of years backward from which you want to obtain statistics.
* First translation was added to the database at 2010-02-17 18:09:13.
*
* @var int
*/
private int $number_of_years = 3;
/**
* Number of translators with more translations each year to get feedback from.
*
* @var int
*/
private int $number_of_translators = 100;
/**
* First user at DotOrg registered the current year.
*
* @var int
*/
private int $id_first_user_of_this_year = 0;
/**
* Percentage of non-EN_US sites.
*/
private string $translated_sites_pct = '';
/**
* Id of each forum database, for each country
*
* @var int[]
*/
private array $forum_ids = array(
'pt' => 383,
'make' => 384,
// '' => 385,
'emoji' => 386,
'hau' => 387,
'fao' => 388,
'af' => 389,
'am' => 390,
'id' => 391,
'mya' => 392,
'ja' => 393,
'ru' => 394,
'de' => 395,
'bs' => 396,
'th' => 397,
'cn' => 398,
'ak' => 399,
'an' => 400,
'ar' => 401,
'arq' => 402,
'ug' => 403,
'tw' => 404,
'bg' => 405,
'sr' => 406,
'hr' => 407,
'nl' => 408,
'he' => 409,
'ka' => 410,
'fi' => 411,
'mk' => 412,
'ca' => 413,
'sk' => 414,
'pl' => 415,
'ro' => 416,
'es' => 417,
'br' => 418,
'en' => 419,
'sq' => 420,
'hy' => 421,
'rup' => 422,
'frp' => 423,
'as' => 424,
'ast' => 425,
'az' => 426,
'az-tr' => 427,
'bcc' => 428,
'eu' => 429,
'bel' => 430,
'bn' => 431,
'bre' => 432,
'ceb' => 433,
'zh-hk' => 434,
'co' => 435,
'cs' => 436,
'et' => 437,
'eo' => 438,
'uz' => 439,
);
private string $header = '';
private string $originals_by_year = '';
private string $originals_by_translation_source = '';
private string $translations_translators_by_year = '';
private string $forum_post_and_replies_by_year = '';
private string $wordpress_translation_percentage = '';
private string $packages_generated_by_year = '';
private string $themes_plugins_by_year = '';
private string $feedback_received = '';
private string $contributors_per_locale = '';
private string $managers_stats = '';
private string $most_active_translators = '';
private string $stats_comparison = '';
/**
* The name of the custom post type used to the translation feedback.
*/
private const FEEDBACK_POST_TYPE = 'gth_original';
/**
* Whether the class should print the info in the CLI or not.
*
* @var bool
*/
private bool $echo_the_values = false;
/**
* The id of the blog where the stats are stored.
*
* @var int
*/
private const MAKE_POLYGLOTS_BLOG_ID = 19;
/**
* The id of the page where the stats are stored.
*
* @var int
*/
private const POLYGLOTS_PAGE_ID = 42132;
/**
* Prints the Polyglots stats or stores them on a page.
*
* @param bool $echo_the_values Whether it should print the info in the CLI or stores it on a page.
*
* @return void
*/
public function __invoke( bool $echo_the_values = false ) {
global $wpdb;
// This value is only set in the production site (translate.wenpai.org).
// This check avoids to fire the cron job from another sites.
if ( ! isset( $wpdb->project_translation_status ) ) {
return;
}
$this->echo_the_values = $echo_the_values;
//$this->set_number_of_years_with_data();
//$this->print_header();
//$this->print_wordpress_translation_percentage();
//$this->print_stats_for_translation_sources();
//$this->print_packages_generated();
//$this->print_unique_themes_plugins_by_year();
//$this->print_originals_natural_year();
//$this->print_total_translations_translators_by_year();
//$this->print_forum_by_locale_and_year();
//$this->print_feedback_received();
//$this->print_contributors_per_locale();
//$this->print_managers_stats();
//$this->print_most_active_translators();
//$this->store_stats();
//$this->print_stats_comparison( gmdate( 'Y-m-d' ) );
//$this->update_page();
}
/**
* Store the generated stats in the database
*
* @return void
*/
private function store_stats() {
global $wpdb;
$this->translated_sites_pct = $this->get_translated_sites_pct();
$contributors_per_locale = Plugin::get_contributors_count();
$total_contributors_count = array_sum( $contributors_per_locale );
$all_locales_data = get_locales_data();
$stats_data = $all_locales_data['status_counts'];
$total_gtes = array_sum( $this->count_managers( 'general_translation_editor' ) );
$total_ptes = array_sum( $this->count_managers( 'translation_editor' ) );
$locale_requests = $this->get_locale_requests();
$editor_requests = $this->get_editor_requests();
$result = $wpdb->insert(
'polyglot_stats',
array(
'releases_by_locale' => $stats_data['all'],
'releases_by_locale_uptodate' => $stats_data['latest'],
'releases_by_locale_minor_behind' => $stats_data['minor-behind'],
'releases_by_locale_one_major_behind' => $stats_data['major-behind-one'],
'releases_by_locale_multi_major_behind' => $stats_data['major-behind-many'],
'locales_total' => $this->get_core_total(),
'locales_100' => $this->get_core_full_translated(),
'locales_95_plus' => $this->get_core_interval( 100, 95 ),
'locales_90_plus' => $this->get_core_interval( 95, 90 ),
'locales_50_plus' => $this->get_core_interval( 90, 50 ),
'locales_below_50' => $this->get_core_interval( 50, 0, '<', '>' ),
'locales_with_language_packs' => $stats_data['has-language-pack'],
'locales_without_project' => $stats_data['no-wp-project'],
'requests_total' => $editor_requests['total'],
'requests_unresolved' => $editor_requests['unresolved_editor_requests'],
'locale_requests_total' => $locale_requests['total'],
'locale_requests_unresolved' => $locale_requests['unresolved_locale_requests'],
'translators_gtes' => $total_gtes,
'translators_ptes' => $total_ptes,
'translators_contributors' => $total_contributors_count,
'wp_translated_sites_pct' => $this->translated_sites_pct,
'date' => gmdate( 'Y-m-d' ),
)
);
}
/**
* Fetch total number of locale requests and number of unresolved locale requests.
*
* @return array $locale_requests Array of the count of each of resolved and unresolved locale requests.
*/
private function get_locale_requests() {
$locale_requests = array();
switch_to_blog( self::MAKE_POLYGLOTS_BLOG_ID );
$args = array(
'post_type' => 'post',
'tag' => 'locale-requests',
'post_status' => 'publish',
);
$locale_requests['total'] = ( new WP_query( $args ) )->found_posts;
register_taxonomy(
'p2_resolved',
'post',
array(
'public' => true,
'query_var' => 'resolved',
'rewrite' => false,
'show_ui' => false,
)
);
$args = array(
'tag' => 'locale-requests',
'tax_query' => array(
array(
'taxonomy' => 'p2_resolved',
'field' => 'slug',
'terms' => 'unresolved',
),
),
);
$locale_requests['unresolved_locale_requests'] = ( new WP_query( $args ) )->found_posts;
restore_current_blog();
return $locale_requests;
}
/**
* Fetch total number of editor requests and number of unresolved editor requests.
*
* @return array $editor_requests Array of the count of each of resolved and unresolved editor requests.
*/
private function get_editor_requests() {
$editor_requests = array();
switch_to_blog( self::MAKE_POLYGLOTS_BLOG_ID );
register_taxonomy(
'p2_resolved',
'post',
array(
'public' => true,
'query_var' => 'resolved',
'rewrite' => false,
'show_ui' => false,
)
);
$args = array(
'post_type' => 'post',
'tag' => 'editor-requests',
'post_status' => 'publish',
);
$editor_requests['total'] = ( new WP_query( $args ) )->found_posts;
$args = array(
'post_type' => 'post',
'tag' => 'editor-requests',
'tax_query' => array(
array(
'taxonomy' => 'p2_resolved',
'field' => 'slug',
'terms' => 'unresolved',
),
),
);
$editor_requests['unresolved_editor_requests'] = ( new WP_query( $args ) )->found_posts;
restore_current_blog();
return $editor_requests;
}
/**
* Fetch stats data for a specific date.
*
* @param string $date The date for the record to be retrieved.
*
* @return mixed
*/
private function get_data_for_date( $date ) {
global $wpdb;
$sql = "SELECT * FROM polyglot_stats WHERE date = '" . $date . "'";
$results = $wpdb->get_results( $sql );
return $results[0];
}
/**
* Get the percentage of non-EN_US sites.
*
* @return float
*/
private function get_translated_sites_pct() {
$translation_stats_json = file_get_contents( 'https://api.wordpress.org/stats/locale/1.0/' );
$ranslation_stats_array = $translation_stats_json && '{' == $translation_stats_json[0] ? json_decode( $translation_stats_json, true ) : null;
$wp_translated_sites_pct = 100 - $ranslation_stats_array['English (US)'];
return $wp_translated_sites_pct;
}
/**
* Print stats compared week on week.
*
* @param string $current_date The date for which we display the stats.
* @param string $old_date The date to compare the stats with.
*
* @return void
*/
private function print_stats_comparison( $current_date, $old_date = null ) {
if ( ! $this->is_date_valid( $current_date ) || ( $old_date && ! $this->is_date_valid( $old_date ) ) ) {
return;
}
$old_date = is_null( $old_date ) ? date( 'Y-m-d', strtotime( '-1 week' ) ) : $old_date;
$current_date_data = $this->get_data_for_date( $current_date );
$old_date_data = $this->get_data_for_date( $old_date );
if ( ! $current_date_data || ! $old_date_data ) {
return;
}
if ( ! $this->echo_the_values ) {
$this->stats_comparison = $this->create_gutenberg_heading( 'Summary for weekly stats' );
} else {
$this->print_wpcli_heading( 'Summary for weekly stats' );
}
$stats_diff = new \stdClass();
foreach ( $current_date_data as $key => $value ) {
$stats_diff->$key = $value - $old_date_data->$key;
}
$all_locales_data = get_locales_data();
$stats_data = $all_locales_data['status_counts'];
$code = 'Below stats are dated ' . $current_date . ' compared to ' . $old_date . ' (differences between brackets)' . PHP_EOL . PHP_EOL;
$code .= 'Releases: ' . $current_date_data->releases_by_locale . ' (' . $this->prefix_num( $stats_diff->releases_by_locale ) . ') locale, ' . $current_date_data->releases_by_locale_uptodate . ' (' . $this->prefix_num( $stats_diff->releases_by_locale_uptodate ) . ') up to date, ' . $current_date_data->releases_by_locale_minor_behind . ' (' . $this->prefix_num( $stats_diff->releases_by_locale_minor_behind ) . ') behind by minor versions, ' . $current_date_data->releases_by_locale_one_major_behind . ' (' . $this->prefix_num( $stats_diff->releases_by_locale_one_major_behind ) . ') behind by one major version, ' . $current_date_data->releases_by_locale_multi_major_behind . ' (' . $this->prefix_num( $stats_diff->releases_by_locale_multi_major_behind ) . ') behind more than one major version, ' . $stats_data['no-site'] . ' (N/A) have site but never released, ' . $stats_data['no-releases'] . ' (N/A) have no site.' . PHP_EOL . PHP_EOL;
$code .= 'Translations: ' . $current_date_data->locales_total . ' (' . $this->prefix_num( $stats_diff->locales_total ) . ') total, ' . $current_date_data->locales_100 . ' (' . $this->prefix_num( $stats_diff->locales_100 ) . ') at 100%, ' . $current_date_data->locales_95_plus . ' (' . $this->prefix_num( $stats_diff->locales_95_plus ) . ') over 95%, ' . $current_date_data->locales_90_plus . ' (' . $this->prefix_num( $stats_diff->locales_90_plus ) . ') over 90%, ' . $current_date_data->locales_50_plus . ' (' . $this->prefix_num( $stats_diff->locales_50_plus ) . ') over 50%, ' . $current_date_data->locales_below_50 . ' (' . $this->prefix_num( $stats_diff->locales_below_50 ) . ') below 50%, ' . $current_date_data->locales_with_language_packs . ' (' . $this->prefix_num( $stats_diff->locales_with_language_packs ) . ') have a language pack generated, ' . $current_date_data->locales_without_project . ' (' . $this->prefix_num( $stats_diff->locales_without_project ) . ') have no project.' . PHP_EOL . PHP_EOL;
$code .= 'Requests: There are ' . $current_date_data->requests_unresolved . ' unresolved editor requests out of ' . $current_date_data->requests_total . ' (' . $this->prefix_num( $stats_diff->requests_unresolved ) . ') total and ' . $current_date_data->locale_requests_unresolved . ' unresolved locale requests out of ' . $current_date_data->locale_requests_total . ' (' . $this->prefix_num( $stats_diff->locale_requests_unresolved ) . ') total.' . PHP_EOL . PHP_EOL;
$code .= 'Translators: There are ' . $current_date_data->translators_gtes . ' (' . $this->prefix_num( $stats_diff->translators_gtes ) . ') GTEs, ' . $current_date_data->translators_ptes . ' (' . $this->prefix_num( $stats_diff->translators_ptes ) . ') PTEs and ' . $current_date_data->translators_contributors . ' (' . $this->prefix_num( $stats_diff->translators_contributors ) . ') translation contributors.' . PHP_EOL;
$code .= '(A wordpress.org account could have multiple roles over different locale)' . PHP_EOL . PHP_EOL;
$code .= 'Site language: ' . $current_date_data->wp_translated_sites_pct . '% (' . $this->prefix_num( round( $stats_diff->wp_translated_sites_pct, 3 ) ) . '%) of WordPress sites are running a translated WordPress site. ' . PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->stats_comparison .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Checks if date is valid
*
* @param string $date The date that needs to be validated.
*
* @return boolean True if date is valid and False if invalid
*/
private function is_date_valid( $date, $format = 'Y-m-d' ) {
$_date = DateTime::createFromFormat( $format, $date );
return $_date && $_date->format( $format ) === $date;
}
/**
* Prefix numbers greater than zero with plus sign and plus_minus sign if number is zero.
*
* @param int $num Number to be prefixed.
*
* @return string Prefixed number
*/
private function prefix_num( $num ) {
if ( 0 === $num ) {
return '±0';
}
return $num > 0 ? sprintf( '+%d', $num ) : $num;
}
/**
* Set the number of years between 2010 (first year with translations) and the current year.
*
* @return void
*/
private function set_number_of_years_with_data(): void {
$this->number_of_years = gmdate( 'Y' ) - 2010 + 1;
}
/**
* Print the main header.
*
* @return void
*/
private function print_header(): void {
if ( ! $this->echo_the_values ) {
$this->header = $this->create_gutenberg_paragraph( 'Polyglots stats. Created at ' . gmdate( 'Y-m-d H:i:s' ) . ' ' . date_default_timezone_get() );
$this->header .= $this->create_gutenberg_paragraph( 'Created using the <b>wp wporg-translate show-stats --url=translate.wenpai.org</b> command.' );
} else {
$this->print_wpcli_heading( 'Polyglots stats. Created at ' . gmdate( 'Y-m-d H:i:s' ) . ' ' . date_default_timezone_get() );
}
}
/**
* Print the number of original strings grouped by year.
*
* @return void
*/
private function print_originals_natural_year(): void {
global $wpdb;
$originals = $wpdb->get_results(
"SELECT
YEAR( date_added ) as year,
count(*) as strings
FROM {$wpdb->gp_originals}
GROUP BY YEAR( date_added )
ORDER BY YEAR( date_added ) DESC"
);
if ( ! $this->echo_the_values ) {
$this->originals_by_year = $this->create_gutenberg_heading( 'Number of originals in the last ' . $this->number_of_years . ' years.' );
} else {
$this->print_wpcli_heading( 'Number of originals in the last ' . $this->number_of_years . ' years.' );
}
$code = "Year \t\t Number of strings" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
foreach ( $originals as $original ) {
if ( gmdate( 'Y' ) == $original->year ) {
$originals_estimated = $this->estimate_value_for_full_year( $original->strings );
$code .= $original->year . " (*) \t " . number_format_i18n( $originals_estimated ) . PHP_EOL;
}
$code .= $original->year . " \t\t " . number_format_i18n( $original->strings ) . PHP_EOL;
}
$code .= '................................................................' . PHP_EOL;
$code .= '(*) Estimated for the current year.' . PHP_EOL;
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->originals_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the number of locales, with and without variants, and the % of core translation in each locale.
*
* @return void
*/
private function print_wordpress_translation_percentage(): void {
global $wpdb;
$core_total = $this->get_core_total();
$core_100 = $this->get_core_full_translated();
$core_95_100 = $this->get_core_interval( 100, 95 );
$core_90_95 = $this->get_core_interval( 95, 90 );
$core_50_90 = $this->get_core_interval( 90, 50 );
$core_50 = $this->get_core_interval( 50, 0, '<', '>' );
$core_0 = $this->get_core_empty_translated();
if ( ! $this->echo_the_values ) {
$this->wordpress_translation_percentage = $this->create_gutenberg_heading( 'WordPress core: Translated percentage by the locale.' );
} else {
$this->print_wpcli_heading( 'WordPress core: Translated percentage by the locale.' );
}
$code = '................................................................' . PHP_EOL;
$code .= "Number of locales: \t" . number_format_i18n( count( $this->get_existing_locales() ) ) . PHP_EOL;
$code .= "Number of locales with variants: \t" . number_format_i18n( count( $this->get_existing_locales( 'with_variants' ) ) ) . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
$code .= 'Info from the historical stats.' . PHP_EOL;
$code .= "Number of WordPress (wp/dev) to translate: \t" . $core_total . PHP_EOL;
$code .= "100% WordPress translated: \t" . $core_100 . PHP_EOL;
$code .= "95-100% WordPress translated: \t" . $core_95_100 . PHP_EOL;
$code .= "90-95% WordPress translated: \t" . $core_90_95 . PHP_EOL;
$code .= "50-90% WordPress translated: \t" . $core_50_90 . PHP_EOL;
$code .= "0-50% WordPress translated: \t" . $core_50 . PHP_EOL;
$code .= "0% WordPress translated: \t" . $core_0 . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
$code .= 'The difference between the number of locales and the number of ' . PHP_EOL;
$code .= "WordPress (wp/dev) is due to some duplicated variants. \n" . PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->wordpress_translation_percentage .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the packages generated each year.
*
* @return void
*/
private function print_packages_generated() {
global $wpdb;
$packages = $wpdb->get_results(
"SELECT
LEFT( updated, 4 ) as year,
SUM( CASE WHEN type = 'core' THEN 1 ELSE 0 END ) as core_packs,
SUM( CASE WHEN type = 'plugin' THEN 1 ELSE 0 END ) as plugin_packs,
SUM( CASE WHEN type = 'theme' THEN 1 ELSE 0 END ) as theme_packs,
count(*) as total_packs
FROM language_packs
WHERE updated >= '2010-01-01'
GROUP BY LEFT( updated, 4 )
ORDER BY LEFT( updated, 4 ) DESC",
ARRAY_A
);
$header = 'Language Packs generated per year.';
if ( ! $this->echo_the_values ) {
$this->packages_generated_by_year = $this->create_gutenberg_heading( $header );
} else {
$this->print_wpcli_heading( $header );
}
$code = "Year \t Core Packs \t Plugin Packs \t Theme Packs \t Total Packs " . PHP_EOL;
$code .= '.......................................................................' . PHP_EOL;
foreach ( $packages as $package ) {
if ( gmdate( 'Y' ) == $package['year'] ) {
$code .= str_pad( $package['year'] . ' (*) ', 10 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['core_packs'] ) ), 16 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['plugin_packs'] ) ), 16 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['theme_packs'] ) ), 16 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['total_packs'] ) ), 16 ) .
PHP_EOL;
}
$code .= str_pad( $package['year'], 10 ) .
str_pad( number_format_i18n( $package['core_packs'] ), 16 ) .
str_pad( number_format_i18n( $package['plugin_packs'] ), 16 ) .
str_pad( number_format_i18n( $package['theme_packs'] ), 16 ) .
str_pad( number_format_i18n( $package['total_packs'] ), 16 ) .
PHP_EOL;
}
$code .= '.......................................................................' . PHP_EOL;
$code .= '(*) Estimated for the current year.' . PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->packages_generated_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the Unique Plugins / Themes language packs per year.
*
* @return void
*/
private function print_unique_themes_plugins_by_year() {
global $wpdb;
$packages = $wpdb->get_results(
"SELECT
year,
SUM( CASE WHEN type = 'plugin' THEN 1 ELSE 0 END ) as plugins,
SUM( CASE WHEN type = 'theme' THEN 1 ELSE 0 END ) as themes,
count(*) as total
FROM (
SELECT
domain,
type,
LEFT( updated, 4 ) as year
FROM language_packs
WHERE updated >= '2010-01-01' AND type IN( 'plugin', 'theme' )
GROUP BY domain, type, LEFT( updated, 4 )
ORDER BY type, LEFT( updated, 4 )+0 ASC
)a
GROUP BY year
ORDER BY year DESC",
ARRAY_A
);
$header = 'Unique Plugins / Themes language packs per year.';
if ( ! $this->echo_the_values ) {
$this->themes_plugins_by_year = $this->create_gutenberg_heading( $header );
} else {
$this->print_wpcli_heading( $header );
}
$code = "Year \t Plugins \t Themes \t Total" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
foreach ( $packages as $package ) {
if ( gmdate( 'Y' ) == $package['year'] ) {
$code .= str_pad( $package['year'] . ' (*) ', 10 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['plugins'] ) ), 16 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['themes'] ) ), 16 ) .
str_pad( number_format_i18n( $this->estimate_value_for_full_year( $package['total'] ) ), 16 ) .
PHP_EOL;
}
$code .= str_pad( $package['year'], 10 ) .
str_pad( number_format_i18n( $package['plugins'] ), 16 ) .
str_pad( number_format_i18n( $package['themes'] ), 16 ) .
str_pad( number_format_i18n( $package['total'] ), 16 ) .
PHP_EOL;
}
$code .= '................................................................' . PHP_EOL;
$code .= '(*) Estimated for the current year.' . PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->themes_plugins_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the number of translations and translators in the last years.
*
* We don't use a query like this one because we get MySQL timeout:
* $translators = $wpdb->get_var(
* $wpdb->prepare(
* "SELECT COUNT( DISTINCT user_id) as translators
* FROM gp_translations
* WHERE date_modified >= '%s' and date_modified <= '%s'",
* '2022-01-01 00:00:00',
* '2022-12-31 23:59:59'
* ) );
*
* We get the first and the last translation id for each year and
* then count the number of different translators.
*
* @return void
*/
private function print_total_translations_translators_by_year() {
global $wpdb;
if ( ! $this->echo_the_values ) {
$this->translations_translators_by_year = $this->create_gutenberg_heading( 'Number of translations and translators in the last ' . $this->number_of_years . ' years.' );
} else {
$this->print_wpcli_heading( 'Number of translations and translators in the last ' . $this->number_of_years . ' years.' );
}
$code = "Year \t\t Number of translations \t Translators \t Translators with > 1 translation" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
$last_year = gmdate( 'Y' );
$first_year = $last_year - $this->number_of_years;
$first_id = 0;
for ( $year = $last_year; $year > $first_year; $year -- ) {
if ( gmdate( 'Y' ) == $year ) {
$last_id = $wpdb->get_var( "SELECT MAX(id) FROM {$wpdb->gp_translations}" );
} else {
$last_id = $first_id - 1;
}
$first_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT MIN(id) FROM {$wpdb->gp_translations} WHERE date_added BETWEEN %s AND %s",
$year . '-01-01 00:00:00',
$year . '-01-02 00:00:00',
)
);
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT
COUNT(*) as strings_added,
COUNT( DISTINCT user_id ) as contributors
FROM {$wpdb->gp_translations}
WHERE id BETWEEN %d AND %d",
$first_id,
$last_id,
),
ARRAY_A
);
$repeat_contributors_val = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(user_id) FROM (
SELECT DISTINCT user_id, count(id) as translations_number
FROM {$wpdb->gp_translations}
WHERE id BETWEEN %d AND %d
GROUP BY user_id
HAVING translations_number > 1
) a",
$first_id,
$last_id,
)
);
if ( gmdate( 'Y' ) == $year ) {
$strings_added = str_pad( number_format_i18n( $this->estimate_value_for_full_year( $row['strings_added'] ) ), 10, ' ', STR_PAD_LEFT );
$contributors = str_pad( number_format_i18n( $this->estimate_value_for_full_year( $row['contributors'] ) ), 6, ' ', STR_PAD_LEFT );
$repeat_contributors = str_pad( number_format_i18n( $this->estimate_value_for_full_year( $repeat_contributors_val ) ), 8, ' ', STR_PAD_LEFT );
$code .= "{$year} (*) \t {$strings_added} \t\t\t {$contributors} \t {$repeat_contributors}" . PHP_EOL;
}
$strings_added = number_format_i18n( $row['strings_added'] );
$contributors = number_format_i18n( $row['contributors'] );
$strings_added = str_pad( $strings_added, 10, ' ', STR_PAD_LEFT );
$contributors = str_pad( $contributors, 6, ' ', STR_PAD_LEFT );
$repeat_contributors = str_pad( number_format_i18n( $repeat_contributors_val ), 8, ' ', STR_PAD_LEFT );
$code .= "{$year} \t\t {$strings_added} \t\t\t {$contributors} \t {$repeat_contributors}" . PHP_EOL;
}
$code .= '................................................................' . PHP_EOL;
$code .= '(*) Estimated for the current year.' . PHP_EOL;
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->translations_translators_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
private function print_forum_by_locale_and_year() {
$last_year = gmdate( 'Y' );
$first_year = $last_year - $this->number_of_years;
$forum_posts = array();
$forum_replies = array();
$code = '';
for ( $year = $last_year; $year > $first_year; $year -- ) {
$forum_posts[ $year ] = $this->get_forums_stats( 'topic', $year );
$forum_replies[ $year ] = $this->get_forums_stats( 'reply', $year );
}
ksort( $this->forum_ids );
$header = 'Forums. Topics by year and locale.';
if ( ! $this->echo_the_values ) {
$this->forum_post_and_replies_by_year = $this->create_gutenberg_heading( $header );
} else {
$this->print_wpcli_heading( $header );
}
$code .= str_pad( 'Locale', 12 );
for ( $year = $last_year; $year > $first_year; $year -- ) {
$code .= str_pad( $year, 8, ' ', STR_PAD_LEFT );
}
$code .= PHP_EOL;
foreach ( $this->forum_ids as $key => $value ) {
$code .= str_pad( $key, 12 );
for ( $year = $last_year; $year > $first_year; $year -- ) {
$code .= str_pad( number_format_i18n( $forum_posts[ $year ][ $key ] ), 8, ' ', STR_PAD_LEFT );
}
$code .= PHP_EOL;
}
if ( ! $this->echo_the_values ) {
$this->forum_post_and_replies_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
$header = 'Forums. Replies by year and locale.';
if ( ! $this->echo_the_values ) {
$this->forum_post_and_replies_by_year .= $this->create_gutenberg_heading( $header );
} else {
$this->print_wpcli_heading( $header );
}
$code = str_pad( 'Locale', 12 );
for ( $year = $last_year; $year > $first_year; $year -- ) {
$code .= str_pad( $year, 8, ' ', STR_PAD_LEFT );
}
$code .= PHP_EOL;
foreach ( $this->forum_ids as $key => $value ) {
$code .= str_pad( $key, 12 );
for ( $year = $last_year; $year > $first_year; $year -- ) {
$code .= str_pad( number_format_i18n( $forum_replies[ $year ][ $key ] ), 8, ' ', STR_PAD_LEFT );
}
$code .= PHP_EOL;
}
if ( ! $this->echo_the_values ) {
$this->forum_post_and_replies_by_year .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the most active translators in the last years.
*
* @return void
*/
private function print_most_active_translators(): void {
global $wpdb;
$last_year = gmdate( 'Y' );
$first_year = $last_year - $this->number_of_years;
$first_id = 0;
if ( ! $this->echo_the_values ) {
$this->most_active_translators = $this->create_gutenberg_heading( 'Most active translators in the last ' . $this->number_of_years . ' years.' );
} else {
$this->print_wpcli_heading( 'Most active translators in the last ' . $this->number_of_years . ' years.' );
}
$code = "Year \t Translations \t Translator" . PHP_EOL;
for ( $year = $last_year; $year > $first_year; $year -- ) {
if ( gmdate( 'Y' ) == $year ) {
$last_id = $wpdb->get_var( "SELECT MAX(id) FROM {$wpdb->gp_translations}" );
} else {
$last_id = $first_id - 1;
}
$first_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT MIN(id) FROM {$wpdb->gp_translations} WHERE date_added >= %s",
$year . '-01-01'
)
);
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT
user_id,
COUNT(*) as strings_added
FROM {$wpdb->gp_translations}
WHERE id BETWEEN %d AND %d
GROUP BY user_id
ORDER BY `strings_added` DESC
LIMIT %d",
$first_id,
$last_id,
$this->number_of_translators
),
ARRAY_A
);
$code .= '................................................................' . PHP_EOL;
foreach ( $rows as $row ) {
if ( 0 == $row['user_id'] ) {
continue;
}
$strings_added = number_format_i18n( $row['strings_added'] );
$contributor = get_user_by( 'id', $row['user_id'] );
if ( $contributor ) {
$code .= "{$year} \t {$strings_added} \t {$contributor->user_login}" . PHP_EOL;
}
}
}
if ( ! $this->echo_the_values ) {
$this->most_active_translators .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Estimate the value for the end of the year, using a direct rule of 3.
*
* @param int $current_value The value used to calculate the estimation at the end of the year.
*
* @return int The estimated value.
*/
private function estimate_value_for_full_year( int $current_value ): int {
$current_day_of_year = gmdate( 'z' ) + 1;
$days_in_current_year = date_diff( new DateTime( 'last day of december' ), new DateTime( 'first day of january' ) )->days + 1;
return round( $current_value * $days_in_current_year / $current_day_of_year );
}
/**
*
* We release the feedback functionality on July 28, 2022
* in the Polyglots Coffee Break
* https://make.wordpress.org/polyglots/2022/06/28/polyglots-coffee-break-july-28-2022-at-2200-utc/
*
* @return void
*/
private function print_feedback_received() {
global $wpdb;
$original_strings_with_comments = 0;
$total_comments = 0;
$optin_users = 0;
$comment_meta_translation_ids = array();
$comment_user_ids = array();
$commenters_number = 0;
$commenters = array();
$status_counter = array(
'changesrequested' => 0,
'current' => 0,
'current_from_rejection' => 0,
'fuzzy' => 0,
'rejected' => 0,
'old' => 0,
'unknown' => 0,
'waiting' => 0,
);
// Get the number of opt-in users.
$optin_users = number_format_i18n(
$wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(user_id) as optin_users
FROM {$wpdb->usermeta}
WHERE meta_key = %s AND meta_value LIKE %s",
$wpdb->base_prefix . WPORG_TRANSLATE_BLOGID . '_gp_default_sort',
'%s:19:\"notifications_optin\";s:2:\"on\";%'
)
)
);
// Get the number of original strings with comments: one CPT for each original.
$feedback_posts_args = array(
'posts_per_page' => - 1,
'post_status' => 'publish',
'post_type' => self::FEEDBACK_POST_TYPE,
'date_query' => array(
array( 'after' => '2022-07-28' ),
),
);
$feedback_posts_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->posts WHERE post_status='publish' AND post_type=%s AND post_date > %s",
self::FEEDBACK_POST_TYPE,
'2022-07-28'
)
);
$original_strings_with_comments = number_format_i18n( $feedback_posts_count );
// Get the total number of comments.
$total_comments = number_format_i18n(
get_comments(
array(
'number' => - 1,
'post_type' => self::FEEDBACK_POST_TYPE,
'count' => true,
'date_query' => array(
array( 'after' => '2022-07-28' ),
),
)
)
);
// Get some info related with the status of the translations who get feedback.
// First, get the comments related with a translation, because we can get comments related
// only with the original.
$comment_meta_translation_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT cm.meta_value FROM $wpdb->commentmeta cm, $wpdb->comments c, $wpdb->posts p WHERE p.post_status='publish' AND p.post_type=%s AND p.post_date > %s AND cm.meta_key = 'translation_id' AND c.comment_post_id = p.id AND cm.comment_id = c.comment_id",
self::FEEDBACK_POST_TYPE,
'2022-07-28'
)
);
// Check all comments with a related translation.
foreach ( $comment_meta_translation_ids as $comment_meta_translation_id ) {
$translation = $wpdb->get_row(
$wpdb->prepare(
"SELECT s.locale, t.id, t.original_id, t.user_id, t.status
FROM {$wpdb->gp_translations} t
INNER JOIN {$wpdb->gp_translation_sets} s ON t.translation_set_id = s.id
WHERE t.id = %s",
$comment_meta_translation_id
)
);
if ( ! $translation ) {
continue;
}
// If this translation was rejected, I look for a current translation for the same original (original_id),
// translator (user_id) and language (locale).
if ( 'rejected' == $translation->status ) {
$is_current_from_rejection = $wpdb->get_row(
$wpdb->prepare(
"SELECT t.id, s.locale
FROM {$wpdb->gp_translations} t
INNER JOIN {$wpdb->gp_translation_sets} s ON t.translation_set_id = s.id
WHERE
t.status='current'
AND t.original_id = %d
AND t.user_id = %d
AND s.locale = %s",
$translation->original_id,
$translation->user_id,
$translation->locale
)
);
if ( $is_current_from_rejection ) {
$status_counter['current_from_rejection'] ++;
}
}
$status_counter[ $translation->status ?: 'unknown' ] ++;
}
// Get most active commenter's.
$comment_user_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT c.user_id FROM $wpdb->comments c, $wpdb->posts p WHERE p.post_status='publish' AND p.post_type=%s AND p.post_date > %s AND c.comment_post_id = p.id",
self::FEEDBACK_POST_TYPE,
'2022-07-28'
)
);
$commenters_with_comment_count = array_count_values( $comment_user_ids );
arsort( $commenters_with_comment_count );
$commenters_number = number_format_i18n( count( $commenters_with_comment_count ) );
foreach ( $commenters_with_comment_count as $user_id => $comment_number ) {
$user = get_user_by( 'id', $user_id );
$user->comments_number = $comment_number;
$commenters[] = $user;
}
// Format and print the info.
$comment_meta_translation_id_number = number_format_i18n( count( $comment_meta_translation_ids ) );
$rejected_number = number_format_i18n( $status_counter['rejected'] );
$current_from_rejection_number = number_format_i18n( $status_counter['current_from_rejection'] );
$current_number = number_format_i18n( $status_counter['current'] );
$fuzzy_number = number_format_i18n( $status_counter['fuzzy'] );
$old_number = number_format_i18n( $status_counter['old'] );
if ( ! $this->echo_the_values ) {
$this->feedback_received = $this->create_gutenberg_heading( 'Feedback in the last ' . $this->number_of_years . ' years (starting on 2022-07-28).' );
} else {
$this->print_wpcli_heading( 'Feedback in the last ' . $this->number_of_years . ' years (starting on 2022-07-28).' );
}
$code = "Opt-in users: \t\t\t\t\t {$optin_users}" . PHP_EOL;
$code .= "Original strings with comments: \t\t {$original_strings_with_comments}" . PHP_EOL;
$code .= "Comments: \t\t\t\t\t {$total_comments}" . PHP_EOL;
$code .= "Different translations with comments: \t\t {$comment_meta_translation_id_number}" . PHP_EOL;
$code .= " - Rejected translations: \t\t\t {$rejected_number}" . PHP_EOL;
$code .= " - Approved translations from a rejection: \t {$current_from_rejection_number}" . PHP_EOL;
$code .= " - Approved translations (not rejected): \t {$current_number}" . PHP_EOL;
$code .= " - Fuzzy translations (not rejected): \t\t {$fuzzy_number}" . PHP_EOL;
$code .= " - Old translations: \t\t\t\t {$old_number}" . PHP_EOL;
$code .= "Number of different commenters: \t\t {$commenters_number}" . PHP_EOL;
foreach ( $commenters as $commenter ) {
if ( strlen( $commenter->user_login ) > 10 ) {
$tabs = "\t";
} else {
$tabs = "\t\t";
}
$url = 'https://profiles.wordpress.org/' . sanitize_title_with_dashes( $commenter->user_login );
$comments_number = number_format_i18n( $commenter->comments_number );
$code .= " - {$commenter->user_login}: {$tabs} {$comments_number} comments. Profile: {$url}" . PHP_EOL;
}
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->feedback_received .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the total number of LM, GTE and PTE and grouped by locale.
*
* @return void
*/
private function print_managers_stats() {
$this->id_first_user_of_this_year = $this->get_id_first_user_of_this_year();
$locales = get_locales();
$locale_managers = $this->count_managers( 'locale_manager', 'total' );
$registered_this_year_new_locale_managers = $this->count_managers( 'locale_manager', 'registered_this_year' );
$started_this_year_new_locale_managers = $this->count_managers( 'locale_manager', 'started_this_year' );
$general_translation_editors = $this->count_managers( 'general_translation_editor' );
$registered_this_year_new_general_translation_editors = $this->count_managers( 'general_translation_editor', 'registered_this_year' );
$started_this_year_new_general_translation_editors = $this->count_managers( 'general_translation_editor', 'started_this_year' );
$project_translation_editors = $this->count_managers( 'translation_editor' );
$registered_this_year_new_project_translation_editors = $this->count_managers( 'translation_editor', 'registered_this_year' );
$started_this_year_new_translation_editor = $this->count_managers( 'translation_editor', 'started_this_year' );
if ( ! $this->echo_the_values ) {
$this->managers_stats = $this->create_gutenberg_heading( 'Managers stats.' );
} else {
$this->print_wpcli_heading( 'Managers stats.' );
}
$code = 'Local managers (LM):' . PHP_EOL;
$code .= " - Total:\t\t\t\t\t\t\t\t" . number_format_i18n( array_sum( $locale_managers ) ) . PHP_EOL;
$code .= " - Total users that have been registered this year and get the role:\t" . number_format_i18n( array_sum( $registered_this_year_new_locale_managers ) ) . PHP_EOL;
$code .= " - Total users that have start translating this year and get the role:\t" . number_format_i18n( array_sum( $started_this_year_new_locale_managers ) ) . PHP_EOL;
foreach ( $locales as $locale ) {
if ( array_key_exists( $locale->english_name, $locale_managers ) ) {
$code .= "\t - " . $locale->english_name . ': ' . number_format_i18n( $locale_managers[ $locale->english_name ] ) . ', ' . number_format_i18n( $registered_this_year_new_locale_managers[ $locale->english_name ] ) . ', ' . number_format_i18n( $started_this_year_new_locale_managers[ $locale->english_name ] ) . PHP_EOL;
}
}
$code .= 'General Translator Editors (GTE):' . PHP_EOL;
$code .= " - Total:\t\t\t\t\t\t\t\t" . number_format_i18n( array_sum( $general_translation_editors ) ) . PHP_EOL;
$code .= " - Total users that have been registered this year and get the role:\t" . number_format_i18n( array_sum( $registered_this_year_new_general_translation_editors ) ) . PHP_EOL;
$code .= " - Total users that have start translating this year and get the role:\t" . number_format_i18n( array_sum( $started_this_year_new_general_translation_editors ) ) . PHP_EOL;
foreach ( $locales as $locale ) {
if ( array_key_exists( $locale->english_name, $locale_managers ) ) {
$code .= "\t - " . $locale->english_name . ': ' . number_format_i18n( $general_translation_editors[ $locale->english_name ] ) . ', ' . number_format_i18n( $registered_this_year_new_general_translation_editors[ $locale->english_name ] ) . ', ' . number_format_i18n( $started_this_year_new_general_translation_editors[ $locale->english_name ] ) . PHP_EOL;
}
}
$code .= 'Project Translation Editors (PTE):' . PHP_EOL;
$code .= " - Total:\t\t\t\t\t\t\t\t" . number_format_i18n( array_sum( $project_translation_editors ) ) . PHP_EOL;
$code .= " - Total users that have been registered this year and get the role:\t" . number_format_i18n( array_sum( $registered_this_year_new_project_translation_editors ) ) . PHP_EOL;
$code .= " - Total users that have start translating this year and get the role:\t" . number_format_i18n( array_sum( $started_this_year_new_translation_editor ) ) . PHP_EOL;
foreach ( $locales as $locale ) {
if ( array_key_exists( $locale->english_name, $locale_managers ) ) {
$code .= "\t - " . $locale->english_name . ': ' . number_format_i18n( $project_translation_editors[ $locale->english_name ] ) . ', ' . number_format_i18n( $registered_this_year_new_project_translation_editors[ $locale->english_name ] ) . ', ' . number_format_i18n( $started_this_year_new_translation_editor[ $locale->english_name ] ) . PHP_EOL;
}
}
if ( ! $this->echo_the_values ) {
$this->managers_stats .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the number of contributors per locale.
*
* @return void
*/
private function print_contributors_per_locale():void {
$locales = get_locales();
$header = 'Contributors per locale';
if ( ! $this->echo_the_values ) {
$this->contributors_per_locale = $this->create_gutenberg_heading( $header );
} else {
$this->print_wpcli_heading( $header );
}
$code = '.........................................................................................' . PHP_EOL;
$code .= 'Active contributor: 1 translation in the last 365 days.' . PHP_EOL;
$code .= '.........................................................................................' . PHP_EOL;
$code .= "Locale \t\t\t\t Active contributors Past contributors All contributors" . PHP_EOL;
$code .= '.........................................................................................' . PHP_EOL;
foreach ( $locales as $locale ) {
$current_contributors = $this->get_translation_contributors( $locale, 365 );
$all_contributors = $this->get_translation_contributors( $locale );
$code .= str_pad( $locale->english_name, 40 ) .
str_pad( number_format_i18n( count( $current_contributors ) ), 20 ) .
str_pad( number_format_i18n( count( $all_contributors ) - count( $current_contributors ) ), 20 ) .
str_pad( number_format_i18n( count( $all_contributors ) ), 20 ) . PHP_EOL;
}
if ( ! $this->echo_the_values ) {
$this->contributors_per_locale .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Print the stats for translation sources.
*
* @return void
*/
private function print_stats_for_translation_sources(): void {
global $wpdb;
// Source used: frontend, file import, playground
$originals = $wpdb->get_results(
"SELECT
meta_key,
meta_value,
count(*) as number_of_strings
FROM `translate_meta`
WHERE
object_type = 'translation'
AND meta_key = 'source'
AND meta_value <> ''
GROUP BY
meta_key,
meta_value
ORDER BY
`translate_meta`.
`meta_key` ASC,
count(*) desc
"
);
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source = $this->create_gutenberg_heading( 'Number of translations by translation source (starting on 2023-06-30)' );
} else {
$this->print_wpcli_heading( 'Number of translations by translation source (starting on 2023-06-30)' );
}
$code = "Source \t\t\t\t Number of strings" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
foreach ( $originals as $original ) {
$code .= str_pad( ucfirst( $original->meta_value ), 15 ) . " \t\t " . str_pad( number_format_i18n( $original->number_of_strings ), 15, ' ', STR_PAD_LEFT ) . PHP_EOL;
}
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
// Suggestion used: TM, OpenAI, DeepL, undefined.
$suggestions = $wpdb->get_results(
"SELECT
meta_key,
meta_value,
count(*) as number_of_strings
FROM `translate_meta`
WHERE
object_type = 'translation'
AND meta_key = 'suggestion_used'
AND
(meta_value LIKE 'tm%'
OR meta_value LIKE 'openai%'
OR meta_value LIKE 'deepl%')
GROUP BY
meta_key,
meta_value
ORDER BY
`translate_meta`.
`meta_key` ASC,
count(*) desc
"
);
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source .= $this->create_gutenberg_heading( 'Number of translations by suggestion source (starting on 2023-06-30)' );
} else {
$this->print_wpcli_heading( 'Number of translations by suggestion source (starting on 2023-06-30)' );
}
$code = "Source \t\t\t\t Number of strings" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
foreach ( $suggestions as $suggestion ) {
$code .= str_pad( $suggestion->meta_value, 15 ) . " \t\t " . str_pad( number_format_i18n( $suggestion->number_of_strings ), 15, ' ', STR_PAD_LEFT ) . PHP_EOL;
}
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
// Suggestion from other languages.
$suggestions_ol = $wpdb->get_results(
"SELECT
meta_key,
meta_value,
count(*) as number_of_strings
FROM `translate_meta`
WHERE
object_type = 'translation'
AND meta_key = 'suggestion_used'
AND
NOT (meta_value LIKE 'tm%'
OR meta_value LIKE 'undefined%'
OR meta_value LIKE 'openai%'
OR meta_value LIKE 'deepl%')
GROUP BY
meta_key,
meta_value
ORDER BY
`translate_meta`.
`meta_key` ASC,
count(*) desc
"
);
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source .= $this->create_gutenberg_heading( 'Number of translations suggested from another language (starting on 2023-06-30)' );
} else {
$this->print_wpcli_heading( 'Number of translations suggested from another language (starting on 2023-06-30)' );
}
$code = "Language \t\t\t\t Number of strings" . PHP_EOL;
$code .= '................................................................' . PHP_EOL;
foreach ( $suggestions_ol as $suggestion ) {
$code .= str_pad( $suggestion->meta_value, 15 ) . " \t\t " . str_pad( number_format_i18n( $suggestion->number_of_strings ), 15, ' ', STR_PAD_LEFT ) . PHP_EOL;
}
$code .= PHP_EOL;
if ( ! $this->echo_the_values ) {
$this->originals_by_translation_source .= $this->create_gutenberg_code( $code );
} else {
WP_CLI::log( $code );
}
}
/**
* Get the locales at DotOrg.
*
* @param string $locale_slug Locale slug.
* - 'default' for the default locales without variants.
* - 'with_variants' for the default locales with variants.
*
* @return array Number of locales
*/
private function get_existing_locales( string $locale_slug = 'default' ): array {
global $wpdb;
$query = '';
if ( 'with_variants' == $locale_slug ) {
$query = $wpdb->prepare(
"SELECT locale FROM {$wpdb->gp_translation_sets} WHERE `project_id` = %d",
396517 // 396517 = core/dev/body
);
} elseif ( 'default' == $locale_slug ) {
$query = $wpdb->prepare(
"SELECT locale FROM {$wpdb->gp_translation_sets} WHERE `project_id` = %d and slug = %s",
396517, // 396517 = core/dev/body
'default'
);
} else {
return array();
}
return $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Returns the number of locales with WordPress core translation project generated.
*
* @return mixed
*/
private function get_core_total() {
global $wpdb;
return $wpdb->get_var(
"SELECT count(*) as counter FROM (
SELECT
(100 * stats.current/stats.all) as percent_complete
FROM {$wpdb->project_translation_status} stats
LEFT JOIN {$wpdb->gp_projects} projects ON stats.project_id = projects.id
WHERE
projects.path = 'wp/dev'
AND projects.active = 1
) n"
);
}
/**
* Returns the number of locales with WordPress full translated
*
* @return mixed
*/
private function get_core_full_translated() {
global $wpdb;
return $wpdb->get_var(
"SELECT count(*) as counter FROM (
SELECT
(100 * stats.current/stats.all) as percent_complete
FROM {$wpdb->project_translation_status} stats
LEFT JOIN {$wpdb->gp_projects} projects ON stats.project_id = projects.id
WHERE
projects.path = 'wp/dev'
AND projects.active = 1
HAVING
percent_complete >= 100.00
) n"
);
}
/**
* Returns the number of locales with WordPress core translation between the two values.
*
* @param int $upper_value
* @param int $lower_value
* @param string $minor_symbol
* @param string $greater_symbol
*
* @return mixed
*/
private function get_core_interval( int $upper_value, int $lower_value, string $minor_symbol = '<', string $greater_symbol = '>=' ) {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT count(*) as counter FROM (
SELECT
(100 * stats.current/stats.all) as percent_complete
FROM {$wpdb->project_translation_status} stats
LEFT JOIN {$wpdb->gp_projects} projects ON stats.project_id = projects.id
WHERE
projects.path = 'wp/dev'
AND projects.active = 1
HAVING
percent_complete %1\$s %2\$d
AND percent_complete %3\$s %4\$d
) n",
$greater_symbol,
$lower_value,
$minor_symbol,
$upper_value
)
);
}
/**
* Returns the number of locales with WordPress empty translated.
*
* @return mixed
*/
private function get_core_empty_translated() {
global $wpdb;
return $wpdb->get_var(
"SELECT count(*) as counter FROM (
SELECT
(100 * stats.current/stats.all) as percent_complete
FROM {$wpdb->project_translation_status} stats
LEFT JOIN {$wpdb->gp_projects} projects ON stats.project_id = projects.id
WHERE
projects.path = 'wp/dev'
AND projects.active = 1
HAVING
percent_complete <= 0.00
) n"
);
}
/**
* Returns the first user registered in the current year.
*
* @return int
*/
private function get_id_first_user_of_this_year(): int {
global $wpdb;
return $wpdb->get_var(
$wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE user_registered >= %s LIMIT 1",
gmdate( 'Y-m-d H:i:s', strtotime( 'first day of january this year' ) )
)
);
}
/**
* Gets the managers for the $role category for each locale.
*
* The role can be:
* - locale_manager.
* - general_translation_editor.
* - translation_editor.
*
* The $type parameter can be:
* - total: get all the contributors.
* - registered_this_year: get the users that have been registered this year and get the $role.
* - started_this_year: get the users that have start translating this year and get the $role.
*
* @param string $role Translator role.
* @param string $type Filter.
*
* @return array
*/
private function count_managers( string $role, string $type = 'total' ): array {
global $wpdb;
$locales = get_locales();
$managers = array();
foreach ( $locales as $locale ) {
$result = get_sites(
array(
'locale' => $locale->wp_locale,
'network_id' => WPORG_GLOBAL_NETWORK_ID,
'path' => '/',
'fields' => 'ids',
'number' => '1',
)
);
$site_id = array_shift( $result );
if ( ! $site_id ) {
continue;
}
$query = new \WP_User_Query();
$query->prepare_query(
array(
'blog_id' => $site_id,
'role' => $role,
)
);
if ( 'registered_this_year' === $type ) {
// Get the users that have registered this year.
$query->query_where .= ' AND user_id >= ' . $this->id_first_user_of_this_year;
}
if ( 'started_this_year' === $type ) {
// Get the users that have start translating this year.
$user_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT `user_id`
FROM `{$wpdb->user_translations_count}`
WHERE `locale` = %s AND
`accepted` > 0 AND
`date_added` > %s",
$locale->slug,
gmdate( 'Y-01-01' )
)
);
$query->query_where .= ' AND user_id IN (' . implode( ', ', array_map( 'intval', $user_ids ) ) . ')';
}
$query->query();
$users = $query->get_results();
$managers[ $locale->english_name ] = count( $users );
}
return $managers;
}
/**
* Gets the translation contributors for the given locale.
*
* @return array
*/
private function get_translation_contributors( GP_Locale $locale, $max_age_days = null ): array {
global $wpdb;
$contributors = array();
$date_constraint = '';
if ( null !== $max_age_days ) {
$date_constraint = $wpdb->prepare( ' AND date_modified >= CURRENT_DATE - INTERVAL %d DAY', $max_age_days );
}
[ $locale, $locale_slug ] = array_merge( explode( '/', $locale->slug ), array( 'default' ) );
$users = $wpdb->get_col(
$wpdb->prepare(
"SELECT DISTINCT user_id
FROM {$wpdb->user_translations_count}
WHERE accepted > 0 AND
locale = %s AND
locale_slug = %s",
$locale,
$locale_slug
) . $date_constraint
);
if ( ! $users ) {
return $contributors;
}
$user_data = $wpdb->get_results( "SELECT user_nicename, display_name, user_email FROM $wpdb->users WHERE ID IN (" . implode( ',', $users ) . ')' );
foreach ( $user_data as $user ) {
if ( $user->display_name && $user->display_name !== $user->user_nicename ) {
$contributors[ $user->user_nicename ] = array(
'display_name' => $user->display_name,
'nice_name' => $user->user_nicename,
);
} else {
$contributors[ $user->user_nicename ] = array(
'display_name' => $user->user_nicename,
'nice_name' => $user->user_nicename,
);
}
}
uasort( $contributors, fn( $a, $b ) => strnatcasecmp( $a['display_name'], $b['display_name'] ) );
return $contributors;
}
/**
* Type:
* - topic
* - reply
*
* @param string $type Type of query: topic or reply.
* @param int|null $year Year fot the stats.
* @return array
*/
private function get_forums_stats( string $type, int $year = null ): array {
global $wpdb;
$date_constraint = '';
if ( $year ) {
$date_constraint = $wpdb->prepare( ' AND YEAR(post_date) = %d', $year );
}
foreach ( $this->forum_ids as $key => $value ) {
global $wpdb;
$posts[ $key ] = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(id) as posts FROM {$wpdb->base_prefix}{$value}_posts WHERE post_status = 'publish' AND post_type = %s",
$type
) . $date_constraint
);
}
return $posts;
}
/**
* Update the page with the Polyglots stats.
*
* All the previous information is deleted.
* The page updated is https://make.wordpress.org/polyglots/stats.
*
* @return void
*/
private function update_page() {
if ( $this->echo_the_values ) {
return;
}
add_filter(
'wp_revisions_to_keep',
function ( $num, $post ) {
if ( self::POLYGLOTS_PAGE_ID === $post->ID ) {
$num = 0; // pretend we don't want to keep revisions so that it will not lookup all old revisions.
}
return $num;
},
10,
2
);
switch_to_blog( self::MAKE_POLYGLOTS_BLOG_ID );
wp_update_post(
array(
'ID' => self::POLYGLOTS_PAGE_ID,
'post_type' => 'page',
'post_author' => 'Amieiro',
'post_content' => $this->get_polyglots_stats_page_content(),
),
true
);
restore_current_blog();
}
/**
* Return the content for the web page, concatenating some strings.
*
* @return string
*/
private function get_polyglots_stats_page_content(): string {
return $this->header .
$this->stats_comparison .
$this->wordpress_translation_percentage .
$this->originals_by_translation_source .
$this->originals_by_year .
$this->packages_generated_by_year .
$this->themes_plugins_by_year .
$this->translations_translators_by_year .
$this->forum_post_and_replies_by_year .
$this->feedback_received .
$this->contributors_per_locale .
$this->managers_stats .
$this->most_active_translators;
}
/**
* Create a Gutenberg heading.
*
* @param string $text_to_insert
* @param string $header_type
*
* @return string
*/
private function create_gutenberg_heading( string $text_to_insert, string $header_type = 'h1' ): string {
$heading = '<!-- wp:heading -->';
$heading .= '<' . $header_type . '>' . $text_to_insert . '</' . $header_type . '>';
$heading .= '<!-- /wp:heading -->';
return $heading;
}
/**
* Print a WP-CLI heading.
*
* @param string $text_to_print
*
* @return void
*/
private function print_wpcli_heading( string $text_to_print ): void {
WP_CLI::log( '' );
WP_CLI::log( '----------------------------------------------------------------' );
WP_CLI::log( $text_to_print );
WP_CLI::log( '----------------------------------------------------------------' );
}
/**
* Create a Gutenberg paragraph.
*
* @param string $text_to_insert
*
* @return string
*/
private function create_gutenberg_paragraph( string $text_to_insert ): string {
$paragraph = '<!-- wp:paragraph -->';
$paragraph .= '<p>' . $text_to_insert . '<p>';
$paragraph .= '<!-- /wp:paragraph -->';
return $paragraph;
}
/**
* Create a Gutenberg paragraph.
*
* @param string $text_to_insert
*
* @return string
*/
private function create_gutenberg_code( string $text_to_insert ): string {
$code = '<!-- wp:code -->';
$code .= '<pre class="wp-block-code"><code>';
$code .= '<p>' . $text_to_insert . '<p>';
$code .= '</code></pre>';
$code .= '<!-- /wp:code -->';
return $code;
}
}