Changeset 10628
- Timestamp:
- 01/24/2021 02:30:29 PM (4 years ago)
- Location:
- sites/trunk/wordpress.org/public_html/wp-content/plugins/wp-i18n-teams
- Files:
-
- 3 added
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordpress.org/public_html/wp-content/plugins/wp-i18n-teams/wp-i18n-teams.php
r10318 r10628 2 2 /* 3 3 Plugin Name: WP I18N Teams 4 Description: Provides shortcodes for displaying details abouttranslation teams.4 Description: Provides shortcodes and blocks for managing translation teams. 5 5 Version: 1.0 6 6 License: GPLv2 or later … … 10 10 */ 11 11 12 class WP_I18n_Teams { 13 const TEAM_PAGE = 'https://make.wordpress.org/polyglots/teams/?locale=%s'; 12 namespace WordPressdotorg\I18nTeams; 14 13 15 public function __construct() { 16 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ) ); 17 } 14 const PLUGIN_FILE = __FILE__; 15 const PLUGIN_DIR = __DIR__; 18 16 19 /** 20 * Attaches hooks and registers shortcodes once plugins are loasded. 21 */ 22 public function plugins_loaded() { 23 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), 20 ); 24 add_shortcode( 'wp-locales', array( $this, 'wp_locales' ) ); 17 require_once PLUGIN_DIR . '/inc/namespace.php'; 18 require_once PLUGIN_DIR . '/inc/locales.php'; 25 19 26 add_filter( 'term_link', array( $this, 'link_locales' ), 10, 3 ); 27 } 28 29 /** 30 * Links #locale to the teams page. 31 * 32 * @param string $termlink Term link URL. 33 * @param object $term Term object. 34 * @param string $taxonomy Taxonomy slug. 35 * @return string URL to teams page of a locale. 36 */ 37 public function link_locales( $termlink, $term, $taxonomy ) { 38 if ( 'post_tag' !== $taxonomy ) { 39 return $termlink; 40 } 41 42 static $available_locales; 43 44 if ( ! isset( $available_locales ) ) { 45 $available_locales = self::get_locales(); 46 $available_locales = wp_list_pluck( $available_locales, 'wp_locale' ); 47 $available_locales = array_flip( $available_locales ); 48 } 49 50 if ( isset( $available_locales[ $term->name ] ) || isset( $available_locales[ $term->slug ] ) ) { 51 return sprintf( self::TEAM_PAGE, $term->name ); 52 } 53 54 return $termlink; 55 } 56 57 /** 58 * Enqueue JavaScript and CSS 59 */ 60 public function enqueue_assets() { 61 if ( is_singular() && false !== strpos( get_post()->post_content, '[wp-locales' ) ) { 62 wp_enqueue_style( 'wp-i18n-teams', plugins_url( 'css/i18n-teams.css', __FILE__ ), array(), 13 ); 63 wp_enqueue_script( 'wp-i18n-teams', plugins_url( 'js/i18n-teams.js', __FILE__ ), array( 'jquery', 'o2-app' ), 5 ); 64 } 65 } 66 67 /** 68 * Render the [wp-locales] shortcode. 69 * 70 * @param array $attributes 71 * 72 * @return string 73 */ 74 public function wp_locales( $attributes ) { 75 ob_start(); 76 77 if ( empty( $_GET['locale'] ) ) { 78 $locales = self::get_locales(); 79 $locale_data = $this->get_locales_data(); 80 $percentages = $this->get_core_translation_data(); 81 $language_packs_data = $this->get_language_packs_data(); 82 require( __DIR__ . '/views/all-locales.php' ); 83 } else { 84 require_once GLOTPRESS_LOCALES_PATH; 85 $locale = GP_Locales::by_field( 'wp_locale', $_GET['locale'] ); 86 if ( $locale ) { 87 $locale_data = $this->get_extended_locale_data( $locale ); 88 require( __DIR__ . '/views/locale-details.php' ); 89 } else { 90 printf( 91 '<div class="callout callout-warning"><p>%s</p><p><a href="%s">%s</a></p></div>', 92 sprintf( 93 __( 'Locale %s doesn’t exist.', 'wporg' ), 94 '<code>' . esc_html( $_GET['locale'] ) . '</code>' 95 ), 96 esc_url( get_permalink() ), 97 __( 'Return to All Locales', 'wporg' ) 98 ); 99 } 100 } 101 102 return ob_get_clean(); 103 } 104 105 /** 106 * Get GlotPress locales that have a wp_locale, sorted alphabetically. 107 * 108 * @return array 109 */ 110 protected static function get_locales() { 111 require_once GLOTPRESS_LOCALES_PATH; 112 113 $locales = GP_Locales::locales(); 114 $locales = array_filter( $locales, array( __CLASS__, 'filter_locale_for_wp' ) ); 115 unset( $locales['en'] ); 116 usort( $locales, array( __CLASS__, 'sort_locales' ) ); 117 118 return $locales; 119 } 120 121 /** 122 * Remove locales that are missing a wp_locale. 123 * 124 * This is a callback for array_filter(). 125 * 126 * @param GP_Locale $element 127 * 128 * @return bool 129 */ 130 protected static function filter_locale_for_wp( $element ) { 131 return isset( $element->wp_locale ); 132 } 133 134 /** 135 * Sort GlotPress locales alphabetically by the English name. 136 * 137 * @param GP_Locale $a 138 * @param GP_Locale $b 139 * 140 * @return int 141 */ 142 protected static function sort_locales( $a, $b ) { 143 return strcmp( $a->english_name, $b->english_name ); 144 } 145 146 /** 147 * Gather all the required data and cache it. 148 */ 149 public function get_locales_data() { 150 global $wpdb; 151 152 $cache = get_transient( 'wp_i18n_teams_locales_data' ); 153 if ( false !== $cache ) { 154 return $cache; 155 } 156 157 $gp_locales = self::get_locales(); 158 $translation_data = $this->get_core_translation_data(); 159 $language_packs_data = $this->get_language_packs_data(); 160 $locale_data = array(); 161 162 $statuses = array( 163 'no-wp-project' => 0, 164 'no-site' => 0, 165 'no-releases' => 0, 166 'latest' => 0, 167 'minor-behind' => 0, 168 'major-behind-one' => 0, 169 'major-behind-many' => 0, 170 'translated-100' => 0, 171 'translated-95' => 0, 172 'translated-90' => 0, 173 'translated-50' => 0, 174 'translated-50-less' => 0, 175 'has-language-pack' => 0, 176 'no-language-pack' => 0, 177 ); 178 179 $wporg_data = $wpdb->get_results( 'SELECT locale, subdomain, latest_release FROM wporg_locales ORDER BY locale', OBJECT_K ); 180 181 foreach ( $gp_locales as $locale ) { 182 $subdomain = $latest_release = ''; 183 if ( ! empty( $wporg_data[ $locale->wp_locale ] ) ) { 184 $subdomain = $wporg_data[ $locale->wp_locale ]->subdomain; 185 $latest_release = $wporg_data[ $locale->wp_locale ]->latest_release; 186 } 187 $release_status = self::get_locale_release_status( $subdomain, $latest_release ); 188 $statuses[ $release_status ]++; 189 190 if ( isset( $translation_data[ $locale->wp_locale ] ) ) { 191 $translation_status = self::get_locale_translation_status( $translation_data[ $locale->wp_locale ] ); 192 } else { 193 $translation_status = 'no-wp-project'; 194 } 195 $statuses[ $translation_status ]++; 196 197 if ( isset( $language_packs_data[ $locale->wp_locale ] ) ) { 198 $language_pack_status = 'has-language-pack'; 199 } else { 200 $language_pack_status = 'no-language-pack'; 201 } 202 $statuses[ $language_pack_status ]++; 203 204 $sites = get_sites( [ 205 'locale' => $locale->wp_locale, 206 'network_id' => WPORG_GLOBAL_NETWORK_ID, 207 'orderby' => 'path_length', 208 'number' => '', 209 ] ); 210 211 $locale_data[ $locale->wp_locale ] = array( 212 'release_status' => $release_status, 213 'translation_status' => $translation_status, 214 'language_pack_status' => $language_pack_status, 215 'sites' => $sites, 216 'subdomain' => $subdomain, 217 'rosetta_site_url' => "https://$subdomain.wordpress.org/", 218 'latest_release' => $latest_release ? $latest_release : false, 219 ); 220 } 221 222 $locale_data['status_counts'] = $statuses; 223 $locale_data['status_counts']['all'] = count( $gp_locales ); 224 set_transient( 'wp_i18n_teams_locales_data', $locale_data, 900 ); 225 return $locale_data; 226 } 227 228 public function get_language_packs_data() { 229 global $wpdb; 230 231 $cache = get_transient( 'wp_i18n_teams_language_packs_data' ); 232 if ( false !== $cache ) { 233 return $cache; 234 } 235 236 $language_packs = $wpdb->get_results( "SELECT language AS locale, version FROM `language_packs` WHERE `type` = 'core' AND `active` = 1 AND `version` NOT LIKE '%-%'" ); 237 238 $language_packs_data = array(); 239 foreach ( $language_packs as $pack ) { 240 if ( ! isset( $language_packs_data[ $pack->locale ] ) ) { 241 $language_packs_data[ $pack->locale ] = array(); 242 } 243 244 $language_packs_data[ $pack->locale ][] = $pack->version; 245 } 246 247 set_transient( 'wp_i18n_teams_language_packs_data', $language_packs_data, 900 ); 248 return $language_packs_data; 249 } 250 251 public function get_extended_locale_data( $locale ) { 252 $locales_data = $this->get_locales_data(); 253 $locale_data = $locales_data[ $locale->wp_locale ]; 254 $locale_data['localized_core_url'] = $locale_data['language_pack_url'] = false; 255 256 $latest_release = $locale_data['latest_release']; 257 if ( $latest_release ) { 258 $locale_data['localized_core_url'] = sprintf( 'https://downloads.wordpress.org/release/%s/wordpress-%s.zip', $locale->wp_locale, $latest_release ); 259 $language_packs_data = $this->get_language_packs_data(); 260 261 if ( version_compare( $latest_release, '4.0', '>=' ) && ! empty( $language_packs_data[ $locale->wp_locale ] ) ) { 262 list( $x, $y ) = explode( '.', $latest_release ); 263 $latest_branch = "$x.$y"; 264 265 $pack_version = null; 266 if ( in_array( $latest_release, $language_packs_data[ $locale->wp_locale ] ) ) { 267 $pack_version = $latest_release; 268 } elseif ( in_array( $latest_branch, $language_packs_data[ $locale->wp_locale ] ) ) { 269 $pack_version = $latest_branch; 270 } 271 272 if ( $pack_version ) { 273 $locale_data['language_pack_version'] = $pack_version; 274 $locale_data['language_pack_url'] = sprintf( 'https://downloads.wordpress.org/translation/core/%s/%s.zip', $pack_version, $locale->wp_locale ); 275 } 276 } 277 } 278 279 $contributors = $this->get_contributors( $locale ); 280 $locale_data['locale_managers'] = $contributors['locale_managers']; 281 $locale_data['validators'] = $contributors['validators']; 282 $locale_data['project_validators'] = $contributors['project_validators']; 283 $locale_data['translators'] = $contributors['translators']; 284 $locale_data['translators_past'] = $contributors['translators_past']; 285 286 return $locale_data; 287 } 288 289 /** 290 * Get the translators and validators for the given locale. 291 * 292 * @param GP_Locale $locale 293 * @return array 294 */ 295 public function get_contributors( $locale ) { 296 $cache = wp_cache_get( 'contributors-data:' . $locale->wp_locale, 'wp-i18n-teams' ); 297 if ( false !== $cache ) { 298 return $cache; 299 } 300 301 // Editors are only assigned to the parent locale. 302 $parent_locale = null; 303 if ( isset( $locale->root_slug ) ) { 304 $parent_locale = GP_Locales::by_slug( $locale->root_slug ); 305 } 306 307 $contributors = []; 308 $contributors['locale_managers'] = $this->get_locale_managers( $parent_locale ?? $locale ); 309 $contributors['validators'] = $this->get_general_translation_editors( $parent_locale ?? $locale ); 310 $contributors['project_validators'] = $this->get_project_translation_editors( $parent_locale ?? $locale ); 311 $contributors['translators'] = $this->get_translation_contributors( $locale, 365 ); // Contributors from the past year 312 $contributors['translators_past'] = array_diff_key( $this->get_translation_contributors( $locale ), $contributors['translators'] ); 313 314 wp_cache_set( 'contributors-data:' . $locale->wp_locale, $contributors, 'wp-i18n-teams', 2 * HOUR_IN_SECONDS ); 315 316 return $contributors; 317 } 318 319 public function get_core_translation_data() { 320 $cache = get_transient( 'core_translation_data' ); 321 if ( false !== $cache ) { 322 return $cache; 323 } 324 325 $projects = array( 'wp/dev', 'wp/dev/cc', 'wp/dev/admin', 'wp/dev/admin/network' ); 326 $counts = $percentages = array(); 327 foreach ( $projects as $project ) { 328 $results = json_decode( file_get_contents( 'https://translate.wordpress.org/api/projects/' . $project ) ); 329 foreach ( $results->translation_sets as $set ) { 330 331 if ( ! isset( $set->wp_locale ) ) { 332 continue; 333 } 334 335 $wp_locale = $set->wp_locale; 336 if ( $set->slug !== 'default' ) { 337 $wp_locale = $wp_locale . '_' . $set->slug; 338 } 339 340 if ( ! isset( $counts[ $wp_locale ] ) ) { 341 $counts[ $wp_locale ] = 0; 342 } 343 $counts[ $wp_locale ] += (int) $set->percent_translated; 344 } 345 } 346 347 foreach ( $counts as $locale => $percent_translated ) { 348 // English locales don't have wp/dev/cc. 349 $projects_count = 0 === strpos( $locale, 'en_' ) ? 3 : 4; 350 351 /* 352 * > 50% round down, so that a project with all strings except 1 translated shows 99%, instead of 100%. 353 * < 50% round up, so that a project with just a few strings shows 1%, instead of 0%. 354 */ 355 $percent_complete = 100 / ( 100 * $projects_count ) * $percent_translated; 356 $percent_complete = ( $percent_complete > 50 ) ? floor( $percent_complete ) : ceil( $percent_complete ); 357 358 $percentages[ $locale ] = $percent_complete; 359 } 360 361 set_transient( 'core_translation_data', $percentages, 900 ); 362 363 return $percentages; 364 } 365 366 /** 367 * Get the locale managers for the given locale. 368 * 369 * @param GP_Locale $locale 370 * @return array 371 */ 372 private function get_locale_managers( $locale ) { 373 $locale_managers = []; 374 375 $result = get_sites( [ 376 'locale' => $locale->wp_locale, 377 'network_id' => WPORG_GLOBAL_NETWORK_ID, 378 'path' => '/', 379 'fields' => 'ids', 380 'number' => '1', 381 ] ); 382 $site_id = array_shift( $result ); 383 if ( ! $site_id ) { 384 return $locale_managers; 385 } 386 387 $users = get_users( [ 388 'blog_id' => $site_id, 389 'role' => 'locale_manager', 390 'count_total' => false, 391 ] ); 392 393 foreach ( $users as $user ) { 394 $locale_managers[ $user->user_nicename ] = $this->prepare_user( $user ); 395 } 396 397 uasort( $locale_managers, [ $this, '_sort_display_name_callback' ] ); 398 399 return $locale_managers; 400 } 401 402 /** 403 * Get the general translation editors for the given locale. 404 * 405 * @param GP_Locale $locale 406 * @return array 407 */ 408 private function get_general_translation_editors( $locale ) { 409 $editors = []; 410 411 $result = get_sites( [ 412 'locale' => $locale->wp_locale, 413 'network_id' => WPORG_GLOBAL_NETWORK_ID, 414 'path' => '/', 415 'fields' => 'ids', 416 'number' => '1', 417 ] ); 418 $site_id = array_shift( $result ); 419 if ( ! $site_id ) { 420 return $editors; 421 } 422 423 $users = get_users( [ 424 'blog_id' => $site_id, 425 'role' => 'general_translation_editor', 426 'count_total' => false, 427 ] ); 428 429 foreach ( $users as $user ) { 430 $editors[ $user->user_nicename ] = $this->prepare_user( $user ); 431 } 432 433 uasort( $editors, [ $this, '_sort_display_name_callback' ] ); 434 435 return $editors; 436 } 437 438 /** 439 * Get the general translation editors for the given locale. 440 * 441 * @param GP_Locale $locale 442 * @return array 443 */ 444 private function get_project_translation_editors( $locale ) { 445 $editors = []; 446 447 $result = get_sites( [ 448 'locale' => $locale->wp_locale, 449 'network_id' => WPORG_GLOBAL_NETWORK_ID, 450 'path' => '/', 451 'fields' => 'ids', 452 'number' => '1', 453 ] ); 454 $site_id = array_shift( $result ); 455 if ( ! $site_id ) { 456 return $editors; 457 } 458 459 $users = get_users( [ 460 'blog_id' => $site_id, 461 'role' => 'translation_editor', 462 'count_total' => false, 463 ] ); 464 465 foreach ( $users as $user ) { 466 $editors[ $user->user_nicename ] = $this->prepare_user( $user ); 467 } 468 469 uasort( $editors, [ $this, '_sort_display_name_callback' ] ); 470 471 return $editors; 472 } 473 474 /** 475 * Prepares user objects for output. 476 * 477 * @param \WP_User $user The user. 478 * @return array List of user data. 479 */ 480 private function prepare_user( $user ) { 481 if ( $user->display_name && $user->display_name !== $user->user_nicename ) { 482 return [ 483 'display_name' => $user->display_name, 484 'email' => $user->user_email, 485 'nice_name' => $user->user_nicename, 486 'slack' => self::get_slack_username( $user->ID ), 487 ]; 488 } else { 489 return [ 490 'display_name' => $user->user_nicename, 491 'email' => $user->user_email, 492 'nice_name' => $user->user_nicename, 493 'slack' => self::get_slack_username( $user->ID ), 494 ]; 495 } 496 } 497 498 /** 499 * Get the translation contributors for the given locale. 500 * 501 * @param GP_Locale $locale 502 * @return array 503 */ 504 private function get_translation_contributors( $locale, $max_age_days = null ) { 505 global $wpdb; 506 507 $contributors = array(); 508 509 $date_constraint = ''; 510 if ( null !== $max_age_days ) { 511 $date_constraint = $wpdb->prepare( " AND date_modified >= CURRENT_DATE - INTERVAL %d DAY", $max_age_days ); 512 } 513 514 [ $locale, $locale_slug ] = array_merge( explode( '/', $locale->slug ), [ 'default' ] ); 515 516 $users = $wpdb->get_col( $wpdb->prepare( 517 "SELECT DISTINCT user_id FROM translate_user_translations_count WHERE accepted > 0 AND locale = %s AND locale_slug = %s", 518 $locale, 519 $locale_slug 520 ) . $date_constraint ); 521 522 if ( ! $users ) { 523 return $contributors; 524 } 525 526 $user_data = $wpdb->get_results( "SELECT user_nicename, display_name, user_email FROM $wpdb->users WHERE ID IN (" . implode( ',', $users ) . ")" ); 527 foreach ( $user_data as $user ) { 528 if ( $user->display_name && $user->display_name !== $user->user_nicename ) { 529 $contributors[ $user->user_nicename ] = array( 530 'display_name' => $user->display_name, 531 'nice_name' => $user->user_nicename, 532 ); 533 } else { 534 $contributors[ $user->user_nicename ] = array( 535 'display_name' => $user->user_nicename, 536 'nice_name' => $user->user_nicename, 537 ); 538 } 539 } 540 541 uasort( $contributors, array( $this, '_sort_display_name_callback' ) ); 542 543 return $contributors; 544 } 545 546 /** 547 * Determine the release status of the given locale, 548 * 549 * @param string $rosetta_site_url 550 * @param string $latest_release 551 * 552 * @return string 553 */ 554 protected static function get_locale_release_status( $rosetta_site_url, $latest_release ) { 555 if ( ! $rosetta_site_url ) { 556 return 'no-site'; 557 } 558 559 if ( ! $latest_release ) { 560 return 'no-releases'; 561 } 562 563 $one_lower = floatval( WP_CORE_LATEST_RELEASE ) - 0.1; 564 565 if ( $latest_release == WP_CORE_LATEST_RELEASE ) { 566 return 'latest'; 567 } elseif ( substr( $latest_release, 0, 3 ) == substr( WP_CORE_LATEST_RELEASE, 0, 3 ) ) { 568 return 'minor-behind'; 569 } elseif ( substr( $latest_release, 0, 3 ) == substr( $one_lower, 0, 3 ) ) { 570 return 'major-behind-one'; 571 } else { 572 return 'major-behind-many'; 573 } 574 } 575 576 /** 577 * Determine the translation status of the given locale. 578 * 579 * @param int $percent_translated 580 * 581 * @return string 582 */ 583 protected static function get_locale_translation_status( $percent_translated ) { 584 if ( $percent_translated == 100 ) { 585 return 'translated-100'; 586 } elseif ( $percent_translated >= 95 ) { 587 return 'translated-95'; 588 } elseif ( $percent_translated >= 90 ) { 589 return 'translated-90'; 590 } elseif ( $percent_translated >= 50 ) { 591 return 'translated-50'; 592 } else { 593 return 'translated-50-less'; 594 } 595 } 596 597 /** 598 * Get the Slack username for a .org user. 599 * 600 * @param int $user_id 601 * 602 * @return string 603 */ 604 protected static function get_slack_username( $user_id ) { 605 global $wpdb; 606 607 $slack_username = ''; 608 609 $data = $wpdb->get_var( $wpdb->prepare( "SELECT profiledata FROM slack_users WHERE user_id = %d", $user_id ) ); 610 if ( $data && ( $data = json_decode( $data, true ) ) ) { 611 if ( !empty( $data['profile']['display_name'] ) && empty( $data['deleted'] ) ) { 612 // Optional Display Name field. 613 $slack_username = $data['profile']['display_name']; 614 } elseif ( !empty( $data['profile']['real_name'] ) && empty( $data['deleted'] ) ) { 615 // Fall back to "Full Name" field. 616 $slack_username = $data['profile']['real_name']; 617 } 618 } 619 620 return $slack_username; 621 } 622 623 public function _sort_display_name_callback( $a, $b ) { 624 return strnatcasecmp( $a['display_name'], $b['display_name'] ); 625 } 626 } 627 628 $GLOBALS['wp_i18n_teams'] = new WP_I18n_Teams(); 20 bootstrap();
Note: See TracChangeset
for help on using the changeset viewer.