From 3672bc51a4daa2d03dc2773eecc1c0f2a194fdfb Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:35:25 +0100 Subject: [PATCH 01/10] Add "Locales" must-use plugin. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/mu-plugins/locales.php | 100 + content/mu-plugins/locales/locales.php | 2905 ++++++++++++++++++++++++ 2 files changed, 3005 insertions(+) create mode 100644 content/mu-plugins/locales.php create mode 100644 content/mu-plugins/locales/locales.php diff --git a/content/mu-plugins/locales.php b/content/mu-plugins/locales.php new file mode 100644 index 0000000..98b2c55 --- /dev/null +++ b/content/mu-plugins/locales.php @@ -0,0 +1,100 @@ +GP_Locale and GP_Locales and extends them with custom locales used throughout wordpress.org. + * License: GPLv2 or later + */ + +namespace { + require_once __DIR__ . '/locales/locales.php'; + require_once __DIR__ . '/locale-switcher/locale-switcher.php'; +} + +namespace WordPressdotorg\Locales { + + use GP_Locales, GP_Locale; + + /** + * Sets available languages to all possible locales. + */ + function set_available_languages() { + static $locales; + + if ( ! isset( $locales ) ) { + $locales = GP_Locales::locales(); + $locales = array_column( $locales, 'wp_locale' ); + $locales = array_filter( $locales ); + } + + return $locales; + } + add_filter( 'get_available_languages', __NAMESPACE__ . '\set_available_languages', 10, 0 ); + + /** + * Retrieves all available locales. + * + * @return GP_Locale[] Array of locale objects. + */ + function get_locales() { + wp_cache_add_global_groups( array( 'locale-associations' ) ); + + $wp_locales = wp_cache_get( 'locale-list', 'locale-associations' ); + if ( false === $wp_locales ) { + $wp_locales = (array) $GLOBALS['wpdb']->get_col( 'SELECT locale FROM wporg_locales' ); + wp_cache_set( 'locale-list', $wp_locales, 'locale-associations' ); + } + + $wp_locales[] = 'en_US'; + $locales = array(); + + foreach ( $wp_locales as $locale ) { + $gp_locale = GP_Locales::by_field( 'wp_locale', $locale ); + if ( ! $gp_locale ) { + continue; + } + + $locales[ $locale ] = $gp_locale; + } + + natsort( $locales ); + + return $locales; + } + + /** + * Get an array of locales with the locale code as key and the native name as value. + * + * @return array + */ + function get_locales_with_native_names() { + $locales = get_locales(); + + return wp_list_pluck( $locales, 'native_name', 'wp_locale' ); + } + + /** + * Get an array of locales with the locale code as key and the English name as value. + * + * @return array + */ + function get_locales_with_english_names() { + $locales = get_locales(); + + return wp_list_pluck( $locales, 'english_name', 'wp_locale' ); + } + + /** + * Get the name of a locale from the code. + * + * @param string $code The locale code to look up. E.g. en_US. + * @param string $name_type Optional. 'native' or 'english'. Default 'native'. + * + * @return mixed|string + */ + function get_locale_name_from_code( $code, $name_type = 'native' ) { + $function = __NAMESPACE__ . "\get_locales_with_{$name_type}_names"; + $locales = $function(); + + return $locales[ $code ] ?? ''; + } +} diff --git a/content/mu-plugins/locales/locales.php b/content/mu-plugins/locales/locales.php new file mode 100644 index 0000000..6e41f2c --- /dev/null +++ b/content/mu-plugins/locales/locales.php @@ -0,0 +1,2905 @@ + $value ) { + $this->$key = $value; + } + } + + public static function __set_state( $state ) { + return new GP_Locale( $state ); + } + + /** + * Make deprecated properties checkable for backwards compatibility. + * + * @param string $name Property to check if set. + * @return bool Whether the property is set. + */ + public function __isset( $name ) { + if ( 'rtl' == $name ) { + return isset( $this->text_direction ); + } + return false; + } + + /** + * Make deprecated properties readable for backwards compatibility. + * + * @param string $name Property to get. + * @return mixed Property. + */ + public function __get( $name ) { + if ( 'rtl' == $name ) { + return ( 'rtl' === $this->text_direction ); + } + + return null; + } + + public function combined_name() { + /* translators: combined name for locales: 1: name in English, 2: native name */ + return sprintf( __( '%1$s/%2$s', 'glotpress' ), $this->english_name, $this->native_name ); + } + + public function numbers_for_index( $index, $how_many = 3, $test_up_to = 1000 ) { + $numbers = array(); + + for( $number = 0; $number < $test_up_to; ++$number ) { + if ( $this->index_for_number( $number ) == $index ) { + $numbers[] = $number; + + if ( count( $numbers ) >= $how_many ) { + break; + } + } + } + + return $numbers; + } + + public function index_for_number( $number ) { + if ( ! isset( $this->_index_for_number ) ) { + $gettext = new Gettext_Translations; + $expression = $gettext->parenthesize_plural_exression( $this->plural_expression ); + $this->_index_for_number = $gettext->make_plural_form_function( $this->nplurals, $expression ); + } + + $f = $this->_index_for_number; + + return $f( $number ); + } + + /** + * When converting the object to a string, the combined name is returned. + * + * @since 3.0.0 + * + * @return string Combined name of locale. + */ + public function __toString() { + return $this->combined_name(); + } +} + +endif; + +if ( ! class_exists( 'GP_Locales' ) ) : + +class GP_Locales { + + public $locales = array(); + + public function __construct() { + $aa = new GP_Locale(); + $aa->english_name = 'Afar'; + $aa->native_name = 'Afaraf'; + $aa->lang_code_iso_639_1 = 'aa'; + $aa->lang_code_iso_639_2 = 'aar'; + $aa->slug = 'aa'; + + $ae = new GP_Locale(); + $ae->english_name = 'Avestan'; + $ae->native_name = 'Avesta'; + $ae->lang_code_iso_639_1 = 'ae'; + $ae->lang_code_iso_639_2 = 'ave'; + $ae->slug = 'ae'; + $ae->alphabet = 'avestan'; + + $af = new GP_Locale(); + $af->english_name = 'Afrikaans'; + $af->native_name = 'Afrikaans'; + $af->lang_code_iso_639_1 = 'af'; + $af->lang_code_iso_639_2 = 'afr'; + $af->country_code = 'za'; + $af->wp_locale = 'af'; + $af->slug = 'af'; + $af->google_code = 'af'; + $af->facebook_locale = 'af_ZA'; + + $ak = new GP_Locale(); + $ak->english_name = 'Akan'; + $ak->native_name = 'Akan'; + $ak->lang_code_iso_639_1 = 'ak'; + $ak->lang_code_iso_639_2 = 'aka'; + $ak->slug = 'ak'; + $ak->facebook_locale = 'ak_GH'; + $ak->alphabet = 'adinkra'; + + $am = new GP_Locale(); + $am->english_name = 'Amharic'; + $am->native_name = 'አማርኛ'; + $am->lang_code_iso_639_1 = 'am'; + $am->lang_code_iso_639_2 = 'amh'; + $am->country_code = 'et'; + $am->wp_locale = 'am'; + $am->slug = 'am'; + $am->facebook_locale = 'am_ET'; + $am->alphabet = 'geez'; + + $an = new GP_Locale(); + $an->english_name = 'Aragonese'; + $an->native_name = 'Aragonés'; + $an->lang_code_iso_639_1 = 'an'; + $an->lang_code_iso_639_2 = 'arg'; + $an->lang_code_iso_639_3 = 'arg'; + $an->country_code = 'es'; + $an->wp_locale = 'arg'; + $an->slug = 'an'; + + $ar = new GP_Locale(); + $ar->english_name = 'Arabic'; + $ar->native_name = 'العربية'; + $ar->lang_code_iso_639_1 = 'ar'; + $ar->lang_code_iso_639_2 = 'ara'; + $ar->wp_locale = 'ar'; + $ar->slug = 'ar'; + $ar->nplurals = 6; + $ar->plural_expression = '(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5))))'; + $ar->text_direction = 'rtl'; + $ar->preferred_sans_serif_font_family = 'Tahoma'; + $ar->google_code = 'ar'; + $ar->facebook_locale = 'ar_AR'; + $ar->alphabet = 'arabic'; + + $arq = new GP_Locale(); + $arq->english_name = 'Algerian Arabic'; + $arq->native_name = 'الدارجة الجزايرية'; + $arq->lang_code_iso_639_1 = 'ar'; + $arq->lang_code_iso_639_3 = 'arq'; + $arq->country_code = 'dz'; + $arq->wp_locale = 'arq'; + $arq->slug = 'arq'; + $arq->nplurals = 6; + $arq->plural_expression = '(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5))))'; + $arq->text_direction = 'rtl'; + $arq->alphabet = 'arabic'; + + $ary = new GP_Locale(); + $ary->english_name = 'Moroccan Arabic'; + $ary->native_name = 'العربية المغربية'; + $ary->lang_code_iso_639_1 = 'ar'; + $ary->lang_code_iso_639_3 = 'ary'; + $ary->country_code = 'ma'; + $ary->wp_locale = 'ary'; + $ary->slug = 'ary'; + $ary->nplurals = 6; + $ary->plural_expression = '(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n % 100 >= 3 && n % 100 <= 10) ? 3 : ((n % 100 >= 11 && n % 100 <= 99) ? 4 : 5))))'; + $ary->text_direction = 'rtl'; + $ary->alphabet = 'arabic'; + + $as = new GP_Locale(); + $as->english_name = 'Assamese'; + $as->native_name = 'অসমীয়া'; + $as->lang_code_iso_639_1 = 'as'; + $as->lang_code_iso_639_2 = 'asm'; + $as->lang_code_iso_639_3 = 'asm'; + $as->country_code = 'in'; + $as->wp_locale = 'as'; + $as->slug = 'as'; + $as->facebook_locale = 'as_IN'; + $as->alphabet = 'assamese'; + + $ast = new GP_Locale(); + $ast->english_name = 'Asturian'; + $ast->native_name = 'Asturianu'; + $ast->lang_code_iso_639_2 = 'ast'; + $ast->lang_code_iso_639_3 = 'ast'; + $ast->country_code = 'es'; + $ast->wp_locale = 'ast'; + $ast->slug = 'ast'; + + $av = new GP_Locale(); + $av->english_name = 'Avaric'; + $av->native_name = 'авар мацӀ'; + $av->lang_code_iso_639_1 = 'av'; + $av->lang_code_iso_639_2 = 'ava'; + $av->slug = 'av'; + $av->alphabet = 'cyrillic'; + + $ay = new GP_Locale(); + $ay->english_name = 'Aymara'; + $ay->native_name = 'aymar aru'; + $ay->lang_code_iso_639_1 = 'ay'; + $ay->lang_code_iso_639_2 = 'aym'; + $ay->country_code = 'bo'; + $ay->slug = 'ay'; + $ay->nplurals = 1; + $ay->plural_expression = '0'; + $ay->facebook_locale = 'ay_BO'; + + $az = new GP_Locale(); + $az->english_name = 'Azerbaijani'; + $az->native_name = 'AzÉ™rbaycan dili'; + $az->lang_code_iso_639_1 = 'az'; + $az->lang_code_iso_639_2 = 'aze'; + $az->country_code = 'az'; + $az->wp_locale = 'az'; + $az->slug = 'az'; + $az->google_code = 'az'; + $az->facebook_locale = 'az_AZ'; + + $azb = new GP_Locale(); + $azb->english_name = 'South Azerbaijani'; + $azb->native_name = 'گؤنئی آذربایجان'; + $azb->lang_code_iso_639_1 = 'az'; + $azb->lang_code_iso_639_3 = 'azb'; + $azb->country_code = 'ir'; + $azb->wp_locale = 'azb'; + $azb->slug = 'azb'; + $azb->text_direction = 'rtl'; + $azb->alphabet = 'persian'; + + $az_tr = new GP_Locale(); + $az_tr->english_name = 'Azerbaijani (Turkey)'; + $az_tr->native_name = 'AzÉ™rbaycan TürkcÉ™si'; + $az_tr->lang_code_iso_639_1 = 'az'; + $az_tr->lang_code_iso_639_2 = 'aze'; + $az_tr->country_code = 'tr'; + $az_tr->wp_locale = 'az_TR'; + $az_tr->slug = 'az-tr'; + + $ba = new GP_Locale(); + $ba->english_name = 'Bashkir'; + $ba->native_name = 'башҡорт теле'; + $ba->lang_code_iso_639_1 = 'ba'; + $ba->lang_code_iso_639_2 = 'bak'; + $ba->wp_locale = 'ba'; + $ba->slug = 'ba'; + $ba->alphabet = 'cyrillic'; + + $bal = new GP_Locale(); + $bal->english_name = 'Catalan (Balear)'; + $bal->native_name = 'Català (Balear)'; + $bal->lang_code_iso_639_2 = 'bal'; + $bal->country_code = 'es'; + $bal->wp_locale = 'bal'; + $bal->slug = 'bal'; + + $bcc = new GP_Locale(); + $bcc->english_name = 'Balochi Southern'; + $bcc->native_name = 'بلوچی مکرانی'; + $bcc->lang_code_iso_639_3 = 'bcc'; + $bcc->country_code = 'pk'; + $bcc->wp_locale = 'bcc'; + $bcc->slug = 'bcc'; + $bcc->nplurals = 1; + $bcc->plural_expression = '0'; + $bcc->text_direction = 'rtl'; + $bcc->alphabet = 'balochi'; + + $be = new GP_Locale(); + $be->english_name = 'Belarusian'; + $be->native_name = 'Беларуская мова'; + $be->lang_code_iso_639_1 = 'be'; + $be->lang_code_iso_639_2 = 'bel'; + $be->country_code = 'by'; + $be->wp_locale = 'bel'; + $be->slug = 'bel'; + $be->nplurals = 3; + $be->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $be->google_code = 'be'; + $be->facebook_locale = 'be_BY'; + $be->alphabet = 'cyrillic'; + + $bg = new GP_Locale(); + $bg->english_name = 'Bulgarian'; + $bg->native_name = 'Български'; + $bg->lang_code_iso_639_1 = 'bg'; + $bg->lang_code_iso_639_2 = 'bul'; + $bg->country_code = 'bg'; + $bg->wp_locale = 'bg_BG'; + $bg->slug = 'bg'; + $bg->google_code = 'bg'; + $bg->facebook_locale = 'bg_BG'; + $bg->alphabet = 'cyrillic'; + + $bgn = new GP_Locale(); + $bgn->english_name = 'Western Balochi'; + $bgn->native_name = 'بلوچی‎'; + $bgn->lang_code_iso_639_3 = 'bgn'; + $bgn->country_code = 'pk'; + $bgn->wp_locale = 'bgn'; + $bgn->slug = 'bgn'; + $bgn->text_direction = 'rtl'; + $bgn->alphabet = 'balochi'; + + $bh = new GP_Locale(); + $bh->english_name = 'Bihari'; + $bh->native_name = 'भोजपुरी'; + $bh->lang_code_iso_639_1 = 'bh'; + $bh->lang_code_iso_639_2 = 'bih'; + $bh->slug = 'bh'; + $bh->alphabet = 'devanagari'; + + $bho = new GP_Locale(); + $bho->english_name = 'Bhojpuri'; + $bho->native_name = 'भोजपुरी'; + $bho->lang_code_iso_639_3 = 'bho'; + $bho->country_code = 'in'; + $bho->wp_locale = 'bho'; + $bho->slug = 'bho'; + $bho->alphabet = 'devanagari'; + + $bi = new GP_Locale(); + $bi->english_name = 'Bislama'; + $bi->native_name = 'Bislama'; + $bi->lang_code_iso_639_1 = 'bi'; + $bi->lang_code_iso_639_2 = 'bis'; + $bi->country_code = 'vu'; + $bi->slug = 'bi'; + + $bm = new GP_Locale(); + $bm->english_name = 'Bambara'; + $bm->native_name = 'Bamanankan'; + $bm->lang_code_iso_639_1 = 'bm'; + $bm->lang_code_iso_639_2 = 'bam'; + $bm->slug = 'bm'; + + $bn_bd = new GP_Locale(); + $bn_bd->english_name = 'Bengali (Bangladesh)'; + $bn_bd->native_name = 'বাংলা'; + $bn_bd->lang_code_iso_639_1 = 'bn'; + $bn_bd->country_code = 'bd'; + $bn_bd->wp_locale = 'bn_BD'; + $bn_bd->slug = 'bn'; + $bn_bd->google_code = 'bn'; + $bn_bd->alphabet = 'bengali'; + + $bn_in = new GP_Locale(); + $bn_in->english_name = 'Bengali (India)'; + $bn_in->native_name = 'বাংলা (ভারত)'; + $bn_in->lang_code_iso_639_1 = 'bn'; + $bn_in->country_code = 'in'; + $bn_in->wp_locale = 'bn_IN'; + $bn_in->slug = 'bn-in'; + $bn_in->google_code = 'bn'; + $bn_in->facebook_locale = 'bn_IN'; + $bn_in->nplurals = 2; + $bn_in->plural_expression = 'n > 1'; + $bn_in->alphabet = 'bengali'; + + $bo = new GP_Locale(); + $bo->english_name = 'Tibetan'; + $bo->native_name = 'བོད་ཡིག'; + $bo->lang_code_iso_639_1 = 'bo'; + $bo->lang_code_iso_639_2 = 'tib'; + $bo->wp_locale = 'bo'; + $bo->slug = 'bo'; + $bo->nplurals = 1; + $bo->plural_expression = '0'; + $bo->alphabet = 'tibetan'; + $bo->word_count_type = 'characters_excluding_spaces'; + + $br = new GP_Locale(); + $br->english_name = 'Breton'; + $br->native_name = 'Brezhoneg'; + $br->lang_code_iso_639_1 = 'br'; + $br->lang_code_iso_639_2 = 'bre'; + $br->lang_code_iso_639_3 = 'bre'; + $br->country_code = 'fr'; + $br->wp_locale = 'bre'; + $br->slug = 'br'; + $br->nplurals = 2; + $br->plural_expression = 'n > 1'; + $br->facebook_locale = 'br_FR'; + + $brx = new GP_Locale(); + $brx->english_name = 'Bodo'; + $brx->native_name = 'बोडो‎'; + $brx->lang_code_iso_639_3 = 'brx'; + $brx->country_code = 'in'; + $brx->wp_locale = 'brx'; + $brx->slug = 'brx'; + $brx->alphabet = 'devanagari'; + + $bs = new GP_Locale(); + $bs->english_name = 'Bosnian'; + $bs->native_name = 'Bosanski'; + $bs->lang_code_iso_639_1 = 'bs'; + $bs->lang_code_iso_639_2 = 'bos'; + $bs->country_code = 'ba'; + $bs->wp_locale = 'bs_BA'; + $bs->slug = 'bs'; + $bs->nplurals = 3; + $bs->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $bs->google_code = 'bs'; + $bs->facebook_locale = 'bs_BA'; + + $ca = new GP_Locale(); + $ca->english_name = 'Catalan'; + $ca->native_name = 'Català '; + $ca->lang_code_iso_639_1 = 'ca'; + $ca->lang_code_iso_639_2 = 'cat'; + $ca->wp_locale = 'ca'; + $ca->slug = 'ca'; + $ca->google_code = 'ca'; + $ca->facebook_locale = 'ca_ES'; + + $ca_valencia = new GP_Locale(); + $ca_valencia->english_name = 'Catalan (Valencian)'; + $ca_valencia->native_name = 'Català (Valencià )'; + $ca_valencia->lang_code_iso_639_1 = 'ca'; + $ca_valencia->lang_code_iso_639_2 = 'cat'; + $ca_valencia->wp_locale = 'ca_valencia'; + $ca_valencia->slug = 'ca-val'; // The slug can only be 10 chars long. + $ca_valencia->google_code = 'ca'; + $ca_valencia->facebook_locale = 'ca_ES'; + + $ce = new GP_Locale(); + $ce->english_name = 'Chechen'; + $ce->native_name = 'Нохчийн мотт'; + $ce->lang_code_iso_639_1 = 'ce'; + $ce->lang_code_iso_639_2 = 'che'; + $ce->slug = 'ce'; + $ce->alphabet = 'cyrillic'; + + $ceb = new GP_Locale(); + $ceb->english_name = 'Cebuano'; + $ceb->native_name = 'Cebuano'; + $ceb->lang_code_iso_639_2 = 'ceb'; + $ceb->lang_code_iso_639_3 = 'ceb'; + $ceb->country_code = 'ph'; + $ceb->wp_locale = 'ceb'; + $ceb->slug = 'ceb'; + $ceb->facebook_locale = 'cx_PH'; + + $ch = new GP_Locale(); + $ch->english_name = 'Chamorro'; + $ch->native_name = 'Chamoru'; + $ch->lang_code_iso_639_1 = 'ch'; + $ch->lang_code_iso_639_2 = 'cha'; + $ch->slug = 'ch'; + + $ckb = new GP_Locale(); + $ckb->english_name = 'Kurdish (Sorani)'; + $ckb->native_name = 'كوردی‎'; + $ckb->lang_code_iso_639_1 = 'ku'; + $ckb->lang_code_iso_639_3 = 'ckb'; + $ckb->country_code = 'iq'; + $ckb->wp_locale = 'ckb'; + $ckb->slug = 'ckb'; + $ckb->text_direction = 'rtl'; + $ckb->facebook_locale = 'cb_IQ'; + $ckb->alphabet = 'sorani'; + + $co = new GP_Locale(); + $co->english_name = 'Corsican'; + $co->native_name = 'Corsu'; + $co->lang_code_iso_639_1 = 'co'; + $co->lang_code_iso_639_2 = 'cos'; + $co->country_code = 'it'; + $co->wp_locale = 'co'; + $co->slug = 'co'; + + $cor = new GP_Locale(); + $cor->english_name = 'Cornish'; + $cor->native_name = 'Kernewek'; + $cor->lang_code_iso_639_1 = 'kw'; + $cor->lang_code_iso_639_2 = 'cor'; + $cor->lang_code_iso_639_2 = 'cor'; + $cor->country_code = 'gb'; + $cor->wp_locale = 'cor'; + $cor->slug = 'cor'; + $cor->nplurals = 6; + $cor->plural_expression = '(n == 0) ? 0 : ((n == 1) ? 1 : (((n % 100 == 2 || n % 100 == 22 || n % 100 == 42 || n % 100 == 62 || n % 100 == 82) || n % 1000 == 0 && (n % 100000 >= 1000 && n % 100000 <= 20000 || n % 100000 == 40000 || n % 100000 == 60000 || n % 100000 == 80000) || n != 0 && n % 1000000 == 100000) ? 2 : ((n % 100 == 3 || n % 100 == 23 || n % 100 == 43 || n % 100 == 63 || n % 100 == 83) ? 3 : ((n != 1 && (n % 100 == 1 || n % 100 == 21 || n % 100 == 41 || n % 100 == 61 || n % 100 == 81)) ? 4 : 5))))'; + + $cr = new GP_Locale(); + $cr->english_name = 'Cree'; + $cr->native_name = 'ᓀᐦᐃᔭᐍᐏᐣ'; + $cr->lang_code_iso_639_1 = 'cr'; + $cr->lang_code_iso_639_2 = 'cre'; + $cr->country_code = 'ca'; + $cr->slug = 'cr'; + $cr->alphabet = 'syllabics'; + + $cs = new GP_Locale(); + $cs->english_name = 'Czech'; + $cs->native_name = 'ÄŒeÅ¡tina'; + $cs->lang_code_iso_639_1 = 'cs'; + $cs->lang_code_iso_639_2 = 'ces'; + $cs->country_code = 'cz'; + $cs->wp_locale = 'cs_CZ'; + $cs->slug = 'cs'; + $cs->nplurals = 3; + $cs->plural_expression = '(n == 1) ? 0 : ((n >= 2 && n <= 4) ? 1 : 2)'; + $cs->google_code = 'cs'; + $cs->facebook_locale = 'cs_CZ'; + + $csb = new GP_Locale(); + $csb->english_name = 'Kashubian'; + $csb->native_name = 'Kaszëbsczi'; + $csb->lang_code_iso_639_2 = 'csb'; + $csb->slug = 'csb'; + $csb->nplurals = 3; + $csb->plural_expression = 'n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2'; + + $cu = new GP_Locale(); + $cu->english_name = 'Church Slavic'; + $cu->native_name = 'ѩзыкъ словѣньскъ'; + $cu->lang_code_iso_639_1 = 'cu'; + $cu->lang_code_iso_639_2 = 'chu'; + $cu->slug = 'cu'; + $cu->alphabet = 'cyrillic'; + + $cv = new GP_Locale(); + $cv->english_name = 'Chuvash'; + $cv->native_name = 'чӑваш чӗлхи'; + $cv->lang_code_iso_639_1 = 'cv'; + $cv->lang_code_iso_639_2 = 'chv'; + $cv->country_code = 'ru'; + $cv->slug = 'cv'; + $cv->alphabet = 'cyrillic'; + + $cy = new GP_Locale(); + $cy->english_name = 'Welsh'; + $cy->native_name = 'Cymraeg'; + $cy->lang_code_iso_639_1 = 'cy'; + $cy->lang_code_iso_639_2 = 'cym'; + $cy->country_code = 'gb'; + $cy->wp_locale = 'cy'; + $cy->slug = 'cy'; + $cy->nplurals = 4; + $cy->plural_expression = '(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3'; + $cy->google_code = 'cy'; + $cy->facebook_locale = 'cy_GB'; + + $da = new GP_Locale(); + $da->english_name = 'Danish'; + $da->native_name = 'Dansk'; + $da->lang_code_iso_639_1 = 'da'; + $da->lang_code_iso_639_2 = 'dan'; + $da->country_code = 'dk'; + $da->wp_locale = 'da_DK'; + $da->slug = 'da'; + $da->google_code = 'da'; + $da->facebook_locale = 'da_DK'; + + $de = new GP_Locale(); + $de->english_name = 'German'; + $de->native_name = 'Deutsch'; + $de->lang_code_iso_639_1 = 'de'; + $de->country_code = 'de'; + $de->wp_locale = 'de_DE'; + $de->slug = 'de'; + $de->google_code = 'de'; + $de->facebook_locale = 'de_DE'; + + $de_formal = clone $de; + $de_formal->english_name = 'German (Formal)'; + $de_formal->native_name = 'Deutsch (Sie)'; + $de_formal->slug = 'de/formal'; + $de_formal->wp_locale = 'de_DE_formal'; + $de_formal->root_slug = $de->slug; + + $de_at = new GP_Locale(); + $de_at->english_name = 'German (Austria)'; + $de_at->native_name = 'Deutsch (Österreich)'; + $de_at->lang_code_iso_639_1 = 'de'; + $de_at->country_code = 'at'; + $de_at->wp_locale = 'de_AT'; + $de_at->slug = 'de-at'; + $de_at->google_code = 'de'; + + $de_ch = new GP_Locale(); + $de_ch->english_name = 'German (Switzerland)'; + $de_ch->native_name = 'Deutsch (Schweiz)'; + $de_ch->lang_code_iso_639_1 = 'de'; + $de_ch->country_code = 'ch'; + $de_ch->wp_locale = 'de_CH'; + $de_ch->slug = 'de-ch'; + $de_ch->google_code = 'de'; + + $de_ch_informal = clone $de_ch; + $de_ch_informal->english_name = 'German (Switzerland, Informal)'; + $de_ch_informal->native_name = 'Deutsch (Schweiz, Du)'; + $de_ch_informal->slug = 'de-ch/informal'; + $de_ch_informal->wp_locale = 'de_CH_informal'; + $de_ch_informal->root_slug = $de_ch->slug; + + $dsb = new GP_Locale(); + $dsb->english_name = 'Lower Sorbian'; + $dsb->native_name = 'Dolnoserbšćina'; + $dsb->lang_code_iso_639_2 = 'dsb'; + $dsb->lang_code_iso_639_3 = 'dsb'; + $dsb->country_code = 'de'; + $dsb->wp_locale = 'dsb'; + $dsb->slug = 'dsb'; + $dsb->nplurals = 4; + $dsb->plural_expression = '(n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3))'; + + $dv = new GP_Locale(); + $dv->english_name = 'Dhivehi'; + $dv->native_name = 'Þ‹Þ¨ÞˆÞ¬Þ€Þ¨'; + $dv->lang_code_iso_639_1 = 'dv'; + $dv->lang_code_iso_639_2 = 'div'; + $dv->country_code = 'mv'; + $dv->wp_locale = 'dv'; + $dv->slug = 'dv'; + $dv->text_direction = 'rtl'; + $dv->alphabet = 'thaana'; + + $dzo = new GP_Locale(); + $dzo->english_name = 'Dzongkha'; + $dzo->native_name = 'རྫོང་ཁ'; + $dzo->lang_code_iso_639_1 = 'dz'; + $dzo->lang_code_iso_639_2 = 'dzo'; + $dzo->country_code = 'bt'; + $dzo->wp_locale = 'dzo'; + $dzo->slug = 'dzo'; + $dzo->nplurals = 1; + $dzo->plural_expression = '0'; + $dzo->alphabet = 'tibetan'; + + $ewe = new GP_Locale(); + $ewe->english_name = 'Ewe'; + $ewe->native_name = 'EÊ‹egbe'; + $ewe->lang_code_iso_639_1 = 'ee'; + $ewe->lang_code_iso_639_2 = 'ewe'; + $ewe->lang_code_iso_639_3 = 'ewe'; + $ewe->country_code = 'gh'; + $ewe->wp_locale = 'ewe'; + $ewe->slug = 'ee'; + + $el = new GP_Locale(); + $el->english_name = 'Greek'; + $el->native_name = 'Ελληνικά'; + $el->lang_code_iso_639_1 = 'el'; + $el->lang_code_iso_639_2 = 'ell'; + $el->country_code = 'gr'; + $el->wp_locale = 'el'; + $el->slug = 'el'; + $el->google_code = 'el'; + $el->facebook_locale = 'el_GR'; + $el->alphabet = 'greek'; + + $el_po = new GP_Locale(); + $el_po->english_name = 'Greek (Polytonic)'; + $el_po->native_name = 'Greek (Polytonic)'; // TODO. + $el_po->country_code = 'gr'; + $el_po->slug = 'el-po'; + $el_po->alphabet = 'polytonic'; + + $emoji = new GP_Locale(); + $emoji->english_name = 'Emoji'; + $emoji->native_name = "\xf0\x9f\x8c\x8f\xf0\x9f\x8c\x8d\xf0\x9f\x8c\x8e (Emoji)"; + $emoji->lang_code_iso_639_2 = 'art'; + $emoji->wp_locale = 'art_xemoji'; + $emoji->slug = 'art-xemoji'; + $emoji->nplurals = 1; + $emoji->plural_expression = '0'; + $emoji->alphabet = 'emoji'; + + $en = new GP_Locale(); + $en->english_name = 'English'; + $en->native_name = 'English'; + $en->lang_code_iso_639_1 = 'en'; + $en->country_code = 'us'; + $en->wp_locale = 'en_US'; + $en->slug = 'en'; + $en->google_code = 'en'; + $en->facebook_locale = 'en_US'; + + $en_au = new GP_Locale(); + $en_au->english_name = 'English (Australia)'; + $en_au->native_name = 'English (Australia)'; + $en_au->lang_code_iso_639_1 = 'en'; + $en_au->lang_code_iso_639_2 = 'eng'; + $en_au->lang_code_iso_639_3 = 'eng'; + $en_au->country_code = 'au'; + $en_au->wp_locale = 'en_AU'; + $en_au->slug = 'en-au'; + $en_au->google_code = 'en'; + + $en_ca = new GP_Locale(); + $en_ca->english_name = 'English (Canada)'; + $en_ca->native_name = 'English (Canada)'; + $en_ca->lang_code_iso_639_1 = 'en'; + $en_ca->lang_code_iso_639_2 = 'eng'; + $en_ca->lang_code_iso_639_3 = 'eng'; + $en_ca->country_code = 'ca'; + $en_ca->wp_locale = 'en_CA'; + $en_ca->slug = 'en-ca'; + $en_ca->google_code = 'en'; + + $en_gb = new GP_Locale(); + $en_gb->english_name = 'English (UK)'; + $en_gb->native_name = 'English (UK)'; + $en_gb->lang_code_iso_639_1 = 'en'; + $en_gb->lang_code_iso_639_2 = 'eng'; + $en_gb->lang_code_iso_639_3 = 'eng'; + $en_gb->country_code = 'gb'; + $en_gb->wp_locale = 'en_GB'; + $en_gb->slug = 'en-gb'; + $en_gb->google_code = 'en'; + $en_gb->facebook_locale = 'en_GB'; + + $en_ie = new GP_Locale(); + $en_ie->english_name = 'English (Ireland)'; + $en_ie->native_name = 'English (Ireland)'; + $en_ie->lang_code_iso_639_1 = 'en'; + $en_ie->lang_code_iso_639_2 = 'eng'; + $en_ie->lang_code_iso_639_3 = 'eng'; + $en_ie->country_code = 'ie'; + $en_ie->slug = 'en-ie'; + $en_ie->google_code = 'en'; + + $en_nz = new GP_Locale(); + $en_nz->english_name = 'English (New Zealand)'; + $en_nz->native_name = 'English (New Zealand)'; + $en_nz->lang_code_iso_639_1 = 'en'; + $en_nz->lang_code_iso_639_2 = 'eng'; + $en_nz->lang_code_iso_639_3 = 'eng'; + $en_nz->country_code = 'nz'; + $en_nz->wp_locale = 'en_NZ'; + $en_nz->slug = 'en-nz'; + $en_nz->google_code = 'en'; + + $en_za = new GP_Locale(); + $en_za->english_name = 'English (South Africa)'; + $en_za->native_name = 'English (South Africa)'; + $en_za->lang_code_iso_639_1 = 'en'; + $en_za->lang_code_iso_639_2 = 'eng'; + $en_za->lang_code_iso_639_3 = 'eng'; + $en_za->country_code = 'za'; + $en_za->wp_locale = 'en_ZA'; + $en_za->slug = 'en-za'; + $en_za->google_code = 'en'; + + $eo = new GP_Locale(); + $eo->english_name = 'Esperanto'; + $eo->native_name = 'Esperanto'; + $eo->lang_code_iso_639_1 = 'eo'; + $eo->lang_code_iso_639_2 = 'epo'; + $eo->wp_locale = 'eo'; + $eo->slug = 'eo'; + $eo->google_code = 'eo'; + $eo->facebook_locale = 'eo_EO'; + + $es = new GP_Locale(); + $es->english_name = 'Spanish (Spain)'; + $es->native_name = 'Español'; + $es->lang_code_iso_639_1 = 'es'; + $es->lang_code_iso_639_2 = 'spa'; + $es->lang_code_iso_639_3 = 'spa'; + $es->country_code = 'es'; + $es->wp_locale = 'es_ES'; + $es->slug = 'es'; + $es->google_code = 'es'; + $es->facebook_locale = 'es_ES'; + + $es_ar = new GP_Locale(); + $es_ar->english_name = 'Spanish (Argentina)'; + $es_ar->native_name = 'Español de Argentina'; + $es_ar->lang_code_iso_639_1 = 'es'; + $es_ar->lang_code_iso_639_2 = 'spa'; + $es_ar->lang_code_iso_639_3 = 'spa'; + $es_ar->country_code = 'ar'; + $es_ar->wp_locale = 'es_AR'; + $es_ar->slug = 'es-ar'; + $es_ar->google_code = 'es'; + $es_ar->facebook_locale = 'es_LA'; + + $es_cl = new GP_Locale(); + $es_cl->english_name = 'Spanish (Chile)'; + $es_cl->native_name = 'Español de Chile'; + $es_cl->lang_code_iso_639_1 = 'es'; + $es_cl->lang_code_iso_639_2 = 'spa'; + $es_cl->lang_code_iso_639_3 = 'spa'; + $es_cl->country_code = 'cl'; + $es_cl->wp_locale = 'es_CL'; + $es_cl->slug = 'es-cl'; + $es_cl->google_code = 'es'; + $es_cl->facebook_locale = 'es_LA'; + + $es_co = new GP_Locale(); + $es_co->english_name = 'Spanish (Colombia)'; + $es_co->native_name = 'Español de Colombia'; + $es_co->lang_code_iso_639_1 = 'es'; + $es_co->lang_code_iso_639_2 = 'spa'; + $es_co->lang_code_iso_639_3 = 'spa'; + $es_co->country_code = 'co'; + $es_co->wp_locale = 'es_CO'; + $es_co->slug = 'es-co'; + $es_co->google_code = 'es'; + $es_co->facebook_locale = 'es_LA'; + + $es_cr = new GP_Locale(); + $es_cr->english_name = 'Spanish (Costa Rica)'; + $es_cr->native_name = 'Español de Costa Rica'; + $es_cr->lang_code_iso_639_1 = 'es'; + $es_cr->lang_code_iso_639_2 = 'spa'; + $es_cr->lang_code_iso_639_3 = 'spa'; + $es_cr->country_code = 'cr'; + $es_cr->wp_locale = 'es_CR'; + $es_cr->slug = 'es-cr'; + $es_cr->google_code = 'es'; + $es_cr->facebook_locale = 'es_LA'; + + $es_do = new GP_Locale(); + $es_do->english_name = 'Spanish (Dominican Republic)'; + $es_do->native_name = 'Español de República Dominicana'; + $es_do->lang_code_iso_639_1 = 'es'; + $es_do->lang_code_iso_639_2 = 'spa'; + $es_do->lang_code_iso_639_3 = 'spa'; + $es_do->country_code = 'do'; + $es_do->wp_locale = 'es_DO'; + $es_do->slug = 'es-do'; + $es_do->google_code = 'es'; + $es_do->facebook_locale = 'es_LA'; + + $es_ec = new GP_Locale(); + $es_ec->english_name = 'Spanish (Ecuador)'; + $es_ec->native_name = 'Español de Ecuador'; + $es_ec->lang_code_iso_639_1 = 'es'; + $es_ec->lang_code_iso_639_2 = 'spa'; + $es_ec->lang_code_iso_639_3 = 'spa'; + $es_ec->country_code = 'ec'; + $es_ec->wp_locale = 'es_EC'; + $es_ec->slug = 'es-ec'; + $es_ec->google_code = 'es'; + $es_ec->facebook_locale = 'es_LA'; + + $es_gt = new GP_Locale(); + $es_gt->english_name = 'Spanish (Guatemala)'; + $es_gt->native_name = 'Español de Guatemala'; + $es_gt->lang_code_iso_639_1 = 'es'; + $es_gt->lang_code_iso_639_2 = 'spa'; + $es_gt->lang_code_iso_639_3 = 'spa'; + $es_gt->country_code = 'gt'; + $es_gt->wp_locale = 'es_GT'; + $es_gt->slug = 'es-gt'; + $es_gt->google_code = 'es'; + $es_gt->facebook_locale = 'es_LA'; + + $es_hn = new GP_Locale(); + $es_hn->english_name = 'Spanish (Honduras)'; + $es_hn->native_name = 'Español de Honduras'; + $es_hn->lang_code_iso_639_1 = 'es'; + $es_hn->lang_code_iso_639_2 = 'spa'; + $es_hn->lang_code_iso_639_3 = 'spa'; + $es_hn->country_code = 'hn'; + $es_hn->wp_locale = 'es_HN'; + $es_hn->slug = 'es-hn'; + $es_hn->google_code = 'es'; + $es_hn->facebook_locale = 'es_LA'; + + $es_mx = new GP_Locale(); + $es_mx->english_name = 'Spanish (Mexico)'; + $es_mx->native_name = 'Español de México'; + $es_mx->lang_code_iso_639_1 = 'es'; + $es_mx->lang_code_iso_639_2 = 'spa'; + $es_mx->lang_code_iso_639_3 = 'spa'; + $es_mx->country_code = 'mx'; + $es_mx->wp_locale = 'es_MX'; + $es_mx->slug = 'es-mx'; + $es_mx->google_code = 'es'; + $es_mx->facebook_locale = 'es_LA'; + + $es_pa = new GP_Locale(); + $es_pa->english_name = 'Spanish (Panama)'; + $es_pa->native_name = 'Español de Panamá'; + $es_pa->lang_code_iso_639_1 = 'es'; + $es_pa->lang_code_iso_639_2 = 'spa'; + $es_pa->lang_code_iso_639_3 = 'spa'; + $es_pa->country_code = 'pa'; + $es_pa->slug = 'es-pa'; + $es_pa->google_code = 'es'; + $es_pa->facebook_locale = 'es_LA'; + + $es_pe = new GP_Locale(); + $es_pe->english_name = 'Spanish (Peru)'; + $es_pe->native_name = 'Español de Perú'; + $es_pe->lang_code_iso_639_1 = 'es'; + $es_pe->lang_code_iso_639_2 = 'spa'; + $es_pe->lang_code_iso_639_3 = 'spa'; + $es_pe->country_code = 'pe'; + $es_pe->wp_locale = 'es_PE'; + $es_pe->slug = 'es-pe'; + $es_pe->google_code = 'es'; + $es_pe->facebook_locale = 'es_LA'; + + $es_pr = new GP_Locale(); + $es_pr->english_name = 'Spanish (Puerto Rico)'; + $es_pr->native_name = 'Español de Puerto Rico'; + $es_pr->lang_code_iso_639_1 = 'es'; + $es_pr->lang_code_iso_639_2 = 'spa'; + $es_pr->lang_code_iso_639_3 = 'spa'; + $es_pr->country_code = 'pr'; + $es_pr->wp_locale = 'es_PR'; + $es_pr->slug = 'es-pr'; + $es_pr->google_code = 'es'; + $es_pr->facebook_locale = 'es_LA'; + + $es_us = new GP_Locale(); + $es_us->english_name = 'Spanish (US)'; + $es_us->native_name = 'Español de los Estados Unidos'; + $es_us->lang_code_iso_639_1 = 'es'; + $es_us->lang_code_iso_639_2 = 'spa'; + $es_us->lang_code_iso_639_3 = 'spa'; + $es_us->country_code = 'us'; + $es_us->slug = 'es-us'; + $es_us->google_code = 'es'; + + $es_uy = new GP_Locale(); + $es_uy->english_name = 'Spanish (Uruguay)'; + $es_uy->native_name = 'Español de Uruguay'; + $es_uy->lang_code_iso_639_1 = 'es'; + $es_uy->lang_code_iso_639_2 = 'spa'; + $es_uy->lang_code_iso_639_3 = 'spa'; + $es_uy->country_code = 'uy'; + $es_uy->wp_locale = 'es_UY'; + $es_uy->slug = 'es-uy'; + $es_uy->google_code = 'es'; + $es_uy->facebook_locale = 'es_LA'; + + $es_ve = new GP_Locale(); + $es_ve->english_name = 'Spanish (Venezuela)'; + $es_ve->native_name = 'Español de Venezuela'; + $es_ve->lang_code_iso_639_1 = 'es'; + $es_ve->lang_code_iso_639_2 = 'spa'; + $es_ve->lang_code_iso_639_3 = 'spa'; + $es_ve->country_code = 've'; + $es_ve->wp_locale = 'es_VE'; + $es_ve->slug = 'es-ve'; + $es_ve->google_code = 'es'; + $es_ve->facebook_locale = 'es_LA'; + + $et = new GP_Locale(); + $et->english_name = 'Estonian'; + $et->native_name = 'Eesti'; + $et->lang_code_iso_639_1 = 'et'; + $et->lang_code_iso_639_2 = 'est'; + $et->country_code = 'ee'; + $et->wp_locale = 'et'; + $et->slug = 'et'; + $et->google_code = 'et'; + $et->facebook_locale = 'et_EE'; + + $eu = new GP_Locale(); + $eu->english_name = 'Basque'; + $eu->native_name = 'Euskara'; + $eu->lang_code_iso_639_1 = 'eu'; + $eu->lang_code_iso_639_2 = 'eus'; + $eu->country_code = 'es'; + $eu->wp_locale = 'eu'; + $eu->slug = 'eu'; + $eu->google_code = 'eu'; + $eu->facebook_locale = 'eu_ES'; + + $fa = new GP_Locale(); + $fa->english_name = 'Persian'; + $fa->native_name = 'فارسی'; + $fa->lang_code_iso_639_1 = 'fa'; + $fa->lang_code_iso_639_2 = 'fas'; + $fa->wp_locale = 'fa_IR'; + $fa->slug = 'fa'; + $fa->nplurals = 1; + $fa->plural_expression = '0'; + $fa->text_direction = 'rtl'; + $fa->google_code = 'fa'; + $fa->facebook_locale = 'fa_IR'; + $fa->alphabet = 'persian'; + + $fa_af = new GP_Locale(); + $fa_af->english_name = 'Persian (Afghanistan)'; + $fa_af->native_name = '(فارسی (افغانستان'; + $fa_af->lang_code_iso_639_1 = 'fa'; + $fa_af->lang_code_iso_639_2 = 'fas'; + $fa_af->country_code = 'af'; + $fa_af->wp_locale = 'fa_AF'; + $fa_af->slug = 'fa-af'; + $fa_af->nplurals = 1; + $fa_af->plural_expression = '0'; + $fa_af->text_direction = 'rtl'; + $fa_af->google_code = 'fa'; + $fa_af->alphabet = 'persian'; + + $ff_sn = new GP_Locale(); + $ff_sn->english_name = 'Fulah'; + $ff_sn->native_name = 'Pulaar'; + $ff_sn->lang_code_iso_639_1 = 'ff'; + $ff_sn->lang_code_iso_639_2 = 'fuc'; + $ff_sn->country_code = 'sn'; + $ff_sn->wp_locale = 'fuc'; + $ff_sn->slug = 'fuc'; + + $fi = new GP_Locale(); + $fi->english_name = 'Finnish'; + $fi->native_name = 'Suomi'; + $fi->lang_code_iso_639_1 = 'fi'; + $fi->lang_code_iso_639_2 = 'fin'; + $fi->country_code = 'fi'; + $fi->wp_locale = 'fi'; + $fi->slug = 'fi'; + $fi->google_code = 'fi'; + $fi->facebook_locale = 'fi_FI'; + + $fj = new GP_Locale(); + $fj->english_name = 'Fijian'; + $fj->native_name = 'Vosa Vakaviti'; + $fj->lang_code_iso_639_1 = 'fj'; + $fj->lang_code_iso_639_2 = 'fij'; + $fj->country_code = 'fj'; + $fj->slug = 'fj'; + + $fo = new GP_Locale(); + $fo->english_name = 'Faroese'; + $fo->native_name = 'Føroyskt'; + $fo->lang_code_iso_639_1 = 'fo'; + $fo->lang_code_iso_639_2 = 'fao'; + $fo->country_code = 'fo'; + $fo->wp_locale = 'fo'; + $fo->slug = 'fo'; + $fo->facebook_locale = 'fo_FO'; + + $fon = new GP_Locale(); + $fon->english_name = 'Fon'; + $fon->native_name = 'fɔ̀ngbè'; + $fon->lang_code_iso_639_2 = 'fon'; + $fon->lang_code_iso_639_3 = 'fon'; + $fon->country_code = 'bj'; + $fon->wp_locale = 'fon'; + $fon->slug = 'fon'; + + $fr = new GP_Locale(); + $fr->english_name = 'French (France)'; + $fr->native_name = 'Français'; + $fr->lang_code_iso_639_1 = 'fr'; + $fr->country_code = 'fr'; + $fr->wp_locale = 'fr_FR'; + $fr->slug = 'fr'; + $fr->nplurals = 2; + $fr->plural_expression = 'n > 1'; + $fr->google_code = 'fr'; + $fr->facebook_locale = 'fr_FR'; + + $fr_be = new GP_Locale(); + $fr_be->english_name = 'French (Belgium)'; + $fr_be->native_name = 'Français de Belgique'; + $fr_be->lang_code_iso_639_1 = 'fr'; + $fr_be->lang_code_iso_639_2 = 'fra'; + $fr_be->country_code = 'be'; + $fr_be->wp_locale = 'fr_BE'; + $fr_be->slug = 'fr-be'; + $fr_be->nplurals = 2; + $fr_be->plural_expression = 'n > 1'; + + $fr_ca = new GP_Locale(); + $fr_ca->english_name = 'French (Canada)'; + $fr_ca->native_name = 'Français du Canada'; + $fr_ca->lang_code_iso_639_1 = 'fr'; + $fr_ca->lang_code_iso_639_2 = 'fra'; + $fr_ca->country_code = 'ca'; + $fr_ca->wp_locale = 'fr_CA'; + $fr_ca->slug = 'fr-ca'; + $fr_ca->nplurals = 2; + $fr_ca->plural_expression = 'n > 1'; + $fr_ca->facebook_locale = 'fr_CA'; + + $fr_ch = new GP_Locale(); + $fr_ch->english_name = 'French (Switzerland)'; + $fr_ch->native_name = 'Français de Suisse'; + $fr_ch->lang_code_iso_639_1 = 'fr'; + $fr_ch->lang_code_iso_639_2 = 'fra'; + $fr_ch->country_code = 'ch'; + $fr_ch->slug = 'fr-ch'; + $fr_ch->nplurals = 2; + $fr_ch->plural_expression = 'n > 1'; + + $frp = new GP_Locale(); + $frp->english_name = 'Arpitan'; + $frp->native_name = 'Arpitan'; + $frp->lang_code_iso_639_3 = 'frp'; + $frp->country_code = 'fr'; + $frp->wp_locale = 'frp'; + $frp->slug = 'frp'; + $frp->nplurals = 2; + $frp->plural_expression = 'n > 1'; + + $ful = new GP_Locale(); + $ful->english_name = 'Fula'; + $ful->native_name = 'Fulfulde'; + $ful->lang_code_iso_639_1 = 'ff'; + $ful->lang_code_iso_639_2 = 'ful'; + $ful->lang_code_iso_639_3 = 'ful'; + $ful->country_code = 'ng'; + $ful->slug = 'ful'; + $ful->facebook_locale = 'ff_NG'; + + $fur = new GP_Locale(); + $fur->english_name = 'Friulian'; + $fur->native_name = 'Friulian'; + $fur->lang_code_iso_639_2 = 'fur'; + $fur->lang_code_iso_639_3 = 'fur'; + $fur->country_code = 'it'; + $fur->wp_locale = 'fur'; + $fur->slug = 'fur'; + + $fy = new GP_Locale(); + $fy->english_name = 'Frisian'; + $fy->native_name = 'Frysk'; + $fy->lang_code_iso_639_1 = 'fy'; + $fy->lang_code_iso_639_2 = 'fry'; + $fy->country_code = 'nl'; + $fy->wp_locale = 'fy'; + $fy->slug = 'fy'; + $fy->facebook_locale = 'fy_NL'; + + $ga = new GP_Locale(); + $ga->english_name = 'Irish'; + $ga->native_name = 'Gaelige'; + $ga->lang_code_iso_639_1 = 'ga'; + $ga->lang_code_iso_639_2 = 'gle'; + $ga->country_code = 'ie'; + $ga->slug = 'ga'; + $ga->wp_locale = 'ga'; + $ga->nplurals = 5; + $ga->plural_expression = '(n == 1) ? 0 : ((n == 2) ? 1 : ((n >= 3 && n <= 6) ? 2 : ((n >= 7 && n <= 10) ? 3 : 4)))'; + $ga->google_code = 'ga'; + $ga->facebook_locale = 'ga_IE'; + + $gax = new GP_Locale(); + $gax->english_name = 'Borana-Arsi-Guji Oromo'; + $gax->native_name = 'Afaan Oromoo'; + $gax->lang_code_iso_639_3 = 'gax'; + $gax->country_code = 'et'; + $gax->slug = 'gax'; + $gax->wp_locale = 'gax'; + $gax->nplurals = 2; + $gax->plural_expression = 'n > 1'; + + $gd = new GP_Locale(); + $gd->english_name = 'Scottish Gaelic'; + $gd->native_name = 'Gà idhlig'; + $gd->lang_code_iso_639_1 = 'gd'; + $gd->lang_code_iso_639_2 = 'gla'; + $gd->lang_code_iso_639_3 = 'gla'; + $gd->country_code = 'gb'; + $gd->wp_locale = 'gd'; + $gd->slug = 'gd'; + $gd->nplurals = 4; + $gd->plural_expression = '(n == 1 || n == 11) ? 0 : ((n == 2 || n == 12) ? 1 : ((n >= 3 && n <= 10 || n >= 13 && n <= 19) ? 2 : 3))'; + $gd->google_code = 'gd'; + + $gl = new GP_Locale(); + $gl->english_name = 'Galician'; + $gl->native_name = 'Galego'; + $gl->lang_code_iso_639_1 = 'gl'; + $gl->lang_code_iso_639_2 = 'glg'; + $gl->country_code = 'es'; + $gl->wp_locale = 'gl_ES'; + $gl->slug = 'gl'; + $gl->google_code = 'gl'; + $gl->facebook_locale = 'gl_ES'; + + $gn = new GP_Locale(); + $gn->english_name = 'Guaraní'; + $gn->native_name = 'Avañe\'ẽ'; + $gn->lang_code_iso_639_1 = 'gn'; + $gn->lang_code_iso_639_2 = 'grn'; + $gn->slug = 'gn'; + + $gsw = new GP_Locale(); + $gsw->english_name = 'Swiss German'; + $gsw->native_name = 'Schwyzerdütsch'; + $gsw->lang_code_iso_639_2 = 'gsw'; + $gsw->lang_code_iso_639_3 = 'gsw'; + $gsw->country_code = 'ch'; + $gsw->slug = 'gsw'; + + $gu = new GP_Locale(); + $gu->english_name = 'Gujarati'; + $gu->native_name = 'ગુજરાતી'; + $gu->lang_code_iso_639_1 = 'gu'; + $gu->lang_code_iso_639_2 = 'guj'; + $gu->wp_locale = 'gu'; + $gu->slug = 'gu'; + $gu->google_code = 'gu'; + $gu->facebook_locale = 'gu_IN'; + $gu->alphabet = 'gujarati'; + + $ha = new GP_Locale(); + $ha->english_name = 'Hausa (Arabic)'; + $ha->native_name = 'هَوُسَ'; + $ha->lang_code_iso_639_1 = 'ha'; + $ha->lang_code_iso_639_2 = 'hau'; + $ha->slug = 'ha'; + $ha->text_direction = 'rtl'; + $ha->google_code = 'ha'; + $ha->alphabet = 'arabic'; + + $hat = new GP_Locale(); + $hat->english_name = 'Haitian Creole'; + $hat->native_name = 'Kreyol ayisyen'; + $hat->lang_code_iso_639_1 = 'ht'; + $hat->lang_code_iso_639_2 = 'hat'; + $hat->lang_code_iso_639_3 = 'hat'; + $hat->country_code = 'ht'; + $hat->wp_locale = 'hat'; + $hat->slug = 'hat'; + + $hau = new GP_Locale(); + $hau->english_name = 'Hausa'; + $hau->native_name = 'Harshen Hausa'; + $hau->lang_code_iso_639_1 = 'ha'; + $hau->lang_code_iso_639_2 = 'hau'; + $hau->lang_code_iso_639_3 = 'hau'; + $hau->country_code = 'ng'; + $hau->wp_locale = 'hau'; + $hau->slug = 'hau'; + $hau->google_code = 'ha'; + $hau->facebook_locale = 'ha_NG'; + + $haw = new GP_Locale(); + $haw->english_name = 'Hawaiian'; + $haw->native_name = 'ÅŒlelo HawaiÊ»i'; + $haw->lang_code_iso_639_2 = 'haw'; + $haw->country_code = 'us'; + $haw->wp_locale = 'haw_US'; + $haw->slug = 'haw'; + + $haz = new GP_Locale(); + $haz->english_name = 'Hazaragi'; + $haz->native_name = 'هزاره Ú¯ÛŒ'; + $haz->lang_code_iso_639_3 = 'haz'; + $haz->country_code = 'af'; + $haz->wp_locale = 'haz'; + $haz->slug = 'haz'; + $haz->text_direction = 'rtl'; + $haz->alphabet = 'arabic'; + + $he = new GP_Locale(); + $he->english_name = 'Hebrew'; + $he->native_name = 'עִבְרִית'; + $he->lang_code_iso_639_1 = 'he'; + $he->country_code = 'il'; + $he->wp_locale = 'he_IL'; + $he->slug = 'he'; + $he->text_direction = 'rtl'; + $he->google_code = 'iw'; + $he->facebook_locale = 'he_IL'; + $he->alphabet = 'hebrew'; + + $hi = new GP_Locale(); + $hi->english_name = 'Hindi'; + $hi->native_name = 'हिन्दी'; + $hi->lang_code_iso_639_1 = 'hi'; + $hi->lang_code_iso_639_2 = 'hin'; + $hi->country_code = 'in'; + $hi->wp_locale = 'hi_IN'; + $hi->slug = 'hi'; + $hi->google_code = 'hi'; + $hi->facebook_locale = 'hi_IN'; + $hi->alphabet = 'devanagari'; + + $hr = new GP_Locale(); + $hr->english_name = 'Croatian'; + $hr->native_name = 'Hrvatski'; + $hr->lang_code_iso_639_1 = 'hr'; + $hr->lang_code_iso_639_2 = 'hrv'; + $hr->country_code = 'hr'; + $hr->wp_locale = 'hr'; + $hr->slug = 'hr'; + $hr->nplurals = 3; + $hr->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $hr->google_code = 'hr'; + $hr->facebook_locale = 'hr_HR'; + + $hsb = new GP_Locale(); + $hsb->english_name = 'Upper Sorbian'; + $hsb->native_name = 'Hornjoserbšćina'; + $hsb->lang_code_iso_639_2 = 'hsb'; + $hsb->lang_code_iso_639_3 = 'hsb'; + $hsb->country_code = 'de'; + $hsb->wp_locale = 'hsb'; + $hsb->slug = 'hsb'; + $hsb->nplurals = 4; + $hsb->plural_expression = '(n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3))'; + + $hu = new GP_Locale(); + $hu->english_name = 'Hungarian'; + $hu->native_name = 'Magyar'; + $hu->lang_code_iso_639_1 = 'hu'; + $hu->lang_code_iso_639_2 = 'hun'; + $hu->country_code = 'hu'; + $hu->wp_locale = 'hu_HU'; + $hu->slug = 'hu'; + $hu->google_code = 'hu'; + $hu->facebook_locale = 'hu_HU'; + + $hy = new GP_Locale(); + $hy->english_name = 'Armenian'; + $hy->native_name = 'Õ€Õ¡ÕµÕ¥Ö€Õ¥Õ¶'; + $hy->lang_code_iso_639_1 = 'hy'; + $hy->lang_code_iso_639_2 = 'hye'; + $hy->country_code = 'am'; + $hy->wp_locale = 'hy'; + $hy->slug = 'hy'; + $hy->google_code = 'hy'; + $hy->facebook_locale = 'hy_AM'; + $hy->alphabet = 'armenian'; + + $ia = new GP_Locale(); + $ia->english_name = 'Interlingua'; + $ia->native_name = 'Interlingua'; + $ia->lang_code_iso_639_1 = 'ia'; + $ia->lang_code_iso_639_2 = 'ina'; + $ia->slug = 'ia'; + + $ibo = new GP_Locale(); + $ibo->english_name = 'Igbo'; + $ibo->native_name = 'Asụsụ Igbo'; + $ibo->lang_code_iso_639_1 = 'ig'; + $ibo->lang_code_iso_639_2 = 'ibo'; + $ibo->lang_code_iso_639_3 = 'ibo'; + $ibo->country_code = 'ng'; + $ibo->wp_locale = 'ibo'; + $ibo->slug = 'ibo'; + $ibo->nplurals = 1; + $ibo->plural_expression = '0'; + $ibo->google_code = 'ig'; + + $id = new GP_Locale(); + $id->english_name = 'Indonesian'; + $id->native_name = 'Bahasa Indonesia'; + $id->lang_code_iso_639_1 = 'id'; + $id->lang_code_iso_639_2 = 'ind'; + $id->country_code = 'id'; + $id->wp_locale = 'id_ID'; + $id->slug = 'id'; + $id->nplurals = 2; + $id->plural_expression = 'n > 1'; + $id->google_code = 'id'; + $id->facebook_locale = 'id_ID'; + + $ido = new GP_Locale(); + $ido->english_name = 'Ido'; + $ido->native_name = 'Ido'; + $ido->lang_code_iso_639_1 = 'io'; + $ido->lang_code_iso_639_2 = 'ido'; + $ido->lang_code_iso_639_3 = 'ido'; + $ido->wp_locale = 'ido'; + $ido->slug = 'ido'; + + $ike = new GP_Locale(); + $ike->english_name = 'Inuktitut'; + $ike->native_name = 'ᐃᓄᒃᑎᑐᑦ'; + $ike->lang_code_iso_639_1 = 'iu'; + $ike->lang_code_iso_639_2 = 'iku'; + $ike->country_code = 'ca'; + $ike->slug = 'ike'; + $ike->nplurals = 3; + $ike->plural_expression = '(n == 1) ? 0 : ((n == 2) ? 1 : 2)'; + $ike->alphabet = 'syllabics'; + + $ilo = new GP_Locale(); + $ilo->english_name = 'Iloko'; + $ilo->native_name = 'Pagsasao nga Iloko'; + $ilo->lang_code_iso_639_2 = 'ilo'; + $ilo->country_code = 'ph'; + $ilo->slug = 'ilo'; + + $is = new GP_Locale(); + $is->english_name = 'Icelandic'; + $is->native_name = 'Íslenska'; + $is->lang_code_iso_639_1 = 'is'; + $is->lang_code_iso_639_2 = 'isl'; + $is->country_code = 'is'; + $is->slug = 'is'; + $is->wp_locale = 'is_IS'; + $is->nplurals = 2; + $is->plural_expression = 'n % 10 != 1 || n % 100 == 11'; + $is->google_code = 'is'; + $is->facebook_locale = 'is_IS'; + + $it = new GP_Locale(); + $it->english_name = 'Italian'; + $it->native_name = 'Italiano'; + $it->lang_code_iso_639_1 = 'it'; + $it->lang_code_iso_639_2 = 'ita'; + $it->country_code = 'it'; + $it->wp_locale = 'it_IT'; + $it->slug = 'it'; + $it->google_code = 'it'; + $it->facebook_locale = 'it_IT'; + + $ja = new GP_Locale(); + $ja->english_name = 'Japanese'; + $ja->native_name = '日本語'; + $ja->lang_code_iso_639_1 = 'ja'; + $ja->country_code = 'jp'; + $ja->wp_locale = 'ja'; + $ja->slug = 'ja'; + $ja->google_code = 'ja'; + $ja->facebook_locale = 'ja_JP'; + $ja->nplurals = 1; + $ja->plural_expression = '0'; + $ja->alphabet = 'kanji'; + $ja->word_count_type = 'characters_including_spaces'; + + $jv = new GP_Locale(); + $jv->english_name = 'Javanese'; + $jv->native_name = 'Basa Jawa'; + $jv->lang_code_iso_639_1 = 'jv'; + $jv->lang_code_iso_639_2 = 'jav'; + $jv->country_code = 'id'; + $jv->wp_locale = 'jv_ID'; + $jv->slug = 'jv'; + $jv->google_code = 'jw'; + $jv->facebook_locale = 'jv_ID'; + + $ka = new GP_Locale(); + $ka->english_name = 'Georgian'; + $ka->native_name = 'ქართული'; + $ka->lang_code_iso_639_1 = 'ka'; + $ka->lang_code_iso_639_2 = 'kat'; + $ka->country_code = 'ge'; + $ka->wp_locale = 'ka_GE'; + $ka->slug = 'ka'; + $ka->nplurals = 1; + $ka->plural_expression = '0'; + $ka->google_code = 'ka'; + $ka->facebook_locale = 'ka_GE'; + $ka->alphabet = 'georgian'; + + $kaa = new GP_Locale(); + $kaa->english_name = 'Karakalpak'; + $kaa->native_name = 'Qaraqalpaq tili'; + $kaa->lang_code_iso_639_2 = 'kaa'; + $kaa->lang_code_iso_639_3 = 'kaa'; + $kaa->country_code = 'uz'; + $kaa->wp_locale = 'kaa'; + $kaa->slug = 'kaa'; + + $kab = new GP_Locale(); + $kab->english_name = 'Kabyle'; + $kab->native_name = 'Taqbaylit'; + $kab->lang_code_iso_639_2 = 'kab'; + $kab->lang_code_iso_639_3 = 'kab'; + $kab->country_code = 'dz'; + $kab->wp_locale = 'kab'; + $kab->slug = 'kab'; + $kab->nplurals = 2; + $kab->plural_expression = 'n > 1'; + + $kal = new GP_Locale(); + $kal->english_name = 'Greenlandic'; + $kal->native_name = 'Kalaallisut'; + $kal->lang_code_iso_639_1 = 'kl'; + $kal->lang_code_iso_639_2 = 'kal'; + $kal->lang_code_iso_639_3 = 'kal'; + $kal->country_code = 'gl'; + $kal->wp_locale = 'kal'; + $kal->slug = 'kal'; + + $kin = new GP_Locale(); + $kin->english_name = 'Kinyarwanda'; + $kin->native_name = 'Ikinyarwanda'; + $kin->lang_code_iso_639_1 = 'rw'; + $kin->lang_code_iso_639_2 = 'kin'; + $kin->lang_code_iso_639_3 = 'kin'; + $kin->wp_locale = 'kin'; + $kin->country_code = 'rw'; + $kin->slug = 'kin'; + $kin->facebook_locale = 'rw_RW'; + + $kk = new GP_Locale(); + $kk->english_name = 'Kazakh'; + $kk->native_name = 'Қазақ тілі'; + $kk->lang_code_iso_639_1 = 'kk'; + $kk->lang_code_iso_639_2 = 'kaz'; + $kk->country_code = 'kz'; + $kk->wp_locale = 'kk'; + $kk->slug = 'kk'; + $kk->google_code = 'kk'; + $kk->facebook_locale = 'kk_KZ'; + $kk->alphabet = 'cyrillic'; + + $km = new GP_Locale(); + $km->english_name = 'Khmer'; + $km->native_name = 'ភាសាខ្មែរ'; + $km->lang_code_iso_639_1 = 'km'; + $km->lang_code_iso_639_2 = 'khm'; + $km->country_code = 'kh'; + $km->wp_locale = 'km'; + $km->slug = 'km'; + $km->nplurals = 1; + $km->plural_expression = '0'; + $km->google_code = 'km'; + $km->facebook_locale = 'km_KH'; + $km->alphabet = 'khmer'; + $km->word_count_type = 'characters_excluding_spaces'; + + $kmr = new GP_Locale(); + $kmr->english_name = 'Kurdish (Kurmanji)'; + $kmr->native_name = 'Kurdî'; + $kmr->lang_code_iso_639_1 = 'ku'; + $kmr->lang_code_iso_639_3 = 'kmr'; + $kmr->country_code = 'tr'; + $kmr->wp_locale = 'kmr'; + $kmr->slug = 'kmr'; + $kmr->facebook_locale = 'ku_TR'; + + $kn = new GP_Locale(); + $kn->english_name = 'Kannada'; + $kn->native_name = 'ಕನ್ನಡ'; + $kn->lang_code_iso_639_1 = 'kn'; + $kn->lang_code_iso_639_2 = 'kan'; + $kn->country_code = 'in'; + $kn->wp_locale = 'kn'; + $kn->slug = 'kn'; + $kn->google_code = 'kn'; + $kn->facebook_locale = 'kn_IN'; + $kn->alphabet = 'kannada'; + + $ko = new GP_Locale(); + $ko->english_name = 'Korean'; + $ko->native_name = '한국어'; + $ko->lang_code_iso_639_1 = 'ko'; + $ko->lang_code_iso_639_2 = 'kor'; + $ko->country_code = 'kr'; + $ko->wp_locale = 'ko_KR'; + $ko->slug = 'ko'; + $ko->nplurals = 1; + $ko->plural_expression = '0'; + $ko->google_code = 'ko'; + $ko->facebook_locale = 'ko_KR'; + $ko->alphabet = 'hangul'; + + $ks = new GP_Locale(); + $ks->english_name = 'Kashmiri'; + $ks->native_name = 'कश्मीरी'; + $ks->lang_code_iso_639_1 = 'ks'; + $ks->lang_code_iso_639_2 = 'kas'; + $ks->slug = 'ks'; + $ks->alphabet = 'devanagari'; + + $kir = new GP_Locale(); + $kir->english_name = 'Kyrgyz'; + $kir->native_name = 'Кыргызча'; + $kir->lang_code_iso_639_1 = 'ky'; + $kir->lang_code_iso_639_2 = 'kir'; + $kir->lang_code_iso_639_3 = 'kir'; + $kir->country_code = 'kg'; + $kir->wp_locale = 'kir'; + $kir->slug = 'kir'; + $kir->nplurals = 1; + $kir->plural_expression = '0'; + $kir->google_code = 'ky'; + $kir->alphabet = 'cyrillic'; + + $la = new GP_Locale(); + $la->english_name = 'Latin'; + $la->native_name = 'Latine'; + $la->lang_code_iso_639_1 = 'la'; + $la->lang_code_iso_639_2 = 'lat'; + $la->slug = 'la'; + $la->google_code = 'la'; + $la->facebook_locale = 'la_VA'; + + $lb = new GP_Locale(); + $lb->english_name = 'Luxembourgish'; + $lb->native_name = 'Lëtzebuergesch'; + $lb->lang_code_iso_639_1 = 'lb'; + $lb->country_code = 'lu'; + $lb->wp_locale = 'lb_LU'; + $lb->slug = 'lb'; + + $li = new GP_Locale(); + $li->english_name = 'Limburgish'; + $li->native_name = 'Limburgs'; + $li->lang_code_iso_639_1 = 'li'; + $li->lang_code_iso_639_2 = 'lim'; + $li->lang_code_iso_639_3 = 'lim'; + $li->country_code = 'nl'; + $li->wp_locale = 'li'; + $li->slug = 'li'; + $li->facebook_locale = 'li_NL'; + + $lij = new GP_Locale(); + $lij->english_name = 'Ligurian'; + $lij->native_name = 'Lìgure'; + $lij->lang_code_iso_639_3 = 'lij'; + $lij->country_code = 'it'; + $lij->wp_locale = 'lij'; + $lij->slug = 'lij'; + + $lin = new GP_Locale(); + $lin->english_name = 'Lingala'; + $lin->native_name = 'Ngala'; + $lin->lang_code_iso_639_1 = 'ln'; + $lin->lang_code_iso_639_2 = 'lin'; + $lin->country_code = 'cd'; + $lin->wp_locale = 'lin'; + $lin->slug = 'lin'; + $lin->nplurals = 2; + $lin->plural_expression = 'n > 1'; + $lin->facebook_locale = 'ln_CD'; + + $lmo = new GP_Locale(); + $lmo->english_name = 'Lombard'; + $lmo->native_name = 'Lombardo'; + $lmo->lang_code_iso_639_3 = 'lmo'; + $lmo->country_code = 'it'; + $lmo->wp_locale = 'lmo'; + $lmo->slug = 'lmo'; + + $lo = new GP_Locale(); + $lo->english_name = 'Lao'; + $lo->native_name = 'ພາສາລາວ'; + $lo->lang_code_iso_639_1 = 'lo'; + $lo->lang_code_iso_639_2 = 'lao'; + $lo->country_code = 'la'; + $lo->wp_locale = 'lo'; + $lo->slug = 'lo'; + $lo->nplurals = 1; + $lo->plural_expression = '0'; + $lo->google_code = 'lo'; + $lo->facebook_locale = 'lo_LA'; + $lo->alphabet = 'lao'; + + $lt = new GP_Locale(); + $lt->english_name = 'Lithuanian'; + $lt->native_name = 'Lietuvių kalba'; + $lt->lang_code_iso_639_1 = 'lt'; + $lt->lang_code_iso_639_2 = 'lit'; + $lt->country_code = 'lt'; + $lt->wp_locale = 'lt_LT'; + $lt->slug = 'lt'; + $lt->nplurals = 3; + $lt->plural_expression = '(n % 10 == 1 && (n % 100 < 11 || n % 100 > 19)) ? 0 : ((n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19)) ? 1 : 2)'; + $lt->google_code = 'lt'; + $lt->facebook_locale = 'lt_LT'; + + $lug = new GP_Locale(); + $lug->english_name = 'Luganda'; + $lug->native_name = 'Oluganda'; + $lug->lang_code_iso_639_1 = 'lg'; + $lug->lang_code_iso_639_2 = 'lug'; + $lug->lang_code_iso_639_3 = 'lug'; + $lug->country_code = 'ug'; + $lug->wp_locale = 'lug'; + $lug->slug = 'lug'; + + $lv = new GP_Locale(); + $lv->english_name = 'Latvian'; + $lv->native_name = 'LatvieÅ¡u valoda'; + $lv->lang_code_iso_639_1 = 'lv'; + $lv->lang_code_iso_639_2 = 'lav'; + $lv->country_code = 'lv'; + $lv->wp_locale = 'lv'; + $lv->slug = 'lv'; + $lv->nplurals = 3; + $lv->plural_expression = '(n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2)'; + $lv->google_code = 'lv'; + $lv->facebook_locale = 'lv_LV'; + + $mai = new GP_Locale(); + $mai->english_name = 'Maithili'; + $mai->native_name = 'मैथिली'; + $mai->lang_code_iso_639_2 = 'mai'; + $mai->lang_code_iso_639_3 = 'mai'; + $mai->country_code = 'in'; + $mai->wp_locale = 'mai'; + $mai->slug = 'mai'; + $mai->alphabet = 'devanagari'; + + $me = new GP_Locale(); + $me->english_name = 'Montenegrin'; + $me->native_name = 'Crnogorski jezik'; + $me->country_code = 'me'; + $me->wp_locale = 'me_ME'; + $me->slug = 'me'; + $me->nplurals = 3; + $me->plural_expression = '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'; + + $mfe = new GP_Locale(); + $mfe->english_name = 'Mauritian Creole'; + $mfe->native_name = 'Kreol Morisien'; + $mfe->lang_code_iso_639_3 = 'mfe'; + $mfe->country_code = 'mu'; + $mfe->wp_locale = 'mfe'; + $mfe->slug = 'mfe'; + $mfe->nplurals = 1; + $mfe->plural_expression = '0'; + + $mg = new GP_Locale(); + $mg->english_name = 'Malagasy'; + $mg->native_name = 'Malagasy'; + $mg->lang_code_iso_639_1 = 'mg'; + $mg->lang_code_iso_639_2 = 'mlg'; + $mg->country_code = 'mg'; + $mg->wp_locale = 'mg_MG'; + $mg->slug = 'mg'; + $mg->google_code = 'mg'; + $mg->facebook_locale = 'mg_MG'; + + $mhr = new GP_Locale(); + $mhr->english_name = 'Mari (Meadow)'; + $mhr->native_name = 'Олык марий'; + $mhr->lang_code_iso_639_3 = 'mhr'; + $mhr->country_code = 'ru'; + $mhr->slug = 'mhr'; + $mhr->alphabet = 'cyrillic'; + + $mk = new GP_Locale(); + $mk->english_name = 'Macedonian'; + $mk->native_name = 'Македонски јазик'; + $mk->lang_code_iso_639_1 = 'mk'; + $mk->lang_code_iso_639_2 = 'mkd'; + $mk->country_code = 'mk'; + $mk->wp_locale = 'mk_MK'; + $mk->slug = 'mk'; + $mk->nplurals = 2; + $mk->plural_expression = 'n % 10 != 1 || n % 100 == 11'; + $mk->google_code = 'mk'; + $mk->facebook_locale = 'mk_MK'; + $mk->alphabet = 'cyrillic'; + + $ml = new GP_Locale(); + $ml->english_name = 'Malayalam'; + $ml->native_name = 'മലയാളം'; + $ml->lang_code_iso_639_1 = 'ml'; + $ml->lang_code_iso_639_2 = 'mal'; + $ml->country_code = 'in'; + $ml->wp_locale = 'ml_IN'; + $ml->slug = 'ml'; + $ml->google_code = 'ml'; + $ml->facebook_locale = 'ml_IN'; + $ml->alphabet = 'malayalam'; + + $mlt = new GP_Locale(); + $mlt->english_name = 'Maltese'; + $mlt->native_name = 'Malti'; + $mlt->lang_code_iso_639_1 = 'mt'; + $mlt->lang_code_iso_639_2 = 'mlt'; + $mlt->lang_code_iso_639_3 = 'mlt'; + $mlt->country_code = 'mt'; + $mlt->wp_locale = 'mlt'; + $mlt->slug = 'mlt'; + $mlt->nplurals = 4; + $mlt->plural_expression = '(n == 1) ? 0 : ((n == 0 || n % 100 >= 2 && n % 100 <= 10) ? 1 : ((n % 100 >= 11 && n % 100 <= 19) ? 2 : 3))'; + $mlt->google_code = 'mt'; + $mlt->facebook_locale = 'mt_MT'; + + $mn = new GP_Locale(); + $mn->english_name = 'Mongolian'; + $mn->native_name = 'Монгол'; + $mn->lang_code_iso_639_1 = 'mn'; + $mn->lang_code_iso_639_2 = 'mon'; + $mn->country_code = 'mn'; + $mn->wp_locale = 'mn'; + $mn->slug = 'mn'; + $mn->google_code = 'mn'; + $mn->facebook_locale = 'mn_MN'; + $mn->alphabet = 'cyrillic'; + + $mr = new GP_Locale(); + $mr->english_name = 'Marathi'; + $mr->native_name = 'मराठी'; + $mr->lang_code_iso_639_1 = 'mr'; + $mr->lang_code_iso_639_2 = 'mar'; + $mr->wp_locale = 'mr'; + $mr->slug = 'mr'; + $mr->google_code = 'mr'; + $mr->facebook_locale = 'mr_IN'; + $mr->alphabet = 'devanagari'; + + $mri = new GP_Locale(); + $mri->english_name = 'Maori'; + $mri->native_name = 'Te Reo Māori'; + $mri->lang_code_iso_639_1 = 'mi'; + $mri->lang_code_iso_639_3 = 'mri'; + $mri->country_code = 'nz'; + $mri->slug = 'mri'; + $mri->wp_locale = 'mri'; + $mri->nplurals = 2; + $mri->plural_expression = 'n > 1'; + $mri->google_code = 'mi'; + + $mrj = new GP_Locale(); + $mrj->english_name = 'Mari (Hill)'; + $mrj->native_name = 'Кырык мары'; + $mrj->lang_code_iso_639_3 = 'mrj'; + $mrj->country_code = 'ru'; + $mrj->slug = 'mrj'; + $mrj->alphabet = 'cyrillic'; + + $ms = new GP_Locale(); + $ms->english_name = 'Malay'; + $ms->native_name = 'Bahasa Melayu'; + $ms->lang_code_iso_639_1 = 'ms'; + $ms->lang_code_iso_639_2 = 'msa'; + $ms->wp_locale = 'ms_MY'; + $ms->slug = 'ms'; + $ms->nplurals = 1; + $ms->plural_expression = '0'; + $ms->google_code = 'ms'; + $ms->facebook_locale = 'ms_MY'; + + $mwl = new GP_Locale(); + $mwl->english_name = 'Mirandese'; + $mwl->native_name = 'Mirandés'; + $mwl->lang_code_iso_639_2 = 'mwl'; + $mwl->slug = 'mwl'; + + $my = new GP_Locale(); + $my->english_name = 'Myanmar (Burmese)'; + $my->native_name = 'ဗမာစာ'; + $my->lang_code_iso_639_1 = 'my'; + $my->lang_code_iso_639_2 = 'mya'; + $my->country_code = 'mm'; + $my->wp_locale = 'my_MM'; + $my->slug = 'mya'; + $my->google_code = 'my'; + $my->alphabet = 'burmese'; + + $ne = new GP_Locale(); + $ne->english_name = 'Nepali'; + $ne->native_name = 'नेपाली'; + $ne->lang_code_iso_639_1 = 'ne'; + $ne->lang_code_iso_639_2 = 'nep'; + $ne->country_code = 'np'; + $ne->wp_locale = 'ne_NP'; + $ne->slug = 'ne'; + $ne->google_code = 'ne'; + $ne->facebook_locale = 'ne_NP'; + $ne->alphabet = 'devanagari'; + + $nb = new GP_Locale(); + $nb->english_name = 'Norwegian (BokmÃ¥l)'; + $nb->native_name = 'Norsk bokmÃ¥l'; + $nb->lang_code_iso_639_1 = 'nb'; + $nb->lang_code_iso_639_2 = 'nob'; + $nb->country_code = 'no'; + $nb->wp_locale = 'nb_NO'; + $nb->slug = 'nb'; + $nb->google_code = 'no'; + $nb->facebook_locale = 'nb_NO'; + + $nl = new GP_Locale(); + $nl->english_name = 'Dutch'; + $nl->native_name = 'Nederlands'; + $nl->lang_code_iso_639_1 = 'nl'; + $nl->lang_code_iso_639_2 = 'nld'; + $nl->country_code = 'nl'; + $nl->wp_locale = 'nl_NL'; + $nl->slug = 'nl'; + $nl->google_code = 'nl'; + $nl->facebook_locale = 'nl_NL'; + + $nl_formal = clone $nl; + $nl_formal->english_name = 'Dutch (Formal)'; + $nl_formal->native_name = 'Nederlands (Formeel)'; + $nl_formal->slug = 'nl/formal'; + $nl_formal->wp_locale = 'nl_NL_formal'; + $nl_formal->root_slug = $nl->slug; + + $nl_be = new GP_Locale(); + $nl_be->english_name = 'Dutch (Belgium)'; + $nl_be->native_name = 'Nederlands (België)'; + $nl_be->lang_code_iso_639_1 = 'nl'; + $nl_be->lang_code_iso_639_2 = 'nld'; + $nl_be->country_code = 'be'; + $nl_be->wp_locale = 'nl_BE'; + $nl_be->slug = 'nl-be'; + $nl_be->google_code = 'nl'; + + $no = new GP_Locale(); + $no->english_name = 'Norwegian'; + $no->native_name = 'Norsk'; + $no->lang_code_iso_639_1 = 'no'; + $no->lang_code_iso_639_2 = 'nor'; + $no->country_code = 'no'; + $no->slug = 'no'; + $no->google_code = 'no'; + + $nn = new GP_Locale(); + $nn->english_name = 'Norwegian (Nynorsk)'; + $nn->native_name = 'Norsk nynorsk'; + $nn->lang_code_iso_639_1 = 'nn'; + $nn->lang_code_iso_639_2 = 'nno'; + $nn->country_code = 'no'; + $nn->wp_locale = 'nn_NO'; + $nn->slug = 'nn'; + $nn->google_code = 'no'; + $nn->facebook_locale = 'nn_NO'; + + $nqo = new GP_Locale(); + $nqo->english_name = 'N’ko'; + $nqo->native_name = 'ߒߞߏ'; + $nqo->lang_code_iso_639_2 = 'nqo'; + $nqo->lang_code_iso_639_3 = 'nqo'; + $nqo->country_code = 'gn'; + $nqo->wp_locale = 'nqo'; + $nqo->slug = 'nqo'; + $nqo->text_direction = 'rtl'; + $nqo->alphabet = 'nko'; + + $nso = new GP_Locale(); + $nso->english_name = 'Northern Sotho'; + $nso->native_name = 'Sesotho sa Leboa'; + $nso->lang_code_iso_639_2 = 'nso'; + $nso->lang_code_iso_639_3 = 'nso'; + $nso->country_code = 'za'; + $nso->slug = 'nso'; + + $oci = new GP_Locale(); + $oci->english_name = 'Occitan'; + $oci->native_name = 'Occitan'; + $oci->lang_code_iso_639_1 = 'oc'; + $oci->lang_code_iso_639_2 = 'oci'; + $oci->country_code = 'fr'; + $oci->wp_locale = 'oci'; + $oci->slug = 'oci'; + $oci->nplurals = 2; + $oci->plural_expression = 'n > 1'; + + $orm = new GP_Locale(); + $orm->english_name = 'Oromo'; + $orm->native_name = 'Afaan Oromo'; + $orm->lang_code_iso_639_1 = 'om'; + $orm->lang_code_iso_639_2 = 'orm'; + $orm->lang_code_iso_639_3 = 'orm'; + $orm->slug = 'orm'; + $orm->plural_expression = 'n > 1'; + + $ory = new GP_Locale(); + $ory->english_name = 'Oriya'; + $ory->native_name = 'ଓଡ଼ିଆ'; + $ory->lang_code_iso_639_1 = 'or'; + $ory->lang_code_iso_639_2 = 'ory'; + $ory->country_code = 'in'; + $ory->wp_locale = 'ory'; + $ory->slug = 'ory'; + $ory->facebook_locale = 'or_IN'; + $ory->alphabet = 'odia'; + + $os = new GP_Locale(); + $os->english_name = 'Ossetic'; + $os->native_name = 'Ирон'; + $os->lang_code_iso_639_1 = 'os'; + $os->lang_code_iso_639_2 = 'oss'; + $os->wp_locale = 'os'; + $os->slug = 'os'; + $os->alphabet = 'cyrillic'; + + $pa = new GP_Locale(); + $pa->english_name = 'Panjabi (India)'; + $pa->native_name = 'ਪੰਜਾਬੀ'; + $pa->lang_code_iso_639_1 = 'pa'; + $pa->lang_code_iso_639_2 = 'pan'; + $pa->country_code = 'in'; + $pa->wp_locale = 'pa_IN'; + $pa->slug = 'pa'; + $pa->google_code = 'pa'; + $pa->nplurals = 2; + $pa->plural_expression = 'n > 1'; + $pa->facebook_locale = 'pa_IN'; + $pa->alphabet = 'gurmukhi'; + + $pa_pk = new GP_Locale(); + $pa_pk->english_name = 'Punjabi (Pakistan)'; + $pa_pk->native_name = 'پنجابی'; + $pa_pk->lang_code_iso_639_1 = 'pa'; + $pa_pk->lang_code_iso_639_2 = 'pan'; + $pa_pk->country_code = 'pk'; + $pa_pk->wp_locale = 'pa_PK'; + $pa_pk->slug = 'pa-pk'; + $pa_pk->nplurals = 2; + $pa_pk->plural_expression = 'n > 1'; + $pa_pk->google_code = 'pa'; + $pa_pk->alphabet = 'shahmukhi'; + + $pap_cw = new GP_Locale(); + $pap_cw->english_name = 'Papiamento (Curaçao and Bonaire)'; + $pap_cw->native_name = 'Papiamentu'; + $pap_cw->lang_code_iso_639_2 = 'pap'; + $pap_cw->lang_code_iso_639_3 = 'pap'; + $pap_cw->country_code = 'cw'; + $pap_cw->wp_locale = 'pap_CW'; + $pap_cw->slug = 'pap-cw'; + + $pap_aw = new GP_Locale(); + $pap_aw->english_name = 'Papiamento (Aruba)'; + $pap_aw->native_name = 'Papiamento'; + $pap_aw->lang_code_iso_639_2 = 'pap'; + $pap_aw->lang_code_iso_639_3 = 'pap'; + $pap_aw->country_code = 'aw'; + $pap_aw->wp_locale = 'pap_AW'; + $pap_aw->slug = 'pap-aw'; + + $pcd = new GP_Locale(); + $pcd->english_name = 'Picard'; + $pcd->native_name = 'Ch’ti'; + $pcd->lang_code_iso_639_3 = 'pcd'; + $pcd->country_code = 'fr'; + $pcd->wp_locale = 'pcd'; + $pcd->slug = 'pcd'; + $pcd->nplurals = 2; + $pcd->plural_expression = 'n > 1'; + + $pcm = new GP_Locale(); + $pcm->english_name = 'Nigerian Pidgin'; + $pcm->native_name = 'Nigerian Pidgin'; + $pcm->lang_code_iso_639_3 = 'pcm'; + $pcm->country_code = 'ng'; + $pcm->wp_locale = 'pcm'; + $pcm->slug = 'pcm'; + + $pirate = new GP_Locale(); + $pirate->english_name = 'English (Pirate)'; + $pirate->native_name = 'English (Pirate)'; + $pirate->lang_code_iso_639_2 = 'art'; + $pirate->wp_locale = 'art_xpirate'; + $pirate->slug = 'pirate'; + $pirate->google_code = 'xx-pirate'; + $pirate->facebook_locale = 'en_PI'; + + $pl = new GP_Locale(); + $pl->english_name = 'Polish'; + $pl->native_name = 'Polski'; + $pl->lang_code_iso_639_1 = 'pl'; + $pl->lang_code_iso_639_2 = 'pol'; + $pl->country_code = 'pl'; + $pl->wp_locale = 'pl_PL'; + $pl->slug = 'pl'; + $pl->nplurals = 3; + $pl->plural_expression = '(n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $pl->google_code = 'pl'; + $pl->facebook_locale = 'pl_PL'; + + $pt = new GP_Locale(); + $pt->english_name = 'Portuguese (Portugal)'; + $pt->native_name = 'Português'; + $pt->lang_code_iso_639_1 = 'pt'; + $pt->country_code = 'pt'; + $pt->wp_locale = 'pt_PT'; + $pt->slug = 'pt'; + $pt->google_code = 'pt-PT'; + $pt->facebook_locale = 'pt_PT'; + + $pt_ao90 = clone $pt; + $pt_ao90->english_name = 'Portuguese (Portugal, AO90)'; + $pt_ao90->native_name = 'Português (AO90)'; + $pt_ao90->slug = 'pt/ao90'; + $pt_ao90->wp_locale = 'pt_PT_ao90'; + $pt_ao90->root_slug = $pt->slug; + + $pt_ao = new GP_Locale(); + $pt_ao->english_name = 'Portuguese (Angola)'; + $pt_ao->native_name = 'Português de Angola'; + $pt_ao->lang_code_iso_639_1 = 'pt'; + $pt_ao->country_code = 'ao'; + $pt_ao->wp_locale = 'pt_AO'; + $pt_ao->slug = 'pt-ao'; + + $pt_br = new GP_Locale(); + $pt_br->english_name = 'Portuguese (Brazil)'; + $pt_br->native_name = 'Português do Brasil'; + $pt_br->lang_code_iso_639_1 = 'pt'; + $pt_br->lang_code_iso_639_2 = 'por'; + $pt_br->country_code = 'br'; + $pt_br->wp_locale = 'pt_BR'; + $pt_br->slug = 'pt-br'; + $pt_br->nplurals = 2; + $pt_br->plural_expression = 'n > 1'; + $pt_br->google_code = 'pt-BR'; + $pt_br->facebook_locale = 'pt_BR'; + + $ps = new GP_Locale(); + $ps->english_name = 'Pashto'; + $ps->native_name = 'پښتو'; + $ps->lang_code_iso_639_1 = 'ps'; + $ps->lang_code_iso_639_2 = 'pus'; + $ps->country_code = 'af'; + $ps->wp_locale = 'ps'; + $ps->slug = 'ps'; + $ps->text_direction = 'rtl'; + $ps->facebook_locale = 'ps_AF'; + $ps->alphabet = 'pashto'; + + $rhg = new GP_Locale(); + $rhg->english_name = 'Rohingya'; + $rhg->native_name = 'Ruáinga'; + $rhg->lang_code_iso_639_3 = 'rhg'; + $rhg->country_code = 'mm'; + $rhg->wp_locale = 'rhg'; + $rhg->slug = 'rhg'; + $rhg->nplurals = 1; + $rhg->plural_expression = '0'; + + $rif = new GP_Locale(); + $rif->english_name = 'Tarifit'; + $rif->native_name = 'Tarifiyt'; + $rif->lang_code_iso_639_3 = 'rif'; + $rif->country_code = 'ma'; + $rif->slug = 'rif'; + + $ro = new GP_Locale(); + $ro->english_name = 'Romanian'; + $ro->native_name = 'Română'; + $ro->lang_code_iso_639_1 = 'ro'; + $ro->lang_code_iso_639_2 = 'ron'; + $ro->country_code = 'ro'; + $ro->wp_locale = 'ro_RO'; + $ro->slug = 'ro'; + $ro->nplurals = 3; + $ro->plural_expression = '(n == 1) ? 0 : ((n == 0 || n % 100 >= 2 && n % 100 <= 19) ? 1 : 2)'; + $ro->google_code = 'ro'; + $ro->facebook_locale = 'ro_RO'; + + $roh = new GP_Locale(); + $roh->english_name = 'Romansh'; + $roh->native_name = 'Rumantsch'; + $roh->lang_code_iso_639_1 = 'rm'; + $roh->lang_code_iso_639_2 = 'roh'; + $roh->lang_code_iso_639_3 = 'roh'; + $roh->country_code = 'ch'; + $roh->wp_locale = 'roh'; + $roh->slug = 'roh'; + + $ru = new GP_Locale(); + $ru->english_name = 'Russian'; + $ru->native_name = 'Русский'; + $ru->lang_code_iso_639_1 = 'ru'; + $ru->lang_code_iso_639_2 = 'rus'; + $ru->country_code = 'ru'; + $ru->wp_locale = 'ru_RU'; + $ru->slug = 'ru'; + $ru->nplurals = 3; + $ru->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $ru->google_code = 'ru'; + $ru->facebook_locale = 'ru_RU'; + $ru->alphabet = 'cyrillic'; + + $rue = new GP_Locale(); + $rue->english_name = 'Rusyn'; + $rue->native_name = 'Русиньскый'; + $rue->lang_code_iso_639_3 = 'rue'; + $rue->slug = 'rue'; + $rue->nplurals = 3; + $rue->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $rue->alphabet = 'cyrillic'; + + $rup = new GP_Locale(); + $rup->english_name = 'Aromanian'; + $rup->native_name = 'Armãneashce'; + $rup->lang_code_iso_639_2 = 'rup'; + $rup->lang_code_iso_639_3 = 'rup'; + $rup->country_code = 'mk'; + $rup->slug = 'rup'; + + $sah = new GP_Locale(); + $sah->english_name = 'Sakha'; + $sah->native_name = 'Сахалыы'; + $sah->lang_code_iso_639_2 = 'sah'; + $sah->lang_code_iso_639_3 = 'sah'; + $sah->country_code = 'ru'; + $sah->wp_locale = 'sah'; + $sah->slug = 'sah'; + $sah->alphabet = 'cyrillic'; + + $sa_in = new GP_Locale(); + $sa_in->english_name = 'Sanskrit'; + $sa_in->native_name = 'भारतम्'; + $sa_in->lang_code_iso_639_1 = 'sa'; + $sa_in->lang_code_iso_639_2 = 'san'; + $sa_in->lang_code_iso_639_3 = 'san'; + $sa_in->country_code = 'in'; + $sa_in->wp_locale = 'sa_IN'; + $sa_in->slug = 'sa-in'; + $sa_in->facebook_locale = 'sa_IN'; + $sa_in->alphabet = 'brahmic'; + + $scn = new GP_Locale(); + $scn->english_name = 'Sicilian'; + $scn->native_name = 'Sicilianu'; + $scn->lang_code_iso_639_3 = 'scn'; + $scn->country_code = 'it'; + $scn->wp_locale = 'scn'; + $scn->slug = 'scn'; + + $si = new GP_Locale(); + $si->english_name = 'Sinhala'; + $si->native_name = 'සිංහල'; + $si->lang_code_iso_639_1 = 'si'; + $si->lang_code_iso_639_2 = 'sin'; + $si->country_code = 'lk'; + $si->wp_locale = 'si_LK'; + $si->slug = 'si'; + $si->google_code = 'si'; + $si->facebook_locale = 'si_LK'; + $si->alphabet = 'sinhala'; + + $sk = new GP_Locale(); + $sk->english_name = 'Slovak'; + $sk->native_name = 'Slovenčina'; + $sk->lang_code_iso_639_1 = 'sk'; + $sk->lang_code_iso_639_2 = 'slk'; + $sk->country_code = 'sk'; + $sk->slug = 'sk'; + $sk->wp_locale = 'sk_SK'; + $sk->nplurals = 3; + $sk->plural_expression = '(n == 1) ? 0 : ((n >= 2 && n <= 4) ? 1 : 2)'; + $sk->google_code = 'sk'; + $sk->facebook_locale = 'sk_SK'; + + $skr = new GP_Locale(); + $skr->english_name = 'Saraiki'; + $skr->native_name = 'سرائیکی'; + $skr->lang_code_iso_639_3 = 'skr'; + $skr->country_code = 'pk'; + $skr->wp_locale = 'skr'; + $skr->slug = 'skr'; + $skr->text_direction = 'rtl'; + $skr->alphabet = 'saraiki'; + + $sl = new GP_Locale(); + $sl->english_name = 'Slovenian'; + $sl->native_name = 'Slovenščina'; + $sl->lang_code_iso_639_1 = 'sl'; + $sl->lang_code_iso_639_2 = 'slv'; + $sl->country_code = 'si'; + $sl->wp_locale = 'sl_SI'; + $sl->slug = 'sl'; + $sl->nplurals = 4; + $sl->plural_expression = '(n % 100 == 1) ? 0 : ((n % 100 == 2) ? 1 : ((n % 100 == 3 || n % 100 == 4) ? 2 : 3))'; + $sl->google_code = 'sl'; + $sl->facebook_locale = 'sl_SI'; + + $sna = new GP_Locale(); + $sna->english_name = 'Shona'; + $sna->native_name = 'ChiShona'; + $sna->lang_code_iso_639_1 = 'sn'; + $sna->lang_code_iso_639_3 = 'sna'; + $sna->country_code = 'zw'; + $sna->wp_locale = 'sna'; + $sna->slug = 'sna'; + + $snd = new GP_Locale(); + $snd->english_name = 'Sindhi'; + $snd->native_name = 'سنڌي'; + $snd->lang_code_iso_639_1 = 'sd'; + $snd->lang_code_iso_639_2 = 'snd'; + $snd->lang_code_iso_639_3 = 'snd'; + $snd->country_code = 'pk'; + $snd->wp_locale = 'snd'; + $snd->slug = 'snd'; + $snd->text_direction = 'rtl'; + $snd->alphabet = 'arabic'; + + $so = new GP_Locale(); + $so->english_name = 'Somali'; + $so->native_name = 'Afsoomaali'; + $so->lang_code_iso_639_1 = 'so'; + $so->lang_code_iso_639_2 = 'som'; + $so->lang_code_iso_639_3 = 'som'; + $so->country_code = 'so'; + $so->wp_locale = 'so_SO'; + $so->slug = 'so'; + $so->google_code = 'so'; + $so->facebook_locale = 'so_SO'; + + $sq = new GP_Locale(); + $sq->english_name = 'Albanian'; + $sq->native_name = 'Shqip'; + $sq->lang_code_iso_639_1 = 'sq'; + $sq->lang_code_iso_639_2 = 'sqi'; + $sq->wp_locale = 'sq'; + $sq->country_code = 'al'; + $sq->slug = 'sq'; + $sq->google_code = 'sq'; + $sq->facebook_locale = 'sq_AL'; + + $sq_xk = new GP_Locale(); + $sq_xk->english_name = 'Shqip (Kosovo)'; + $sq_xk->native_name = 'Për Kosovën Shqip'; + $sq_xk->lang_code_iso_639_1 = 'sq'; + $sq_xk->country_code = 'xk'; // Temporary country code until Kosovo is assigned an ISO code. + $sq_xk->wp_locale = 'sq_XK'; + $sq_xk->slug = 'sq-xk'; + + $sr = new GP_Locale(); + $sr->english_name = 'Serbian'; + $sr->native_name = 'Српски језик'; + $sr->lang_code_iso_639_1 = 'sr'; + $sr->lang_code_iso_639_2 = 'srp'; + $sr->country_code = 'rs'; + $sr->wp_locale = 'sr_RS'; + $sr->slug = 'sr'; + $sr->nplurals = 3; + $sr->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $sr->google_code = 'sr'; + $sr->facebook_locale = 'sr_RS'; + $sr->alphabet = 'cyrillic'; + + $sr_latin = clone $sr; + $sr_latin->english_name = 'Serbian (Latin)'; + $sr_latin->native_name = 'Srpski jezik'; + $sr_latin->slug = 'sr/latin'; + $sr_latin->wp_locale = 'sr_RS_latin'; + $sr_latin->root_slug = $sr->slug; + + $srd = new GP_Locale(); + $srd->english_name = 'Sardinian'; + $srd->native_name = 'Sardu'; + $srd->lang_code_iso_639_1 = 'sc'; + $srd->lang_code_iso_639_2 = 'srd'; + $srd->country_code = 'it'; + $srd->wp_locale = 'srd'; + $srd->slug = 'srd'; + $srd->facebook_locale = 'sc_IT'; + + $ssw = new GP_Locale(); + $ssw->english_name = 'Swati'; + $ssw->native_name = 'SiSwati'; + $ssw->lang_code_iso_639_1 = 'ss'; + $ssw->lang_code_iso_639_2 = 'ssw'; + $ssw->lang_code_iso_639_3 = 'ssw'; + $ssw->country_code = 'sz'; + $ssw->wp_locale = 'ssw'; + $ssw->slug = 'ssw'; + + $su = new GP_Locale(); + $su->english_name = 'Sundanese'; + $su->native_name = 'Basa Sunda'; + $su->lang_code_iso_639_1 = 'su'; + $su->lang_code_iso_639_2 = 'sun'; + $su->country_code = 'id'; + $su->wp_locale = 'su_ID'; + $su->slug = 'su'; + $su->nplurals = 1; + $su->plural_expression = '0'; + $su->google_code = 'su'; + + $sv = new GP_Locale(); + $sv->english_name = 'Swedish'; + $sv->native_name = 'Svenska'; + $sv->lang_code_iso_639_1 = 'sv'; + $sv->lang_code_iso_639_2 = 'swe'; + $sv->country_code = 'se'; + $sv->wp_locale = 'sv_SE'; + $sv->slug = 'sv'; + $sv->google_code = 'sv'; + $sv->facebook_locale = 'sv_SE'; + + $sw = new GP_Locale(); + $sw->english_name = 'Swahili'; + $sw->native_name = 'Kiswahili'; + $sw->lang_code_iso_639_1 = 'sw'; + $sw->lang_code_iso_639_2 = 'swa'; + $sw->wp_locale = 'sw'; + $sw->slug = 'sw'; + $sw->google_code = 'sw'; + $sw->facebook_locale = 'sw_KE'; + + $syr = new GP_Locale(); + $syr->english_name = 'Syriac'; + $syr->native_name = 'Syriac'; + $syr->lang_code_iso_639_3 = 'syr'; + $syr->country_code = 'iq'; + $syr->wp_locale = 'syr'; + $syr->slug = 'syr'; + $syr->alphabet = 'syriac'; + + $szl = new GP_Locale(); + $szl->english_name = 'Silesian'; + $szl->native_name = 'Åšlōnskŏ gŏdka'; + $szl->lang_code_iso_639_3 = 'szl'; + $szl->country_code = 'pl'; + $szl->wp_locale = 'szl'; + $szl->slug = 'szl'; + $szl->nplurals = 3; + $szl->plural_expression = '(n==1 ? 0 : n%10>=2 && n%10<=4 && n%100==20 ? 1 : 2)'; + $szl->facebook_locale = 'sz_PL'; + + $ta = new GP_Locale(); + $ta->english_name = 'Tamil'; + $ta->native_name = 'தமிழ்'; + $ta->lang_code_iso_639_1 = 'ta'; + $ta->lang_code_iso_639_2 = 'tam'; + $ta->country_code = 'in'; + $ta->wp_locale = 'ta_IN'; + $ta->slug = 'ta'; + $ta->google_code = 'ta'; + $ta->facebook_locale = 'ta_IN'; + $ta->alphabet = 'tamil'; + + $ta_lk = new GP_Locale(); + $ta_lk->english_name = 'Tamil (Sri Lanka)'; + $ta_lk->native_name = 'தமிழ்'; + $ta_lk->lang_code_iso_639_1 = 'ta'; + $ta_lk->lang_code_iso_639_2 = 'tam'; + $ta_lk->country_code = 'lk'; + $ta_lk->wp_locale = 'ta_LK'; + $ta_lk->slug = 'ta-lk'; + $ta_lk->google_code = 'ta'; + $ta_lk->alphabet = 'tamil'; + + $tah = new GP_Locale(); + $tah->english_name = 'Tahitian'; + $tah->native_name = 'Reo Tahiti'; + $tah->lang_code_iso_639_1 = 'ty'; + $tah->lang_code_iso_639_2 = 'tah'; + $tah->lang_code_iso_639_3 = 'tah'; + $tah->country_code = 'pf'; + $tah->wp_locale = 'tah'; + $tah->slug = 'tah'; + $tah->nplurals = 2; + $tah->plural_expression = 'n > 1'; + + $te = new GP_Locale(); + $te->english_name = 'Telugu'; + $te->native_name = 'తెలుగు'; + $te->lang_code_iso_639_1 = 'te'; + $te->lang_code_iso_639_2 = 'tel'; + $te->wp_locale = 'te'; + $te->slug = 'te'; + $te->google_code = 'te'; + $te->facebook_locale = 'te_IN'; + $te->alphabet = 'telugu'; + + $tg = new GP_Locale(); + $tg->english_name = 'Tajik'; + $tg->native_name = 'Тоҷикӣ'; + $tg->lang_code_iso_639_1 = 'tg'; + $tg->lang_code_iso_639_2 = 'tgk'; + $tg->country_code = 'tj'; + $tg->wp_locale = 'tg'; + $tg->slug = 'tg'; + $tg->google_code = 'tg'; + $tg->facebook_locale = 'tg_TJ'; + $tg->alphabet = 'cyrillic'; + + $th = new GP_Locale(); + $th->english_name = 'Thai'; + $th->native_name = 'ไทย'; + $th->lang_code_iso_639_1 = 'th'; + $th->lang_code_iso_639_2 = 'tha'; + $th->wp_locale = 'th'; + $th->slug = 'th'; + $th->nplurals = 1; + $th->plural_expression = '0'; + $th->google_code = 'th'; + $th->facebook_locale = 'th_TH'; + $th->alphabet = 'thai'; + $th->word_count_type = 'characters_excluding_spaces'; + + $tir = new GP_Locale(); + $tir->english_name = 'Tigrinya'; + $tir->native_name = 'ትግርኛ'; + $tir->lang_code_iso_639_1 = 'ti'; + $tir->lang_code_iso_639_2 = 'tir'; + $tir->country_code = 'er'; + $tir->wp_locale = 'tir'; + $tir->slug = 'tir'; + $tir->nplurals = 1; + $tir->plural_expression = '0'; + $tir->alphabet = 'geez'; + + $tlh = new GP_Locale(); + $tlh->english_name = 'Klingon'; + $tlh->native_name = 'TlhIngan'; + $tlh->lang_code_iso_639_2 = 'tlh'; + $tlh->slug = 'tlh'; + $tlh->nplurals = 1; + $tlh->plural_expression = '0'; + $tlh->facebook_locale = 'tl_ST'; + + $tl = new GP_Locale(); + $tl->english_name = 'Tagalog'; + $tl->native_name = 'Tagalog'; + $tl->lang_code_iso_639_1 = 'tl'; + $tl->lang_code_iso_639_2 = 'tgl'; + $tl->country_code = 'ph'; + $tl->wp_locale = 'tl'; + $tl->slug = 'tl'; + $tl->google_code = 'tl'; + $tl->facebook_locale = 'tl_PH'; + + $tr = new GP_Locale(); + $tr->english_name = 'Turkish'; + $tr->native_name = 'Türkçe'; + $tr->lang_code_iso_639_1 = 'tr'; + $tr->lang_code_iso_639_2 = 'tur'; + $tr->country_code = 'tr'; + $tr->wp_locale = 'tr_TR'; + $tr->slug = 'tr'; + $tr->nplurals = 2; + $tr->plural_expression = 'n > 1'; + $tr->google_code = 'tr'; + $tr->facebook_locale = 'tr_TR'; + + $tt_ru = new GP_Locale(); + $tt_ru->english_name = 'Tatar'; + $tt_ru->native_name = 'Татар теле'; + $tt_ru->lang_code_iso_639_1 = 'tt'; + $tt_ru->lang_code_iso_639_2 = 'tat'; + $tt_ru->country_code = 'ru'; + $tt_ru->wp_locale = 'tt_RU'; + $tt_ru->slug = 'tt'; + $tt_ru->nplurals = 1; + $tt_ru->plural_expression = '0'; + $tt_ru->facebook_locale = 'tt_RU'; + $tt_ru->alphabet = 'cyrillic'; + + $tuk = new GP_Locale(); + $tuk->english_name = 'Turkmen'; + $tuk->native_name = 'Türkmençe'; + $tuk->lang_code_iso_639_1 = 'tk'; + $tuk->lang_code_iso_639_2 = 'tuk'; + $tuk->country_code = 'tm'; + $tuk->wp_locale = 'tuk'; + $tuk->slug = 'tuk'; + $tuk->nplurals = 2; + $tuk->plural_expression = 'n > 1'; + $tuk->facebook_locale = 'tk_TM'; + + $twd = new GP_Locale(); + $twd->english_name = 'Tweants'; + $twd->native_name = 'Twents'; + $twd->lang_code_iso_639_3 = 'twd'; + $twd->country_code = 'nl'; + $twd->wp_locale = 'twd'; + $twd->slug = 'twd'; + + $tzm = new GP_Locale(); + $tzm->english_name = 'Tamazight (Central Atlas)'; + $tzm->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; + $tzm->lang_code_iso_639_2 = 'tzm'; + $tzm->country_code = 'ma'; + $tzm->wp_locale = 'tzm'; + $tzm->slug = 'tzm'; + $tzm->nplurals = 2; + $tzm->plural_expression = 'n > 1'; + $tzm->alphabet = 'tifinagh'; + + $udm = new GP_Locale(); + $udm->english_name = 'Udmurt'; + $udm->native_name = 'Удмурт кыл'; + $udm->lang_code_iso_639_2 = 'udm'; + $udm->slug = 'udm'; + $udm->alphabet = 'cyrillic'; + + $ug = new GP_Locale(); + $ug->english_name = 'Uighur'; + $ug->native_name = 'ئۇيغۇرچە'; + $ug->lang_code_iso_639_1 = 'ug'; + $ug->lang_code_iso_639_2 = 'uig'; + $ug->country_code = 'cn'; + $ug->wp_locale = 'ug_CN'; + $ug->slug = 'ug'; + $ug->text_direction = 'rtl'; + $ug->alphabet = 'uyghur'; + + $uk = new GP_Locale(); + $uk->english_name = 'Ukrainian'; + $uk->native_name = 'Українська'; + $uk->lang_code_iso_639_1 = 'uk'; + $uk->lang_code_iso_639_2 = 'ukr'; + $uk->country_code = 'ua'; + $uk->wp_locale = 'uk'; + $uk->slug = 'uk'; + $uk->nplurals = 3; + $uk->plural_expression = '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : 2)'; + $uk->google_code = 'uk'; + $uk->facebook_locale = 'uk_UA'; + $uk->alphabet = 'cyrillic'; + + $ur = new GP_Locale(); + $ur->english_name = 'Urdu'; + $ur->native_name = 'اردو'; + $ur->lang_code_iso_639_1 = 'ur'; + $ur->lang_code_iso_639_2 = 'urd'; + $ur->country_code = 'pk'; + $ur->wp_locale = 'ur'; + $ur->slug = 'ur'; + $ur->text_direction = 'rtl'; + $ur->google_code = 'ur'; + $ur->facebook_locale = 'ur_PK'; + $ur->alphabet = 'persian'; + + $uz = new GP_Locale(); + $uz->english_name = 'Uzbek'; + $uz->native_name = 'O‘zbekcha'; + $uz->lang_code_iso_639_1 = 'uz'; + $uz->lang_code_iso_639_2 = 'uzb'; + $uz->country_code = 'uz'; + $uz->wp_locale = 'uz_UZ'; + $uz->slug = 'uz'; + $uz->nplurals = 1; + $uz->plural_expression = '0'; + $uz->google_code = 'uz'; + $uz->facebook_locale = 'uz_UZ'; + + $vec = new GP_Locale(); + $vec->english_name = 'Venetian'; + $vec->native_name = 'Vèneto'; + $vec->lang_code_iso_639_2 = 'roa'; + $vec->lang_code_iso_639_3 = 'vec'; + $vec->country_code = 'it'; + $vec->slug = 'vec'; + $vec->wp_locale = 'vec'; + + $vi = new GP_Locale(); + $vi->english_name = 'Vietnamese'; + $vi->native_name = 'Tiếng Việt'; + $vi->lang_code_iso_639_1 = 'vi'; + $vi->lang_code_iso_639_2 = 'vie'; + $vi->country_code = 'vn'; + $vi->wp_locale = 'vi'; + $vi->slug = 'vi'; + $vi->nplurals = 1; + $vi->plural_expression = '0'; + $vi->google_code = 'vi'; + $vi->facebook_locale = 'vi_VN'; + + $wa = new GP_Locale(); + $wa->english_name = 'Walloon'; + $wa->native_name = 'Walon'; + $wa->lang_code_iso_639_1 = 'wa'; + $wa->lang_code_iso_639_2 = 'wln'; + $wa->country_code = 'be'; + $wa->slug = 'wa'; + + $wol = new GP_Locale(); + $wol->english_name = 'Wolof'; + $wol->native_name = 'Wolof'; + $wol->lang_code_iso_639_1 = 'wo'; + $wol->lang_code_iso_639_2 = 'wol'; + $wol->lang_code_iso_639_3 = 'wol'; + $wol->country_code = 'sn'; + $wol->wp_locale = 'wol'; + $wol->slug = 'wol'; + $wol->nplurals = 1; + $wol->plural_expression = '0'; + + $xho = new GP_Locale(); + $xho->english_name = 'Xhosa'; + $xho->native_name = 'isiXhosa'; + $xho->lang_code_iso_639_1 = 'xh'; + $xho->lang_code_iso_639_2 = 'xho'; + $xho->lang_code_iso_639_3 = 'xho'; + $xho->country_code = 'za'; + $xho->wp_locale = 'xho'; + $xho->slug = 'xho'; + $xho->google_code = 'xh'; + $xho->facebook_locale = 'xh_ZA'; + + $xmf = new GP_Locale(); + $xmf->english_name = 'Mingrelian'; + $xmf->native_name = 'მარგალური ნინა'; + $xmf->lang_code_iso_639_3 = 'xmf'; + $xmf->country_code = 'ge'; + $xmf->slug = 'xmf'; + $xmf->alphabet = 'georgian'; + + $yi = new GP_Locale(); + $yi->english_name = 'Yiddish'; + $yi->native_name = 'ייִדיש'; + $yi->lang_code_iso_639_1 = 'yi'; + $yi->lang_code_iso_639_2 = 'yid'; + $yi->slug = 'yi'; + $yi->text_direction = 'rtl'; + $yi->google_code = 'yi'; + $yi->alphabet = 'hebrew'; + + $yor = new GP_Locale(); + $yor->english_name = 'Yoruba'; + $yor->native_name = 'Yorùbá'; + $yor->lang_code_iso_639_1 = 'yo'; + $yor->lang_code_iso_639_2 = 'yor'; + $yor->lang_code_iso_639_3 = 'yor'; + $yor->country_code = 'ng'; + $yor->wp_locale = 'yor'; + $yor->slug = 'yor'; + $yor->google_code = 'yo'; + $yor->facebook_locale = 'yo_NG'; + + $zgh = new GP_Locale(); + $zgh->english_name = 'Tamazight'; + $zgh->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; + $zgh->lang_code_iso_639_2 = 'zgh'; + $zgh->lang_code_iso_639_3 = 'zgh'; + $zgh->country_code = 'ma'; + $zgh->wp_locale = 'zgh'; + $zgh->slug = 'zgh'; + $zgh->nplurals = 2; + $zgh->plural_expression = 'n >= 2 && (n < 11 || n > 99)'; + $zgh->alphabet = 'tifinagh'; + + $zh = new GP_Locale(); + $zh->english_name = 'Chinese'; + $zh->native_name = '中文'; + $zh->lang_code_iso_639_1 = 'zh'; + $zh->lang_code_iso_639_2 = 'zho'; + $zh->slug = 'zh'; + $zh->nplurals = 1; + $zh->plural_expression = '0'; + $zh->alphabet = 'hanyu'; + + $zh_cn = new GP_Locale(); + $zh_cn->english_name = 'Chinese (China)'; + $zh_cn->native_name = '简体中文'; + $zh_cn->lang_code_iso_639_1 = 'zh'; + $zh_cn->lang_code_iso_639_2 = 'zho'; + $zh_cn->country_code = 'cn'; + $zh_cn->wp_locale = 'zh_CN'; + $zh_cn->slug = 'zh-cn'; + $zh_cn->nplurals = 1; + $zh_cn->plural_expression = '0'; + $zh_cn->google_code = 'zh-CN'; + $zh_cn->facebook_locale = 'zh_CN'; + $zh_cn->alphabet = 'simplified-chinese'; + $zh_cn->word_count_type = 'characters_excluding_spaces'; + + $zh_hk = new GP_Locale(); + $zh_hk->english_name = 'Chinese (Hong Kong)'; + $zh_hk->native_name = '香港中文'; + $zh_hk->lang_code_iso_639_1 = 'zh'; + $zh_hk->lang_code_iso_639_2 = 'zho'; + $zh_hk->country_code = 'hk'; + $zh_hk->wp_locale = 'zh_HK'; + $zh_hk->slug = 'zh-hk'; + $zh_hk->nplurals = 1; + $zh_hk->plural_expression = '0'; + $zh_hk->facebook_locale = 'zh_HK'; + $zh_hk->alphabet = 'simplified-chinese'; + $zh_hk->word_count_type = 'characters_excluding_spaces'; + + $zh_sg = new GP_Locale(); + $zh_sg->english_name = 'Chinese (Singapore)'; + $zh_sg->native_name = '中文'; + $zh_sg->lang_code_iso_639_1 = 'zh'; + $zh_sg->lang_code_iso_639_2 = 'zho'; + $zh_sg->country_code = 'sg'; + $zh_sg->wp_locale = 'zh_SG'; + $zh_sg->slug = 'zh-sg'; + $zh_sg->nplurals = 1; + $zh_sg->plural_expression = '0'; + $zh_sg->alphabet = 'hanyu'; + $zh_sg->word_count_type = 'characters_excluding_spaces'; + + $zh_tw = new GP_Locale(); + $zh_tw->english_name = 'Chinese (Taiwan)'; + $zh_tw->native_name = '繁體中文'; + $zh_tw->lang_code_iso_639_1 = 'zh'; + $zh_tw->lang_code_iso_639_2 = 'zho'; + $zh_tw->country_code = 'tw'; + $zh_tw->slug = 'zh-tw'; + $zh_tw->wp_locale= 'zh_TW'; + $zh_tw->nplurals = 1; + $zh_tw->plural_expression = '0'; + $zh_tw->google_code = 'zh-TW'; + $zh_tw->facebook_locale = 'zh_TW'; + $zh_tw->alphabet = 'hanyu'; + $zh_tw->word_count_type = 'characters_excluding_spaces'; + + $zul = new GP_Locale(); + $zul->english_name = 'Zulu'; + $zul->native_name = 'isiZulu'; + $zul->lang_code_iso_639_1 = 'zu'; + $zul->lang_code_iso_639_2 = 'zul'; + $zul->lang_code_iso_639_3 = 'zul'; + $zul->country_code = 'za'; + $zul->wp_locale = 'zul'; + $zul->slug = 'zul'; + $zul->google_code = 'zu'; + + $def_vars = get_defined_vars(); + + if ( function_exists( 'apply_filters' ) ) { + /** + * Fires after the locales have been defined but before they have been assigned to the object property. + * + * @since 3.0.0 + * + * @param array $def_vars The array of locale objects. + * + * @return array The updated array of locale objects. + */ + $def_vars = apply_filters( 'gp_locale_definitions_array', $def_vars ); + } + + foreach ( $def_vars as $locale ) { + $this->locales[ $locale->slug ] = $locale; + } + } + + public static function &instance() { + if ( ! isset( $GLOBALS['gp_locales'] ) ) + $GLOBALS['gp_locales'] = new GP_Locales; + + return $GLOBALS['gp_locales']; + } + + public static function locales() { + $instance = GP_Locales::instance(); + return $instance->locales; + } + + public static function exists( $slug ) { + $instance = GP_Locales::instance(); + return isset( $instance->locales[ $slug ] ); + } + + public static function by_slug( $slug ) { + $instance = GP_Locales::instance(); + return isset( $instance->locales[ $slug ] )? $instance->locales[ $slug ] : null; + } + + public static function by_field( $field_name, $field_value ) { + $instance = GP_Locales::instance(); + $result = false; + + foreach( $instance->locales() as $locale ) { + if ( isset( $locale->$field_name ) && $locale->$field_name == $field_value ) { + $result = $locale; + break; + } + } + + return $result; + } +} + +endif; From 213eb46aa271fddcf643c0840cf3779cf675c734 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:35:47 +0100 Subject: [PATCH 02/10] Add "Locale Switcher" must-use plugin. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- .../locale-switcher/build/index.asset.php | 1 + .../mu-plugins/locale-switcher/build/index.js | 1 + .../locale-switcher/build/style-index-rtl.css | 1 + .../locale-switcher/build/style-index.css | 1 + .../locale-switcher/locale-switcher.php | 135 ++++++++++++++++++ 5 files changed, 139 insertions(+) create mode 100644 content/mu-plugins/locale-switcher/build/index.asset.php create mode 100644 content/mu-plugins/locale-switcher/build/index.js create mode 100644 content/mu-plugins/locale-switcher/build/style-index-rtl.css create mode 100644 content/mu-plugins/locale-switcher/build/style-index.css create mode 100644 content/mu-plugins/locale-switcher/locale-switcher.php diff --git a/content/mu-plugins/locale-switcher/build/index.asset.php b/content/mu-plugins/locale-switcher/build/index.asset.php new file mode 100644 index 0000000..91829ce --- /dev/null +++ b/content/mu-plugins/locale-switcher/build/index.asset.php @@ -0,0 +1 @@ + array('react', 'wp-components', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '9e7e1066364df75c678b'); diff --git a/content/mu-plugins/locale-switcher/build/index.js b/content/mu-plugins/locale-switcher/build/index.js new file mode 100644 index 0000000..88b2445 --- /dev/null +++ b/content/mu-plugins/locale-switcher/build/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,t={918:()=>{const e=window.React,t=window.wp.components,n=window.wp.element,o=window.wp.i18n,r=window.wp.url,a=window.wporgLocaleSwitcherConfig||{},l=l=>{const{externalButton:i}=l,{initialValue:c,options:w}=a,[s,d]=(0,n.useState)(!1),[u,p]=(0,n.useState)(!1);return i?.addEventListener("click",(e=>{e.preventDefault(),d(!0)})),(0,n.createElement)(e.Fragment,null,s&&(0,n.createElement)(t.Modal,{closeButtonLabel:(0,o.__)("Cancel","wporg"),onRequestClose:()=>d(!1),title:(0,o.__)("Change language","wporg")},(0,n.createElement)(t.ComboboxControl,{onChange:e=>{p(e),(e=>{const t=window.location.href;window.location=(0,r.addQueryArgs)(t,{locale:e})})(e)},onFilterValueChange:()=>{},options:w,value:u||c})))};document.addEventListener("DOMContentLoaded",(()=>{const e=document.getElementById("wporg-locale-switcher-container"),t={externalButton:document.getElementById("wp-admin-bar-locale-switcher")};(0,n.render)((0,n.createElement)(l,t),e)}))}},n={};function o(e){var r=n[e];if(void 0!==r)return r.exports;var a=n[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,n,r,a)=>{if(!n){var l=1/0;for(s=0;s=a)&&Object.keys(o.O).every((e=>o.O[e](n[c])))?n.splice(c--,1):(i=!1,a0&&e[s-1][2]>a;s--)e[s]=e[s-1];e[s]=[n,r,a]},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={57:0,350:0};o.O.j=t=>0===e[t];var t=(t,n)=>{var r,a,[l,i,c]=n,w=0;if(l.some((t=>0!==e[t]))){for(r in i)o.o(i,r)&&(o.m[r]=i[r]);if(c)var s=c(o)}for(t&&t(n);wo(918)));r=o.O(r)})(); diff --git a/content/mu-plugins/locale-switcher/build/style-index-rtl.css b/content/mu-plugins/locale-switcher/build/style-index-rtl.css new file mode 100644 index 0000000..022998b --- /dev/null +++ b/content/mu-plugins/locale-switcher/build/style-index-rtl.css @@ -0,0 +1 @@ +#wpadminbar #wp-admin-bar-locale-switcher>.ab-item{cursor:pointer}#wpadminbar #wp-admin-bar-locale-switcher>.ab-item:before{content:"";top:2px} diff --git a/content/mu-plugins/locale-switcher/build/style-index.css b/content/mu-plugins/locale-switcher/build/style-index.css new file mode 100644 index 0000000..022998b --- /dev/null +++ b/content/mu-plugins/locale-switcher/build/style-index.css @@ -0,0 +1 @@ +#wpadminbar #wp-admin-bar-locale-switcher>.ab-item{cursor:pointer}#wpadminbar #wp-admin-bar-locale-switcher>.ab-item:before{content:"";top:2px} diff --git a/content/mu-plugins/locale-switcher/locale-switcher.php b/content/mu-plugins/locale-switcher/locale-switcher.php new file mode 100644 index 0000000..e59d267 --- /dev/null +++ b/content/mu-plugins/locale-switcher/locale-switcher.php @@ -0,0 +1,135 @@ + $locales[ $key ], + 'value' => $key, + ); + + return $accumulator; + }, + array() + ); + + /** + * Filter: Modify the list of available locales in the locale switcher. + * + * @param array $locale_options Each locale is an associative array containing a `label` key and a `value` key. + */ + return apply_filters( 'wporg_locale_switcher_options', $locale_options ); +} + +/** + * Add a Locale node to the admin bar on the front end. + * + * @param \WP_Admin_Bar $wp_admin_bar + * + * @return void + */ +function admin_bar_node( $wp_admin_bar ) { + // This only needs to be shown on the front end. + if ( is_admin() ) { + return; + } + + $all_locales = get_locales_with_native_names(); + $current_locale = get_locale(); + + $node = array( + 'id' => 'locale-switcher', + 'parent' => 'top-secondary', + 'title' => sprintf( + __( 'Current language: %s', 'wporg' ), + $all_locales[ $current_locale ] + ), + 'href' => '#', + ); + + $wp_admin_bar->add_node( $node ); +} + +/** + * Enqueue script and style assets. + * + * @return void + */ +function enqueue_assets() { + if ( ! is_admin_bar_showing() ) { + return; + } + + $script_data = require __DIR__ . '/build/index.asset.php'; + + wp_enqueue_style( + 'wporg-locale-switcher', + plugins_url( 'build/style-index.css', __FILE__ ), + array( 'wp-components' ), + $script_data['version'], + 'screen' + ); + + wp_enqueue_script( + 'wporg-locale-switcher', + plugins_url( 'build/index.js', __FILE__ ), + $script_data['dependencies'], + $script_data['version'], + array( 'strategy' => 'defer' ) + ); + + $locale_options = get_locale_options(); + + $locale_config = array( + 'initialValue' => get_locale(), + 'options' => $locale_options, + ); + + wp_add_inline_script( + 'wporg-locale-switcher', + 'var wporgLocaleSwitcherConfig = ' . wp_json_encode( $locale_config ) . ';', + 'before' + ); +} + +/** + * Render a container for the locale switcher. + * + * @return void + */ +function locale_switcher_container() { + echo '
'; +} From 1ca76dbbdfd8e43a9681033a6ff43e2a040653e2 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:34:50 +0100 Subject: [PATCH 03/10] Add "Pattern Directory" plugin. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- .../pattern-directory/bin/block-stats.php | 122 ++ .../pattern-directory/bin/check-spam.php | 113 ++ .../bin/set-core-pattern.php | 114 ++ .../bin/update-contains-block-types.php | 102 ++ .../plugins/pattern-directory/bootstrap.php | 22 + .../build/pattern-post-type-rtl.css | 1 + .../build/pattern-post-type.asset.php | 1 + .../build/pattern-post-type.css | 1 + .../build/pattern-post-type.js | 2 + .../includes/admin-flags.php | 434 +++++++ .../includes/admin-patterns.php | 551 +++++++++ .../includes/admin-settings.php | 151 +++ .../includes/admin-stats.php | 249 ++++ .../pattern-directory/includes/admin.php | 63 + .../pattern-directory/includes/badges.php | 72 ++ .../includes/class-export-csv.php | 341 ++++++ .../class-rest-favorite-controller.php | 124 ++ .../includes/class-rest-flags-controller.php | 285 +++++ .../pattern-directory/includes/favorite.php | 165 +++ .../pattern-directory/includes/logging.php | 101 ++ .../includes/notifications.php | 260 ++++ .../includes/pattern-flag-post-type.php | 209 ++++ .../includes/pattern-post-type.php | 1046 +++++++++++++++++ .../includes/pattern-validation.php | 442 +++++++ .../pattern-directory/includes/search.php | 204 ++++ .../pattern-directory/includes/stats.php | 481 ++++++++ .../views/admin-settings.php | 22 + .../pattern-directory/views/admin-stats.php | 195 +++ .../plugins/pattern-directory/views/view.php | 76 ++ 29 files changed, 5949 insertions(+) create mode 100644 content/plugins/pattern-directory/bin/block-stats.php create mode 100644 content/plugins/pattern-directory/bin/check-spam.php create mode 100644 content/plugins/pattern-directory/bin/set-core-pattern.php create mode 100644 content/plugins/pattern-directory/bin/update-contains-block-types.php create mode 100644 content/plugins/pattern-directory/bootstrap.php create mode 100644 content/plugins/pattern-directory/build/pattern-post-type-rtl.css create mode 100644 content/plugins/pattern-directory/build/pattern-post-type.asset.php create mode 100644 content/plugins/pattern-directory/build/pattern-post-type.css create mode 100644 content/plugins/pattern-directory/build/pattern-post-type.js create mode 100644 content/plugins/pattern-directory/includes/admin-flags.php create mode 100644 content/plugins/pattern-directory/includes/admin-patterns.php create mode 100644 content/plugins/pattern-directory/includes/admin-settings.php create mode 100644 content/plugins/pattern-directory/includes/admin-stats.php create mode 100644 content/plugins/pattern-directory/includes/admin.php create mode 100644 content/plugins/pattern-directory/includes/badges.php create mode 100644 content/plugins/pattern-directory/includes/class-export-csv.php create mode 100644 content/plugins/pattern-directory/includes/class-rest-favorite-controller.php create mode 100644 content/plugins/pattern-directory/includes/class-rest-flags-controller.php create mode 100644 content/plugins/pattern-directory/includes/favorite.php create mode 100644 content/plugins/pattern-directory/includes/logging.php create mode 100644 content/plugins/pattern-directory/includes/notifications.php create mode 100644 content/plugins/pattern-directory/includes/pattern-flag-post-type.php create mode 100644 content/plugins/pattern-directory/includes/pattern-post-type.php create mode 100644 content/plugins/pattern-directory/includes/pattern-validation.php create mode 100644 content/plugins/pattern-directory/includes/search.php create mode 100644 content/plugins/pattern-directory/includes/stats.php create mode 100644 content/plugins/pattern-directory/views/admin-settings.php create mode 100644 content/plugins/pattern-directory/views/admin-stats.php create mode 100644 content/plugins/pattern-directory/views/view.php diff --git a/content/plugins/pattern-directory/bin/block-stats.php b/content/plugins/pattern-directory/bin/block-stats.php new file mode 100644 index 0000000..14aeb01 --- /dev/null +++ b/content/plugins/pattern-directory/bin/block-stats.php @@ -0,0 +1,122 @@ + POST_TYPE, + 'post_status' => $opts['post_status'], + 'posts_per_page' => -1, + 'post_parent' => 0, + 'orderby' => 'date', + 'order' => 'DESC', +); + +$query = new \WP_Query( $args ); + +$type_counts = array(); +$total_counts = array(); +$lt3_count = 0; +$gt75_count = 0; +while ( $query->have_posts() ) { + $query->the_post(); + $pattern = get_post(); + + $all_blocks = array(); + $blocks = parse_blocks( $pattern->post_content ); + $blocks_queue = $blocks; + + while ( count( $blocks_queue ) > 0 ) { // phpcs:ignore -- inline count OK. + $block = array_shift( $blocks_queue ); + array_push( $all_blocks, $block ); + if ( ! empty( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $inner_block ) { + array_push( $blocks_queue, $inner_block ); + } + } + } + + $block_types_count = count( array_unique( wp_list_pluck( $all_blocks, 'blockName' ) ) ); + $block_total_count = count( $all_blocks ); + + if ( $block_types_count < 3 ) { + $lt3_count++; + if ( $opts['verbose'] ) { + if ( 1 === $block_types_count ) { + echo "Pattern has only 1 block type, $block_total_count block(s).\n"; + } else { + echo "Pattern has $block_types_count block types, $block_total_count blocks.\n"; + } + echo ' ' . get_permalink() . "\n"; + } + } + + if ( $block_total_count > 75 ) { + $gt75_count++; + if ( $opts['verbose'] ) { + echo "Pattern has over 75 blocks.\n"; + echo ' ' . get_permalink() . "\n"; + } + } + + $type_counts[] = $block_types_count; + $total_counts[] = $block_total_count; +} + +echo 'Scanned ' . count( $total_counts ) . " patterns.\n"; + +echo "$lt3_count patterns have <3 blocks.\n"; +echo "$gt75_count patterns have >75 blocks.\n"; + +$type_average = array_sum( $type_counts ) / count( $type_counts ); +$type_min = min( $type_counts ); +$type_max = max( $type_counts ); +printf( + "There are %.1f block types per pattern on average, %d min, and %d max.\n", + $type_average, + $type_min, + $type_max +); + +$total_average = array_sum( $total_counts ) / count( $total_counts ); +$total_min = min( $total_counts ); +$total_max = max( $total_counts ); +printf( + "There are %.1f blocks per pattern on average, %d min, and %d max.\n", + $total_average, + $total_min, + $total_max +); diff --git a/content/plugins/pattern-directory/bin/check-spam.php b/content/plugins/pattern-directory/bin/check-spam.php new file mode 100644 index 0000000..6b6883b --- /dev/null +++ b/content/plugins/pattern-directory/bin/check-spam.php @@ -0,0 +1,113 @@ + or explicitly run over --all.\n" ); + die(); +} + +if ( ! $opts['apply'] ) { + echo "Dry run, will not update any patterns.\n"; +} + +$args = array( + 'post_type' => POST_TYPE, + 'post_status' => $opts['post_status'], + 'posts_per_page' => $opts['per_page'] ?: -1, + 'post_parent' => 0, + 'orderby' => 'date', + 'order' => 'DESC', +); +if ( isset( $opts['post'] ) ) { + $args = array( + 'post_type' => POST_TYPE, + 'p' => absint( $opts['post'] ), + ); +} + +$query = new \WP_Query( $args ); + +$count_checked = 0; +$count_spam = 0; +while ( $query->have_posts() ) { + $count_checked++; + $query->the_post(); + $pattern = get_post(); + + list( $is_spam, $spam_reason ) = check_for_spam( + array( + 'ID' => $pattern->ID, + 'post_name' => $pattern->post_name, + 'post_author' => $pattern->post_author, + 'title' => $pattern->post_title, + 'content' => $pattern->post_content, + 'description' => $pattern->wpop_description ?: '', + 'keywords' => $pattern->wpop_keywords ?: '', + ) + ); + + if ( $is_spam ) { + $count_spam++; + + if ( $opts['verbose'] ) { + echo "{$pattern->ID}: Spam found: $spam_reason\n"; // phpcs:ignore + } + + if ( $opts['apply'] ) { + wp_update_post( + array( + 'ID' => $pattern->ID, + 'post_status' => SPAM_STATUS, + ) + ); + echo "{$pattern->ID}: Post status updated.\n"; // phpcs:ignore + + // Add a note explaining why this post is in pending, if it's due to spam. + if ( function_exists( '\WordPressdotorg\InternalNotes\create_note' ) ) { + \WordPressdotorg\InternalNotes\create_note( + $pattern->ID, + array( + 'post_author' => get_user_by( 'login', 'wordpressdotorg' )->ID ?? 0, + 'post_excerpt' => $spam_reason, + ) + ); + } + } + } else { + if ( $opts['verbose'] ) { + echo "{$pattern->ID}: Not spam.\n"; // phpcs:ignore + } + } +} + +echo "$count_checked patterns checked, $count_spam found to be spam.\n"; // phpcs:ignore diff --git a/content/plugins/pattern-directory/bin/set-core-pattern.php b/content/plugins/pattern-directory/bin/set-core-pattern.php new file mode 100644 index 0000000..a5f1f70 --- /dev/null +++ b/content/plugins/pattern-directory/bin/set-core-pattern.php @@ -0,0 +1,114 @@ + --block_types=core/header" + * + * To run in a sandbox, use php directly, ex: + * php ./bin/set-core-pattern.php --post= --block_types=core/header + * + * The `block_types` arg corresponds to the `blockTypes` in pattern registration, + * used for suggestions on given block types. This is optional. + */ + +namespace WordPressdotorg\Pattern_Directory; + +// This script should only be called in a CLI environment. +if ( 'cli' !== php_sapi_name() ) { + die(); +} + +$opts = getopt( '', array( 'post:', 'url:', 'abspath:', 'block_types:' ) ); + +if ( empty( $opts['url'] ) ) { + $opts['url'] = 'https://wordpress.org/patterns/'; +} + +if ( empty( $opts['abspath'] ) && false !== strpos( __DIR__, 'wp-content' ) ) { + $opts['abspath'] = substr( __DIR__, 0, strpos( __DIR__, 'wp-content' ) ); +} + +if ( ! empty( $opts['block_types'] ) ) { + $opts['block_types'] = explode( ',', $opts['block_types'] ); +} else { + $opts['block_types'] = array(); +} + +// Bootstrap WordPress +$_SERVER['HTTP_HOST'] = parse_url( $opts['url'], PHP_URL_HOST ); +$_SERVER['REQUEST_URI'] = parse_url( $opts['url'], PHP_URL_PATH ); + +require rtrim( $opts['abspath'], '/' ) . '/wp-load.php'; + +if ( ! isset( $opts['post'] ) ) { + fwrite( STDERR, "Error! Specify a post ID with --post=\n" ); + die(); +} + +$pattern = get_post( $opts['post'] ); +$wporg_user_id = '5911429'; + +if ( $pattern ) { + $pattern_id = $pattern->ID; + + // Update author + $result = wp_update_post( array( + 'ID' => $pattern_id, + 'post_author' => $wporg_user_id, + ) ); + if ( is_wp_error( $result ) ) { + echo "Error updating author:\n"; + echo $result->get_error_message() . "\n"; + } else { + echo "Updated author.\n"; + } + + // Add locale (just in case). + $result = update_post_meta( $pattern_id, 'wpop_locale', 'en_US' ); + if ( is_wp_error( $result ) ) { + echo "Error updating locale:\n"; + echo $result->get_error_message() . "\n"; + } else { + echo "Updated locale.\n"; + } + + // Add `blockTypes` meta + if ( count( $opts['block_types'] ) ) { + delete_post_meta( $pattern_id, 'wpop_block_types' ); + + foreach ( $opts['block_types'] as $block_type ) { + $result = add_post_meta( $pattern_id, 'wpop_block_types', $block_type ); + if ( is_wp_error( $result ) ) { + echo "Error updating block types:\n"; + echo $result->get_error_message() . "\n"; + } else { + echo "Updated block types.\n"; + } + } + } + + // Add in WP version. + $result = update_post_meta( $pattern_id, 'wpop_wp_version', '6.2' ); + if ( is_wp_error( $result ) ) { + echo "Error updating version:\n"; + echo $result->get_error_message() . "\n"; + } else { + echo "Updated version.\n"; + } + + // Add core tag + $result = wp_set_post_terms( $pattern_id, 'core', 'wporg-pattern-keyword', false ); + if ( is_wp_error( $result ) ) { + echo "Error updating post terms:\n"; + echo $result->get_error_message() . "\n"; + } else { + echo "Marked for core.\n"; + } +} else { + echo "Pattern not found.\n"; +} diff --git a/content/plugins/pattern-directory/bin/update-contains-block-types.php b/content/plugins/pattern-directory/bin/update-contains-block-types.php new file mode 100644 index 0000000..3f6299f --- /dev/null +++ b/content/plugins/pattern-directory/bin/update-contains-block-types.php @@ -0,0 +1,102 @@ + or explicitly run over --all.\n" ); + die(); +} + +if ( ! $opts['apply'] ) { + echo "Dry run, will not update any patterns.\n"; +} + +$args = array( + 'post_type' => POST_TYPE, + 'post_status' => array( 'publish', 'pending' ), + 'posts_per_page' => isset( $opts['per_page'] ) ? $opts['per_page'] : -1, + 'post_parent' => 0, + 'orderby' => 'date', + 'order' => 'DESC', + 'meta_query' => array( + // Only update patterns without this meta. + array( + 'key' => 'wpop_contains_block_types', + 'compare' => 'NOT EXISTS', + ), + ), +); +if ( isset( $opts['post'] ) ) { + $args = array( + 'post_type' => POST_TYPE, + 'p' => absint( $opts['post'] ), + ); +} + +$query = new \WP_Query( $args ); +$meta_updated = 0; + +while ( $query->have_posts() ) { + $query->the_post(); + $pattern = get_post(); + $pattern_id = $pattern->ID; + $blocks = parse_blocks( $pattern->post_content ); + $all_blocks = _flatten_blocks( $blocks ); + + // Get the list of block names and convert it to a single string. + $block_names = wp_list_pluck( $all_blocks, 'blockName' ); + $block_names = array_filter( $block_names ); + $block_names = array_unique( $block_names ); + sort( $block_names ); + $used_blocks = implode( ',', $block_names ); + + if ( $opts['apply'] ) { + $result = update_post_meta( $pattern_id, 'wpop_contains_block_types', $used_blocks ); + if ( $result ) { + $meta_updated++; + } else if ( $opts['verbose'] ) { + echo "Error updating {$pattern_id}.\n"; // phpcs:ignore + } + } else if ( $opts['verbose'] ) { + echo "Will update {$pattern_id} with '{$used_blocks}'.\n"; // phpcs:ignore + } +} + +echo "Updated {$meta_updated} patterns.\n"; // phpcs:ignore +echo "Done.\n\n"; // phpcs:ignore diff --git a/content/plugins/pattern-directory/bootstrap.php b/content/plugins/pattern-directory/bootstrap.php new file mode 100644 index 0000000..5b0a1e5 --- /dev/null +++ b/content/plugins/pattern-directory/bootstrap.php @@ -0,0 +1,22 @@ + array('react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-plugins'), 'version' => '28887396c1a17951f9ab'); diff --git a/content/plugins/pattern-directory/build/pattern-post-type.css b/content/plugins/pattern-directory/build/pattern-post-type.css new file mode 100644 index 0000000..48d80db --- /dev/null +++ b/content/plugins/pattern-directory/build/pattern-post-type.css @@ -0,0 +1 @@ +.wporg-patterns-unlist-button button{margin-right:1em}.wporg-patterns-unlist-notice{display:block;margin:1em 0}.wporg-patterns-unlist-notice h3{margin-bottom:0}.wporg-patterns-unlist__modal .wporg-patterns-unlist__reasons .components-radio-control__option{margin-bottom:8px}.wporg-patterns-unlist__modal .wporg-patterns-unlist__actions{-moz-column-gap:4%;column-gap:4%;display:grid;grid-template-columns:48% 48%;margin-top:8px}.wporg-patterns-unlist__modal .wporg-patterns-unlist__actions button{justify-content:center} diff --git a/content/plugins/pattern-directory/build/pattern-post-type.js b/content/plugins/pattern-directory/build/pattern-post-type.js new file mode 100644 index 0000000..e0abf72 --- /dev/null +++ b/content/plugins/pattern-directory/build/pattern-post-type.js @@ -0,0 +1,2 @@ +(()=>{"use strict";var e={n:t=>{var s=t&&t.__esModule?()=>t.default:()=>t;return e.d(s,{a:s}),s},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.plugins,s=window.wp.i18n,r=window.wp.editPost,n=window.wp.components,o=window.wp.editor,a=window.wp.data,i=window.wp.element,l="unlisted",p=window.wp.a11y,u=window.wp.coreData,c=window.wp.apiFetch;var d=e.n(c);const g=()=>{},w=window.ReactJSXRuntime,h={hasError:!1,isSubmitted:!1,isSubmitting:!1,message:null,reasonList:[]},m=(e,t)=>{const s={hasError:!1,isSubmitted:!1,isSubmitting:!1,message:null};switch(t.status){case"NOTE_SENT":return{...e,...s,isSubmitting:!0};case"NOTE_RECIEVED":return{...e,...s,isSubmitted:!0};case"REASONS_RECIEVED":return{...e,...s,reasonList:t.reasonList};case"ERROR":return{...e,...s,hasError:!0,message:t.message};default:return e}},_=({onClose:e,onSubmit:t})=>{const r=(0,a.useSelect)((e=>{const t=e(o.store).getCurrentPostId(),s=e(o.store).getCurrentPostType(),{rest_base:r}=e(u.store).getPostType(s);return`/wporg/v1/${r}/${t}/internal-notes`})),[l,c]=(0,i.useReducer)(m,h),[_,b]=(0,i.useState)(""),[S,x]=(0,i.useState)(""),j=(0,i.useRef)();(0,i.useEffect)((()=>{(({onSuccess:e=g,onFailure:t=g})=>{d()({path:"/wp/v2/wporg-pattern-flag-reason"}).then((t=>{const s=t.sort(((e,t)=>{switch(!0){case e.slugt.slug:return 1;default:return 0}})).map((e=>({label:e.name,value:e.id+""})));e(s)})).catch(t)})({onSuccess:(e=[])=>{c({status:"REASONS_RECIEVED",reasonList:e})},onFailure:e=>{c({status:"ERROR",message:e.message})}})}),[]);const f=(0,s.__)("The pattern has been unlisted, and your internal note has been saved.","wporg-patterns"),E=()=>{e()};return(0,w.jsx)(n.Modal,{title:(0,s.__)("Unlist this pattern","wporg-patterns"),onRequestClose:E,className:"wporg-patterns-unlist__modal",children:(0,w.jsx)("div",{ref:j,children:l.isSubmitted?(0,w.jsx)("p",{children:f}):(0,w.jsxs)("form",{onSubmit:e=>{if(e.preventDefault(),l.isSubmitted||l.isSubmitting)return;if(!_)return void c({status:"ERROR",message:(0,s.__)("Please select a reason.","wporg-patterns")});const n=l.reasonList.find((({value:e})=>e===_));c({status:"NOTE_SENT"}),(({url:e,note:t,onSuccess:s=g,onFailure:r=g})=>{d()({path:e,method:"POST",data:{excerpt:t}}).then(s).catch(r)})({url:r,note:S?`UNLISTED: ${n.label} — ${S}`:`UNLISTED: ${n.label}`,onSuccess:()=>{"function"==typeof t&&t(_),c({status:"NOTE_RECIEVED"}),(0,p.speak)(f),j.current.closest('[role="dialog"]').focus()},onFailure:e=>{c({status:"ERROR",message:e.message}),(0,p.speak)((0,s.sprintf)(/* translators: %s: Error message. */ /* translators: %s: Error message. */ +(0,s.__)("Error: %s","wporg-patterns"),e.message))}})},children:[l.reasonList.length?(0,w.jsx)(n.RadioControl,{className:"wporg-patterns-unlist__reasons",label:(0,s.__)("Please choose a reason:","wporg-patterns"),help:(0,s.__)("The reason chosen will be used to show a message to the pattern author.","wporg-patterns"),selected:_,options:l.reasonList,onChange:b,required:!0}):(0,w.jsx)(n.Spinner,{}),(0,w.jsx)(n.TextareaControl,{label:(0,s.__)("Please provide internal details","wporg-patterns"),help:(0,s.__)("This note will only be seen by other admins and moderators.","wporg-patterns"),value:S,onChange:x}),l.hasError&&(0,w.jsx)("div",{className:"notice notice-large notice-alt notice-error",children:l.message}),(0,w.jsxs)("div",{className:"wporg-patterns-unlist__actions",children:[(0,w.jsx)(n.Button,{isSecondary:!0,onClick:E,children:(0,s.__)("Cancel","wporg-patterns")}),(0,w.jsx)(n.Button,{type:"submit",isBusy:l.isSubmitting,isPrimary:!0,children:l.isSubmitting?(0,s.__)("Submitting …","wporg-patterns"):(0,s.__)("Unlist Pattern","wporg-patterns")})]})]})})})},b=window.wp.notices,S=()=>{const e=(0,a.useSelect)((e=>e(o.store).getCurrentPostAttribute("status")),[]),{createNotice:t,removeNotice:r}=(0,a.useDispatch)(b.store),n="unlisted-pattern-notice";return(0,i.useEffect)((()=>{e===l?t("warning",(0,s.__)("This pattern is unlisted. It will not appear in the public pattern directory.","wporg-patterns"),{id:n,isDismissible:!1}):r(n)}),[e]),null},x=()=>{const e=(0,a.useSelect)((e=>e(o.store).getCurrentPost().status)),{editPost:t,savePost:p}=(0,a.useDispatch)(o.store),[u,c]=(0,i.useState)(!1),d=e===l?"wporg-patterns-unlist-notice":"wporg-patterns-unlist-button";return(0,w.jsxs)(r.PluginPostStatusInfo,{className:d,children:[e!==l?(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(n.Button,{onClick:()=>c(!0),isSecondary:!0,children:(0,s.__)("Unlist","wporg-patterns")}),(0,w.jsx)("small",{children:(0,s.__)("Remove from the pattern directory","wporg-patterns")})]}):(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)("h3",{children:(0,s.__)("Unlisted","wporg-patterns")}),(0,w.jsx)("small",{children:(0,s.__)("Use the Publish button to re-list this pattern. Note: This overrides the status settings shown above.","wporg-patterns")})]}),u&&(0,w.jsx)(_,{onSubmit:e=>{t({status:l,"wporg-pattern-flag-reason":[e]}),p()},onClose:()=>c(!1)})]})},j="wpop_keywords",f="wpop_description",E="wpop_locale",v=window.wporgLocaleData||{},y=[];for(const[e,t]of Object.entries(v))y.push({value:e,label:t});const C=()=>{const{editPost:e}=(0,a.useDispatch)("core/editor"),{description:t,keywords:i,locale:l,meta:p,title:u}=(0,a.useSelect)((e=>{const{getEditedPostAttribute:t}=e(o.store),s=t("meta")||{};return{description:s[f],keywords:s[j].split(", ").filter((e=>e.length)),locale:s[E],meta:s,title:t("title")||""}}));return(0,w.jsxs)(r.PluginDocumentSettingPanel,{name:"wporg-pattern-details",title:(0,s.__)("Pattern Details","wporg-patterns"),icon:"nothing",children:[(0,w.jsx)(n.TextControl,{label:(0,s.__)("Title","wporg-patterns"),value:u,placeholder:(0,s.__)("Pattern title","wporg-patterns"),onChange:t=>e({title:t})},"title"),(0,w.jsx)(n.TextareaControl,{label:(0,s.__)("Description","wporg-patterns"),value:t,onChange:t=>e({meta:{...p,[f]:t}}),help:(0,s.__)("The description is used to help users of assistive technology understand the content of your pattern.","wporg-patterns")},"description"),(0,w.jsxs)("div",{children:[(0,w.jsx)("p",{children:(0,w.jsx)("strong",{children:(0,s.__)("Keywords","wporg-patterns")})}),(0,w.jsx)("p",{children:(0,s.__)("Keywords are words or short phrases that will help people find your pattern. There is a maximum of 10 keywords.","wporg-patterns")}),(0,w.jsx)(n.FormTokenField,{value:i||[],onChange:t=>{const s=t.join(", ");e({meta:{...p,[j]:s}})},maxLength:10,tokenizeOnSpace:!1})]}),(0,w.jsx)(n.ComboboxControl,{label:(0,s.__)("Language","wporg-patterns"),options:y,value:l,onChange:t=>e({meta:{...p,[E]:t}}),help:(0,s.__)("The language field is used to help users find patterns that were created in their preferred language.","wporg-patterns")},"locale")]})};(0,t.registerPlugin)("pattern-post-type",{render:()=>(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(x,{}),(0,w.jsx)(S,{}),(0,w.jsx)(C,{})]})})})(); diff --git a/content/plugins/pattern-directory/includes/admin-flags.php b/content/plugins/pattern-directory/includes/admin-flags.php new file mode 100644 index 0000000..9e067d0 --- /dev/null +++ b/content/plugins/pattern-directory/includes/admin-flags.php @@ -0,0 +1,434 @@ +id ) { + $query_vars[] = 'post_parent'; + } + + return $query_vars; +} + +/** + * Modify the flags list table columns and their order. + * + * @param array $columns + * + * @return array + */ +function flag_list_table_columns( $columns ) { + $block_pattern = get_post_type_object( PATTERN ); + $flag_reason = get_taxonomy( FLAG_REASON ); + + $cb = array( + 'cb' => $columns['cb'], + ); + + $front_columns = array( + 'pattern' => $block_pattern->labels->singular_name, + 'taxonomy-wporg-pattern-flag-reason' => $flag_reason->labels->singular_name, + 'details' => __( 'Details', 'wporg-patterns' ), + ); + + $columns['author'] = __( 'Reporter', 'wporg-patterns' ); + + unset( $columns['cb'] ); + unset( $columns['title'] ); + unset( $columns['taxonomy-wporg-pattern-flag-reason'] ); + + $columns = $cb + $front_columns + $columns; + + return $columns; +} + +/** + * Render the contents of custom list table columns. + * + * @param string $column_name + * @param int $post_id + * + * @return void + */ +function flag_list_table_render_custom_columns( $column_name, $post_id ) { + global $wp_list_table; + + $current_flag = get_post( $post_id ); + $pattern = get_post( $current_flag->post_parent ); + + switch ( $column_name ) { + case 'pattern': + $status = get_post_status( $current_flag ); + if ( PENDING_STATUS === $status ) { + $title_wrapper = '%s'; + } else { + $title_wrapper = '%s'; + } + + printf( + $title_wrapper, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + esc_html( _draft_or_post_title( $pattern ) ) + ); + + _post_states( $current_flag ); + break; + + case 'details': + echo wp_kses_data( get_the_excerpt( $current_flag ) ); + break; + } +} + +/** + * Modify the post states for the flags list table. + * + * @param array $post_states + * @param WP_Post $post + * + * @return array + */ +function flag_list_table_post_states( $post_states, $post ) { + if ( FLAG === get_post_type( $post ) && RESOLVED_STATUS === get_post_status( $post ) ) { + $status_obj = get_post_status_object( RESOLVED_STATUS ); + $post_states[ RESOLVED_STATUS ] = $status_obj->label; + } + + return $post_states; +} + +/** + * Set up row actions for pattern flags list table. + * + * @param array $actions + * @param WP_Post $post + * + * @return array + */ +function flag_list_table_row_actions( $actions, $post ) { + if ( FLAG !== get_post_type( $post ) ) { + return $actions; + } + + $current_screen = get_current_screen(); + $screen_file = add_query_arg( + 'post_type', + FLAG, + 'edit.php' + ); + + $saved_actions = array_intersect_key( $actions, array_fill_keys( array( 'trash', 'untrash', 'delete' ), true ) ); + $actions = array(); + + $pattern = get_post( $post->post_parent ); + $pattern_title = _draft_or_post_title( $pattern ); + $pattern_url = add_query_arg( + array( + 'post' => $pattern->ID, + 'action' => 'edit', + ), + admin_url( 'post.php' ) + ); + + $actions['review'] = sprintf( + '%s', + esc_attr( $pattern_url ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'Review “%s”', 'wporg-patterns' ), $pattern_title ) ), + __( 'Review Pattern', 'wporg-patterns' ) + ); + + if ( PENDING_STATUS === get_post_status( $post ) ) { + $resolve_url = add_query_arg( + array( + 'action' => 'resolve', + 'post' => array( $post->ID ), + ), + wp_nonce_url( $screen_file, 'bulk-posts' ) + ); + + $actions['resolve'] = sprintf( + '%s', + esc_attr( $resolve_url ), + esc_attr( __( 'Mark this flag as resolved', 'wporg-patterns' ) ), + __( 'Resolve', 'wporg-patterns' ) + ); + } + + if ( RESOLVED_STATUS === get_post_status( $post ) ) { + $unresolve_url = add_query_arg( + array( + 'action' => 'unresolve', + 'post' => array( $post->ID ), + ), + wp_nonce_url( $screen_file, 'bulk-posts' ) + ); + + $actions['unresolve'] = sprintf( + '%s', + esc_attr( $unresolve_url ), + esc_attr( __( 'Mark this flag as pending', 'wporg-patterns' ) ), + __( 'Unresolve', 'wporg-patterns' ) + ); + } + + $actions = $actions + $saved_actions; + + $parent = filter_input( INPUT_GET, 'post_parent', FILTER_VALIDATE_INT ); + if ( ! $parent ) { + $view_all_url = add_query_arg( + array( + 'post_type' => FLAG, + 'post_parent' => $post->post_parent, + ), + admin_url( 'edit.php' ) + ); + + $actions['view-all'] = sprintf( + '
%s', + esc_attr( $view_all_url ), + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'View all flags for “%s”', 'wporg-patterns' ), $pattern_title ) ), + __( 'View All Flags For This Pattern', 'wporg-patterns' ) + ); + } + + return $actions; +} + +/** + * Define bulk actions for the flag list table. + * + * @param array $actions + * + * @return array + */ +function flag_list_table_bulk_actions( $actions ) { + $saved_actions = array_intersect_key( $actions, array_fill_keys( array( 'trash', 'untrash', 'delete' ), true ) ); + + $actions = array( + 'resolve' => __( 'Resolve', 'wporg-patterns' ), + 'unresolve' => __( 'Unresolve', 'wporg-patterns' ), + ); + + return $actions + $saved_actions; +} + +/** + * Execute bulk actions for the flag list table. + * + * @param string $sendback + * @param string $doaction + * @param array $post_ids + * + * @return mixed|string + */ +function flag_list_table_handle_bulk_actions( $sendback, $doaction, $post_ids ) { + $post_data = array( + 'post_type' => FLAG, + 'post' => $post_ids, + ); + + switch ( $doaction ) { + case 'resolve': + $post_data['_status'] = RESOLVED_STATUS; + break; + case 'unresolve': + $post_data['_status'] = PENDING_STATUS; + break; + } + + $result = bulk_edit_posts( $post_data ); + + if ( is_array( $result ) ) { + $result['updated'] = count( $result['updated'] ); + $result['skipped'] = count( $result['skipped'] ); + $result['locked'] = count( $result['locked'] ); + $sendback = add_query_arg( $result, $sendback ); + } + + return $sendback; +} + +/** + * Rearrange the flag list table views. + * + * @param array $views + * + * @return array + */ +function flag_list_table_views( $views ) { + unset( $views['mine'] ); + + $parent_id = filter_input( INPUT_GET, 'post_parent', FILTER_VALIDATE_INT ); + if ( $parent_id ) { + $views = array_map( + // Add a post_parent parameter to each view's URL. + function( $item ) use ( $parent_id ) { + return preg_replace_callback( + '|href=[\'"]+([^\'"]+)[\'"]+|', + function( $matches ) use ( $parent_id ) { + $old_url = wp_kses_decode_entities( $matches[1] ); + $new_url = add_query_arg( array( 'post_parent' => $parent_id ), $old_url ); + + return sprintf( + 'href="%s"', + $new_url + ); + }, + $item + ); + }, + $views + ); + + $post_type_obj = get_post_type_object( FLAG ); + $return = array( + 'return' => sprintf( + '%s', + esc_url( add_query_arg( 'post_type', FLAG, admin_url( 'edit.php' ) ) ), + esc_html( $post_type_obj->labels->all_items ) + ), + ); + + $parent_title = _draft_or_post_title( $parent_id ); + $subtitle = array( + 'filtered' => sprintf( + '%s', + sprintf( __( 'Viewing flags for “%s”', 'wporg-patterns' ), $parent_title ) + ), + ); + + $views = $subtitle + $views + $return; + } + + if ( isset( $views['resolved'] ) ) { + // Resolved comes after Pending, or if not listed, after All. + $resolved = array( $views['resolved'] ); + unset( $views['resolved'] ); + + $split = 1 + array_search( ( isset( $views['pending'] ) ? 'pending' : 'all' ), array_keys( $views ), true ); + $views = array_merge( array_slice( $views, 0, $split ), $resolved, array_slice( $views, $split ) ); + } + + return $views; +} + +/** + * Update post counts when viewing only flags for a specific pattern. + * + * @param object $counts + * @param string $post_type + * + * @return object + */ +function flag_list_table_count_flags_for_pattern( $counts, $post_type ) { + global $wpdb; + + if ( FLAG !== $post_type ) { + return $counts; + } + + $pattern_id = filter_input( INPUT_GET, 'post_parent', FILTER_VALIDATE_INT ); + + if ( ! $pattern_id ) { + return $counts; + } + + $results = $wpdb->get_results( + $wpdb->prepare( + " + SELECT post_status, COUNT( * ) AS num_posts + FROM {$wpdb->posts} + WHERE post_type = %s + AND post_parent = %d + GROUP BY post_status + ", + FLAG, + $pattern_id + ) + ); + + $empty_counts = array_fill_keys( get_post_stati(), 0 ); + $updated_counts = wp_list_pluck( $results, 'num_posts', 'post_status' ); + $updated_counts = array_map( 'absint', $updated_counts ); + $updated_counts = array_merge( $empty_counts, $updated_counts ); + + return (object) $updated_counts; +} + +/** + * Set untrashed flag posts to pending status instead of draft. + * + * @param string $new_status + * @param int $post_id + * + * @return string + */ +function flag_untrash_status( $new_status, $post_id ) { + if ( FLAG === get_post_type( $post_id ) ) { + $new_status = PENDING_STATUS; + } + + return $new_status; +} + +/** + * Make sure the Reasons submenu item is highlighted when editing terms. + * + * @param string $submenu_file + * + * @return string + */ +function flag_reason_submenu_highlight( $submenu_file, $parent_file ) { + global $post_type, $taxonomy; + + if ( + 'edit.php?post_type=wporg-pattern' === $parent_file + && PATTERN === $post_type + && FLAG_REASON === $taxonomy + ) { + $submenu_file = 'edit-tags.php?taxonomy=' . FLAG_REASON . '&post_type=' . PATTERN; + } + + return $submenu_file; +} diff --git a/content/plugins/pattern-directory/includes/admin-patterns.php b/content/plugins/pattern-directory/includes/admin-patterns.php new file mode 100644 index 0000000..4cbe13f --- /dev/null +++ b/content/plugins/pattern-directory/includes/admin-patterns.php @@ -0,0 +1,551 @@ + esc_html__( 'Pending Flags', 'wporg-patterns' ), + 'language' => esc_html__( 'Language', 'wporg-patterns' ), + ) + + array_slice( $columns, 3, null, true ); + + return $columns; +} + +/** + * Render the contents of custom list table columns. + * + * @param string $column_name + * @param int $post_id + * + * @return void + */ +function pattern_list_table_render_custom_columns( $column_name, $post_id ) { + $current_pattern = get_post( $post_id ); + + switch ( $column_name ) { + case 'flags': + $flags = new WP_Query( array( + 'post_type' => FLAG, + 'post_status' => array( 'pending' ), + 'post_parent' => $current_pattern->ID, + 'numberposts' => 1, + ) ); + + if ( $flags->found_posts > 0 ) { + $url = add_query_arg( + array( + 'post_type' => FLAG, + 'post_parent' => $current_pattern->ID, + 'post_status' => 'pending', + ), + admin_url( 'edit.php' ) + ); + + printf( + ' + + %s + ', + esc_attr( $url ), + esc_html( number_format_i18n( $flags->found_posts ) ), + sprintf( + esc_html( _n( + '%s pending flag', + '%s pending flags', + $flags->found_posts, + 'wporg-patterns' + ) ), + esc_html( number_format_i18n( $flags->found_posts ) ) + ) + ); + } else { + echo '—'; + } + break; + + case 'language': + $locale = $current_pattern->wpop_locale ?: 'en_US'; + $locale_labels = get_locales_with_english_names(); + + if ( isset( $locale_labels[ $locale ] ) ) { + printf( + '%s', + esc_html( $locale_labels[ $locale ] ) + ); + } else { + echo '—'; + } + + if ( $current_pattern->wpop_is_translation ) { + $parent = get_post( $current_pattern->post_parent ); + + if ( $parent ) { + printf( + '%s', + esc_html__( 'Translation', 'wporg-patterns' ) + ); + + $view_url = add_query_arg( + array( + 'post_type' => PATTERN, + 'post_id' => $parent->ID, + ), + admin_url( 'edit.php' ) + ); + + printf( + '%2$s', + esc_url( $view_url ), + esc_html__( 'Show original', 'wporg-patterns' ) + ); + } + } else { + printf( + '%s', + esc_html__( 'Original', 'wporg-patterns' ) + ); + + $args = array( + 'post_type' => PATTERN, + 'post_status' => 'any', + 'post_parent' => $current_pattern->ID, + 'number_posts' => 1, + 'meta_query' => array( + array( + 'key' => 'wpop_is_translation', + 'value' => 1, + ), + ), + ); + + $translations = new WP_Query( $args ); + $view_url = add_query_arg( + array( + 'post_type' => PATTERN, + 'is_translation' => true, + 'post_parent' => $current_pattern->ID, + ), + admin_url( 'edit.php' ) + ); + + printf( + '%2$s', + esc_url( $view_url ), + sprintf( + esc_html( _n( + '%s translation', + '%s translations', + $translations->found_posts, + 'wporg-patterns' + ) ), + esc_html( number_format_i18n( $translations->found_posts ) ) + ) + ); + } + break; + } +} + +/** + * Add some styles for the patterns list table. + * + * @param string $which + * + * @return void + */ +function pattern_list_table_styles( $which ) { + global $typenow; + + if ( PATTERN !== $typenow || 'bottom' !== $which ) { + return; + } + + ?> + + PATTERN, + 'has_flags' => 1, + ), + admin_url( 'edit.php' ) + ); + + $extra_attributes = ''; + if ( $wants_flagged ) { + $extra_attributes = ' class="current" aria-current="page"'; + } + + $patterns_with_flags = get_pattern_ids_with_pending_flags(); + + $views['has_flags'] = sprintf( + '%s', + esc_url( $url ), + $extra_attributes, + sprintf( + /* translators: %s: Number of posts. */ + _n( + 'Has Flags (%s)', + 'Have Flags (%s)', + count( $patterns_with_flags ), + 'wporg-patterns' + ), + number_format_i18n( count( $patterns_with_flags ) ) + ) + ); + + $url = add_query_arg( + array( + 'post_type' => PATTERN, + 'post_parent' => 0, + ), + admin_url( 'edit.php' ) + ); + + $extra_attributes = ''; + if ( $wants_originals ) { + $extra_attributes = ' class="current" aria-current="page"'; + } + + $args = array( + 'post_type' => PATTERN, + 'post_status' => array( 'draft', 'pending', 'publish' ), + 'post_parent' => 0, + 'numberposts' => 1, + ); + $query = new WP_Query( $args ); + + $views['originals'] = sprintf( + '%s', + esc_url( $url ), + $extra_attributes, + sprintf( + /* translators: %s: Number of posts. */ + _n( + 'Original (%s)', + 'Originals (%s)', + $query->found_posts, + 'wporg-patterns' + ), + number_format_i18n( $query->found_posts ) + ) + ); + + return $views; +} + +/** + * Modify the query that populates the patterns list table. + * + * @param WP_Query $query + * + * @return void + */ +function handle_pattern_list_table_views( WP_Query $query ) { + $wants_flagged = filter_input( INPUT_GET, 'has_flags', FILTER_VALIDATE_BOOLEAN ); + $wants_translations = filter_input( INPUT_GET, 'is_translation', FILTER_VALIDATE_BOOLEAN ); + $post_id = filter_input( INPUT_GET, 'post_id', FILTER_VALIDATE_INT ); + $post_parent = filter_input( INPUT_GET, 'post_parent', FILTER_VALIDATE_INT ); + + if ( ! is_admin() || ! $query->is_main_query() ) { + return; + } + + $current_screen = get_current_screen(); + + if ( 'edit-' . PATTERN === $current_screen->id ) { + if ( $wants_flagged ) { + $args = array( + 'orderby' => $query->get( 'orderby', 'date' ), + 'order' => $query->get( 'order', 'desc' ), + ); + + $valid_ids = get_pattern_ids_with_pending_flags( $args ); + + if ( empty( $valid_ids ) ) { + $valid_ids = array( 0 ); + } + + $query->set( 'post__in', $valid_ids ); + } + + if ( $wants_translations ) { + $meta_query = $query->get( 'meta_query', array() ); + $meta_query[] = array( + 'key' => 'wpop_is_translation', + 'value' => 1, + ); + $query->set( 'meta_query', $meta_query ); + } + + if ( false !== $post_id ) { + $query->set( 'p', $post_id ); + } + + if ( false !== $post_parent ) { + $query->set( 'post_parent', $post_parent ); + } + } +} + +/** + * More post states for the Patterns list table. + * + * @param array $post_states + * @param WP_Post $post + * + * @return array + */ +function display_post_states( $post_states, $post ) { + if ( isset( $_REQUEST['post_status'] ) ) { + $post_status = $_REQUEST['post_status']; + } else { + $post_status = ''; + } + + if ( + $post->post_status !== $post_status && + in_array( $post->post_status, [ UNLISTED_STATUS, SPAM_STATUS ] ) + ) { + $post_states[ $post->post_status ] = get_post_status_object( $post->post_status )->label; + } + + return $post_states; +} + +/** + * Set up row actions for patterns list table. + * + * Adds action links for "Publish", "Spam", and "Unlist". + * + * @param string[] $actions An array of row action links. + * @param WP_Post $post The post object. + * + * @return array Filtered actions. + */ +function add_row_actions( $actions, $post ) { + if ( PATTERN !== $post->post_type ) { + return $actions; + } + + $saved_actions = array_intersect_key( $actions, array_fill_keys( array( 'trash', 'untrash', 'delete' ), true ) ); + $actions = array_intersect_key( $actions, array_fill_keys( array( 'edit', 'view' ), true ) ); + + $edit_url = add_query_arg( 'post_type', PATTERN, 'edit.php' ); + $title = _draft_or_post_title(); + + if ( PENDING_STATUS === $post->post_status || SPAM_STATUS === $post->post_status ) { + $publish_url = add_query_arg( + array( + 'action' => 'publish', + 'post' => array( $post->ID ), + ), + wp_nonce_url( $edit_url, 'bulk-posts' ) + ); + + $actions['publish'] = sprintf( + '%s', + $publish_url, + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'Publish “%s”', 'wporg-patterns' ), $title ) ), + _x( 'Publish', 'verb', 'wporg-patterns' ) + ); + } + + if ( SPAM_STATUS === $post->post_status ) { + $unlist_url = add_query_arg( + array( + 'action' => 'unlist', + 'post' => array( $post->ID ), + ), + wp_nonce_url( $edit_url, 'bulk-posts' ) + ); + + $actions['unlist'] = sprintf( + '%s', + $unlist_url, + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'Remove “%s” from the directory', 'wporg-patterns' ), $title ) ), + _x( 'Unlist', 'verb', 'wporg-patterns' ) + ); + } + + if ( SPAM_STATUS !== $post->post_status && UNLISTED_STATUS !== $post->post_status ) { + $spam_url = add_query_arg( + array( + 'action' => 'spam', + 'post' => array( $post->ID ), + ), + wp_nonce_url( $edit_url, 'bulk-posts' ) + ); + + $actions['spam'] = sprintf( + '%s', + $spam_url, + /* translators: %s: Post title. */ + esc_attr( sprintf( __( 'Mark “%s” as spam', 'wporg-patterns' ), $title ) ), + _x( 'Spam', 'verb', 'wporg-patterns' ) + ); + } + + return $actions + $saved_actions; +} + +/** + * Define bulk actions for the pattern list table. + * + * @param array $actions + * + * @return array + */ +function add_bulk_actions( $actions ) { + $saved_actions = array_intersect_key( $actions, array_fill_keys( array( 'trash', 'untrash', 'delete' ), true ) ); + + $actions = array( + 'publish' => __( 'Publish', 'wporg-patterns' ), + 'spam' => __( 'Spam', 'wporg-patterns' ), + 'unlist' => __( 'Unlist', 'wporg-patterns' ), + ); + + return $actions + $saved_actions; +} + +/** + * Execute bulk actions for the patterns list table. + * + * @param string $sendback + * @param string $doaction + * @param array $post_ids + * + * @return mixed|string + */ +function handle_bulk_actions( $sendback, $doaction, $post_ids ) { + $post_data = array( + 'post_type' => PATTERN, + 'post' => $post_ids, + ); + + switch ( $doaction ) { + case 'publish': + $post_data['_status'] = 'publish'; + break; + case 'spam': + $post_data['_status'] = SPAM_STATUS; + break; + case 'unlist': + $post_data['_status'] = UNLISTED_STATUS; + + $reason_term = get_term_by( 'slug', '4-spam', FLAG_REASON ); + if ( $reason_term ) { + $post_data['tax_input'] = array( + FLAG_REASON => array( $reason_term->term_id ), + ); + } + break; + } + + $result = bulk_edit_posts( $post_data ); + + if ( is_array( $result ) ) { + $result['updated'] = count( $result['updated'] ); + $result['skipped'] = count( $result['skipped'] ); + $result['locked'] = count( $result['locked'] ); + $sendback = add_query_arg( $result, $sendback ); + } + + return $sendback; +} diff --git a/content/plugins/pattern-directory/includes/admin-settings.php b/content/plugins/pattern-directory/includes/admin-settings.php new file mode 100644 index 0000000..30a6b5f --- /dev/null +++ b/content/plugins/pattern-directory/includes/admin-settings.php @@ -0,0 +1,151 @@ + 'string', + 'sanitize_callback' => function( $value ) { + return in_array( $value, array( 'publish', 'pending' ) ) ? $value : 'publish'; + }, + 'default' => 'publish', + ) + ); + add_settings_field( + 'wporg-pattern-default_status', + esc_html__( 'Default status of new patterns', 'wporg-patterns' ), + __NAMESPACE__ . '\render_status_field', + PAGE_SLUG, + SECTION_NAME, + array( + 'label_for' => 'wporg-pattern-default_status', + ) + ); + + // Flag threshold. + register_setting( + SECTION_NAME, + 'wporg-pattern-flag_threshold', + array( + 'type' => 'integer', + 'sanitize_callback' => function( $value ) { + $value = absint( $value ); + + if ( $value < 1 || $value > 100 ) { + return 5; + } + + return $value; + }, + 'default' => 5, + ) + ); + add_settings_field( + 'wporg-pattern-flag_threshold', + esc_html__( 'Flag threshold', 'wporg-patterns' ), + __NAMESPACE__ . '\render_threshold_field', + PAGE_SLUG, + SECTION_NAME, + array( + 'label_for' => 'wporg-pattern-flag_threshold', + ) + ); +} + +/** + * Render the default status field. + * + * @return void + */ +function render_status_field() { + $current = get_option( 'wporg-pattern-default_status' ); + $statii = array( + 'publish' => esc_html__( 'Published', 'wporg-patterns' ), + 'pending' => esc_html__( 'Pending', 'wporg-patterns' ), + ); + + echo ''; + printf( '

%s

', esc_html__( 'Use this setting to control whether new patterns need moderation before showing up (pending) or not (published).', 'wporg-patterns' ) ); +} + +/** + * Render the flag threshold field. + * + * @return void + */ +function render_threshold_field() { + $current = get_option( 'wporg-pattern-flag_threshold' ); + ?> + + %s

', + esc_html__( 'Use this setting to change the number of times a pattern can be reported before it is automatically unpublished (set to pending) while awaiting review.', 'wporg-patterns' ) + ); +} + +/** + * Display the Block Patterns settings page. + * + * @return void + */ +function render_page() { + require_once dirname( __DIR__ ) . '/views/admin-settings.php'; +} diff --git a/content/plugins/pattern-directory/includes/admin-stats.php b/content/plugins/pattern-directory/includes/admin-stats.php new file mode 100644 index 0000000..3395ffc --- /dev/null +++ b/content/plugins/pattern-directory/includes/admin-stats.php @@ -0,0 +1,249 @@ +cap->edit_posts, + PATTERN_POST_TYPE . '-stats', + __NAMESPACE__ . '\render_subpage' + ); +} + +/** + * Render the stats subpage. + * + * @return void + */ +function render_subpage() { + $schema = get_meta_field_schema(); + $current_data = get_snapshot_data(); + $snapshot_info = get_snapshot_meta_data(); + $next_snapshot = wp_get_scheduled_event( PATTERN_POST_TYPE . '_record_snapshot' ); + $inputs = get_export_form_inputs(); + $export_label = EXPORT_ACTION; + + require dirname( __DIR__ ) . '/views/admin-stats.php'; +} + +/** + * Get meta data about existing snapshots. + * + * @return array + */ +function get_snapshot_meta_data() { + $earliest_snapshot = get_snapshots( array( + 'order' => 'asc', + 'numberposts' => 1, + ) ); + $latest_snapshot_query = get_snapshots( + array( + 'order' => 'desc', + 'numberposts' => 1, + ), + true + ); + + $total_snapshots = $latest_snapshot_query->found_posts; + $earliest_date = ''; + $latest_date = ''; + + if ( $total_snapshots > 0 ) { + $latest_snapshot = $latest_snapshot_query->get_posts(); + $earliest_date = get_the_date( 'Y-m-d', reset( $earliest_snapshot ) ); + $latest_date = get_the_date( 'Y-m-d', reset( $latest_snapshot ) ); + } + + return array( + 'total_snapshots' => $total_snapshots, + 'earliest_date' => $earliest_date, + 'latest_date' => $latest_date, + ); +} + +/** + * Collect and validate the export form inputs. + * + * @return array + */ +function get_export_form_inputs() { + $date_filter = function( $string ) { + $success = preg_match( '|([0-9]{4}\-[0-9]{2}\-[0-9]{2})|', $string, $match ); + + if ( $success ) { + return $match[1]; + } + + return ''; + }; + + $input_config = array( + 'start' => array( + 'filter' => FILTER_CALLBACK, + 'options' => $date_filter, + ), + 'end' => array( + 'filter' => FILTER_CALLBACK, + 'options' => $date_filter, + ), + 'action' => FILTER_DEFAULT, + '_wpnonce' => FILTER_DEFAULT, + ); + + $defaults = array_fill_keys( array_keys( $input_config ), '' ); + $inputs = filter_input_array( INPUT_POST, $input_config ); + + return wp_parse_args( $inputs, $defaults ); +} + +/** + * Process an export form submission. + * + * @return void + */ +function handle_csv_export() { + require_once __DIR__ . '/class-export-csv.php'; + $csv = new \WordCamp\Utilities\Export_CSV(); + + $action = EXPORT_ACTION; + $cpt = get_post_type_object( PATTERN_POST_TYPE ); + $info = get_snapshot_meta_data(); + $inputs = get_export_form_inputs(); + $schema = get_meta_field_schema(); + + if ( $action !== $inputs['action'] ) { + return; + } + + if ( ! current_user_can( $cpt->cap->edit_posts ) ) { + $csv->error->add( 'no_permission', 'Sorry, you do not have permission to do this.' ); + $csv->emit_file(); + } + + if ( ! wp_verify_nonce( $inputs['_wpnonce'], EXPORT_ACTION ) ) { + $csv->error->add( 'invalid_nonce', 'Nonce failed. Try refreshing the screen.' ); + $csv->emit_file(); + } + + try { + $start_date = new \DateTime( $inputs['start'] ); + $end_date = new \DateTime( $inputs['end'] ); + $earliest = new \DateTime( $info['earliest_date'] ); + $latest = new \DateTime( $info['latest_date'] ); + } catch ( \Exception $exception ) { + $csv->error->add( + 'invalid_date', + $exception->getMessage() + ); + $csv->emit_file(); + } + + $csv->set_filename( array( + 'patterns-snapshots', + $start_date->format( 'Ymd' ), + $end_date->format( 'Ymd' ), + ) ); + + $csv->set_column_headers( array_merge( + array( 'Date' ), + array_keys( $schema['properties'] ) + ) ); + + if ( $start_date < $earliest ) { + $csv->error->add( + 'invalid_date', + sprintf( + 'Date range must begin %s or later.', + $earliest->format( 'Y-m-d' ) + ) + ); + $csv->emit_file(); + } + + if ( $end_date > $latest ) { + $csv->error->add( + 'invalid_date', + sprintf( + 'Date range must end %s or earlier.', + $latest->format( 'Y-m-d' ) + ) + ); + $csv->emit_file(); + } + + if ( $start_date > $end_date ) { + $csv->error->add( + 'invalid_date', + 'Date range start must be less than or equal to date range end.' + ); + $csv->emit_file(); + } + + $query_args = array( + 'order' => 'asc', + 'posts_per_page' => -1, + 'date_query' => array( + array( + 'after' => $start_date->format( 'Y-m-d' ), + 'before' => $end_date->format( 'Y-m-d' ), + 'inclusive' => true, + ), + ), + ); + + $snapshots = get_snapshots( $query_args ); + + if ( ! $snapshots ) { + $csv->error->add( + 'no_data', + 'No snapshots were found.' + ); + $csv->emit_file(); + } + + $data = array_map( + function( $snapshot ) use ( $schema ) { + $date = get_the_date( 'Y-m-d', $snapshot ); + $row = array( $date ); + + foreach ( array_keys( $schema['properties'] ) as $key ) { + $row[] = $snapshot->$key; + } + + return $row; + }, + $snapshots + ); + + $csv->add_data_rows( $data ); + + $csv->emit_file(); +} diff --git a/content/plugins/pattern-directory/includes/admin.php b/content/plugins/pattern-directory/includes/admin.php new file mode 100644 index 0000000..b608788 --- /dev/null +++ b/content/plugins/pattern-directory/includes/admin.php @@ -0,0 +1,63 @@ +get_node( 'new-wporg-pattern' ); + if ( $new_pattern ) { + $new_pattern->href = site_url( 'new-pattern/' ); + $wp_admin_bar->add_node( $new_pattern ); + } + + // Top-level "+ New" link, if New Block Pattern is the only item. + $new_content = $wp_admin_bar->get_node( 'new-content' ); + if ( $new_content && str_contains( $new_content->href, POST_TYPE ) ) { + $new_content->href = site_url( 'new-pattern/' ); + $wp_admin_bar->add_node( $new_content ); + } + + // "Edit Block Pattern" link. + if ( is_singular( POST_TYPE ) ) { + $edit_pattern = $wp_admin_bar->get_node( 'edit' ); + if ( $edit_pattern ) { + $pattern_id = wp_get_post_parent_id() ?: get_the_ID(); + $edit_pattern->href = site_url( "pattern/$pattern_id/edit/" ); + if ( wp_get_post_parent_id() !== 0 ) { + $edit_pattern->title = __( 'Edit Original Pattern', 'wporg-patterns' ); + } + $wp_admin_bar->add_node( $edit_pattern ); + } + + // Add a link to the post in wp-admin if the user is a moderator. + $post_type = get_post_type_object( POST_TYPE ); + if ( current_user_can( $post_type->cap->edit_others_posts ) ) { + $wp_admin_bar->add_node( array( + 'id' => 'edit-admin', + 'title' => 'Moderate Pattern', + 'parent' => 'edit-actions', // this node is added by wporg-mu-plugins. + 'href' => get_edit_post_link(), + ) ); + } + } +} diff --git a/content/plugins/pattern-directory/includes/badges.php b/content/plugins/pattern-directory/includes/badges.php new file mode 100644 index 0000000..f27a8b7 --- /dev/null +++ b/content/plugins/pattern-directory/includes/badges.php @@ -0,0 +1,72 @@ +post_author ); + } elseif ( 'publish' === $old_status && 'publish' !== $new_status ) { + // If the user has no published patterns, remove the badge. + $other_posts = get_posts( [ + 'post_type' => PATTERN_POST_TYPE, + 'post_status' => 'publish', + 'author' => $post->post_author, + 'exclude' => $post->ID, + 'numberposts' => 1, + 'fields' => 'ids', + ] ); + + if ( ! $other_posts ) { + remove_badge( 'pattern-author', $post->post_author ); + } + } +} + +/** + * Remove the 'Patterns Team' badge from a user when they're removed from the Patterns site. + */ +function remove_user_from_blog( $user_id ) { + if ( function_exists( 'WordPressdotorg\Profiles\remove_badge' ) ) { + remove_badge( 'patterns-team', $user_id ); + } +} + +/** + * Add/Remove the 'Patterns Team' badge from a user when their role changes. + * + * The badge is added for all roles except for Contributor and Subscriber. + * The badge is removed when the role is set to Contributor or Subscriber. + */ +function set_user_role( $user_id, $role ) { + if ( ! function_exists( 'WordPressdotorg\Profiles\assign_badge' ) ) { + return; + } + + if ( 'subscriber' === $role || 'contributor' === $role ) { + remove_badge( 'patterns-team', $user_id ); + } else { + assign_badge( 'patterns-team', $user_id ); + } +} diff --git a/content/plugins/pattern-directory/includes/class-export-csv.php b/content/plugins/pattern-directory/includes/class-export-csv.php new file mode 100644 index 0000000..09478cd --- /dev/null +++ b/content/plugins/pattern-directory/includes/class-export-csv.php @@ -0,0 +1,341 @@ +error = new \WP_Error(); + + $options = wp_parse_args( $options, array( + 'filename' => array(), + 'headers' => array(), + 'data' => array(), + ) ); + + if ( ! empty( $options['filename'] ) ) { + $this->set_filename( $options['filename'] ); + } + + if ( ! empty( $options['headers'] ) ) { + $this->set_column_headers( $options['headers'] ); + } + + if ( ! empty( $options['data'] ) ) { + $this->add_data_rows( $options['data'] ); + } + } + + /** + * Specify the name for the CSV file. + * + * This method takes an array of string segments that will be concatenated into a single file name string. + * It is not necessary to include the file name suffix (.csv). + * + * Example: + * + * array( 'Payment Activity', '2017-01-01', '2017-12-31' ) + * + * will become: + * + * payment-activity_2017-01-01_2017-12-31.csv + * + * @param array|string $name_segments One or more string segments that will comprise the CSV file name. + * + * @return bool True if the file name was successfully set. Otherwise false. + */ + public function set_filename( $name_segments ) { + if ( ! is_array( $name_segments ) ) { + $name_segments = (array) $name_segments; + } + + $name_segments = array_map( function( $segment ) { + $segment = strtolower( $segment ); + $segment = str_replace( '_', '-', $segment ); + $segment = sanitize_file_name( $segment ); + $segment = str_replace( '.csv', '', $segment ); + + return $segment; + }, $name_segments ); + + if ( ! empty( $name_segments ) ) { + $this->filename = implode( '_', $name_segments ) . '.csv'; + + return true; + } + + return false; + } + + /** + * Set the first row of the CSV file as headers for each column. + * + * If used, this also determines how many columns each row should have. Note that, while optional, this method + * must be used before data rows are added. + * + * @param array $headers The column header strings. + * + * @return bool True if the column headers were successfully set. Otherwise false. + */ + public function set_column_headers( array $headers ) { + if ( ! empty( $this->data_rows ) ) { + $this->error->add( + 'csv_error', + 'Column headers cannot be set after data rows have been added.' + ); + + return false; + } + + $this->header_row = array_map( 'sanitize_text_field', $headers ); + + return true; + } + + /** + * Add a single row of data to the CSV file. + * + * @param array $row A single row of data. + * + * @return bool True if the data row was successfully added. Otherwise false. + */ + public function add_row( array $row ) { + $column_count = 0; + + if ( ! empty( $this->header_row ) ) { + $column_count = count( $this->header_row ); + } elseif ( ! empty( $this->data_rows ) ) { + $column_count = count( $this->data_rows[0] ); + } + + if ( $column_count && count( $row ) !== $column_count ) { + $this->error->add( + 'csv_error', + sprintf( + 'Could not add row because it has %d columns, when it should have %d.', + absint( count( $row ) ), + absint( $column_count ) + ) + ); + + return false; + } + + $this->data_rows[] = array_map( 'sanitize_text_field', $row ); + + return true; + } + + /** + * Wrapper method for adding multiple data rows at once. + * + * @param array $data + * + * @return void + */ + public function add_data_rows( array $data ) { + foreach ( $data as $row ) { + $result = $this->add_row( $row ); + + if ( ! $result ) { + break; + } + } + } + + /** + * Escape an array of strings to be used in a CSV context. + * + * Malicious input can inject formulas into CSV files, opening up the possibility for phishing attacks, + * information disclosure, and arbitrary command execution. + * + * @see http://www.contextis.com/resources/blog/comma-separated-vulnerabilities/ + * @see https://hackerone.com/reports/72785 + * + * Derived from CampTix_Plugin::esc_csv. + * + * Note that this method is not recursive, so should only be used for individual data rows, not an entire data set. + * + * @param array $fields + * + * @return array + */ + public static function esc_csv( array $fields ) { + $active_content_triggers = array( '=', '+', '-', '@' ); + + /* + * Formulas that follow all common delimiters need to be escaped, because the user may choose any delimiter + * when importing a file into their spreadsheet program. Different delimiters are also used as the default + * in different locales. For example, Windows + Russian uses `;` as the delimiter, rather than a `,`. + * + * The file encoding can also effect the behavior; e.g., opening/importing as UTF-8 will enable newline + * characters as delimiters. + */ + $delimiters = array( ',', ';', ':', '|', '^', "\n", "\t", ' ' ); + + foreach ( $fields as $index => $field ) { + // Escape trigger characters at the start of a new field + $first_cell_character = mb_substr( $field, 0, 1 ); + $is_trigger_character = in_array( $first_cell_character, $active_content_triggers, true ); + $is_delimiter = in_array( $first_cell_character, $delimiters, true ); + + if ( $is_trigger_character || $is_delimiter ) { + $field = "'" . $field; + } + + // Escape trigger characters that follow delimiters + foreach ( $delimiters as $delimiter ) { + foreach ( $active_content_triggers as $trigger ) { + $field = str_replace( $delimiter . $trigger, $delimiter . "'" . $trigger, $field ); + } + } + + $fields[ $index ] = $field; + } + + return $fields; + } + + /** + * Generate the contents of the CSV file. + * + * @return string + */ + protected function generate_file_content() { + if ( empty( $this->data_rows ) ) { + $this->error->add( + 'csv_error', + 'No data.' + ); + + return ''; + } + + ob_start(); + + $csv = fopen( 'php://output', 'w' ); + + if ( ! empty( $this->header_row ) ) { + fputcsv( $csv, self::esc_csv( $this->header_row ) ); + } + + foreach ( $this->data_rows as $row ) { + fputcsv( $csv, self::esc_csv( $row ) ); + } + + fclose( $csv ); + + return ob_get_clean(); + } + + /** + * Output the CSV file, or a text file with error messages. + */ + public function emit_file() { + if ( ! $this->filename ) { + $this->error->add( + 'csv_error', + 'Could not generate a CSV file without a file name.' + ); + } + + $content = $this->generate_file_content(); + + header( 'Cache-control: private' ); + header( 'Pragma: private' ); + header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' ); // As seen in CampTix_Plugin::summarize_admin_init. + + if ( ! empty( $this->error->get_error_messages() ) ) { + header( 'Content-Type: text' ); + header( 'Content-Disposition: attachment; filename="error.txt"' ); + + foreach ( $this->error->get_error_codes() as $code ) { + foreach ( $this->error->get_error_messages( $code ) as $message ) { + echo "$code: $message\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + + die(); + } + + header( 'Content-Type: text/csv' ); + header( sprintf( 'Content-Disposition: attachment; filename="%s"', sanitize_file_name( $this->filename ) ) ); + + echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + die(); + } + + /** + * Save the CSV file to a local directory. + * + * @param string $location The path of the directory to save the file in. + * + * @return bool|string + */ + public function save_file( $location ) { + if ( ! $this->filename ) { + $this->error->add( + 'csv_error', + 'Could not generate a CSV file without a file name.' + ); + } + + if ( ! wp_is_writable( $location ) ) { + $this->error->add( + 'filesystem_error', + 'The specified location is not writable.' + ); + + return false; + } + + $full_path = trailingslashit( $location ) . $this->filename; + $content = $this->generate_file_content(); + + $file = fopen( $full_path, 'w' ); + fwrite( $file, $content ); + fclose( $file ); + + return $full_path; + } +} diff --git a/content/plugins/pattern-directory/includes/class-rest-favorite-controller.php b/content/plugins/pattern-directory/includes/class-rest-favorite-controller.php new file mode 100644 index 0000000..150716b --- /dev/null +++ b/content/plugins/pattern-directory/includes/class-rest-favorite-controller.php @@ -0,0 +1,124 @@ + WP_REST_Server::READABLE, + 'callback' => __NAMESPACE__ . '\get_items', + 'permission_callback' => __NAMESPACE__ . '\permissions_check', + ) + ); + + $args = array( + 'id' => array( + 'validate_callback' => function( $param, $request, $key ) { + return is_numeric( $param ); + }, + ), + ); + register_rest_route( + 'wporg/v1', + 'pattern-favorites', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => __NAMESPACE__ . '\create_item', + 'args' => $args, + 'permission_callback' => __NAMESPACE__ . '\permissions_check', + ) + ); + register_rest_route( + 'wporg/v1', + 'pattern-favorites', + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => __NAMESPACE__ . '\delete_item', + 'args' => $args, + 'permission_callback' => __NAMESPACE__ . '\permissions_check', + ) + ); +} + +/** + * Check if a given request has access to favorites. + * The only requirement for anything "favorite" is to be logged in. + * + * @return WP_Error|bool + */ +function permissions_check() { + if ( ! is_user_logged_in() ) { + return new WP_Error( + 'rest_authorization_required', + __( 'You must be logged in to favorite a pattern.', 'wporg-patterns' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; +} + +/** + * Get the list of favorites for the current user. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ +function get_items( $request ) { + $favorites = get_favorites(); + return new WP_REST_Response( $favorites, 200 ); +} + +/** + * Save a pattern to the user's favorites. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ +function create_item( $request ) { + $pattern_id = $request['id']; + $success = add_favorite( $pattern_id ); + + if ( $success ) { + $count = get_favorite_count( $pattern_id ); + return new WP_REST_Response( $count, 200 ); + } + + return new WP_Error( + 'favorite-failed', + __( 'Unable to favorite this pattern.', 'wporg-patterns' ), + array( 'status' => 500 ) + ); +} + +/** + * Remove a pattern from the user's favorites. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|WP_REST_Response + */ +function delete_item( $request ) { + $pattern_id = $request['id']; + $success = remove_favorite( $pattern_id ); + + if ( $success ) { + $count = get_favorite_count( $pattern_id ); + return new WP_REST_Response( $count, 200 ); + } + + return new WP_Error( + 'unfavorite-failed', + __( 'Unable to remove this pattern from your favorites.', 'wporg-patterns' ), + array( 'status' => 500 ) + ); +} diff --git a/content/plugins/pattern-directory/includes/class-rest-flags-controller.php b/content/plugins/pattern-directory/includes/class-rest-flags-controller.php new file mode 100644 index 0000000..11447ac --- /dev/null +++ b/content/plugins/pattern-directory/includes/class-rest-flags-controller.php @@ -0,0 +1,285 @@ +parent_post_type = PATTERN; + } + + /** + * Retrieves an array of endpoint arguments from the item schema for the controller. + * + * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are + * checked for required values and may fall-back to a given default, this is not done + * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE. + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + $endpoint_args = $this->get_item_schema(); + + if ( WP_REST_Server::CREATABLE === $method ) { + $endpoint_args['properties'] = array_intersect_key( + $endpoint_args['properties'], + array( + 'parent' => true, + 'excerpt' => true, + FLAG_TAX => true, + ) + ); + } elseif ( WP_REST_Server::EDITABLE === $method ) { + $endpoint_args['properties'] = array_intersect_key( + $endpoint_args['properties'], + array( + 'status' => true, + FLAG_TAX => true, + ) + ); + } + + return rest_get_endpoint_args_for_schema( $endpoint_args, $method ); + } + + /** + * Checks if a given request has access to read posts. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + $parent_post_type = get_post_type_object( PATTERN ); + + if ( ! current_user_can( $parent_post_type->cap->edit_posts ) ) { + return new WP_Error( + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to view pattern flags.', 'wporg-patterns' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return parent::get_items_permissions_check( $request ); + } + + /** + * Checks if a given request has access to read a post. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $parent = $this->get_parent( $post->post_parent ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + if ( ! current_user_can( 'edit_post', $parent->ID ) ) { + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to view flags for this pattern.', 'wporg-patterns' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return parent::get_item_permissions_check( $request ); + } + + /** + * Checks if a given request has access to create a post. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise. + */ + public function create_item_permissions_check( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( + 'rest_post_exists', + __( 'Cannot create existing post.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + if ( ! is_user_logged_in() ) { + return new WP_Error( + 'rest_authorization_required', + __( 'You must be logged in to submit a flag.', 'wporg-patterns' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + if ( ! is_post_publicly_viewable( $parent ) ) { + return new WP_Error( + 'rest_invalid_post', + __( 'Flags cannot be submitted for this pattern.', 'wporg-patterns' ), + array( 'status' => 403 ) + ); + } + + // Check if the user has already submitted a flag for the pattern. + $flag_check = new WP_Query( array( + 'post_type' => $this->post_type, + 'post_parent' => $parent->ID, + 'post_status' => 'pending', + 'author' => get_current_user_id(), + ) ); + if ( $flag_check->found_posts > 0 ) { + return new WP_Error( + 'rest_already_flagged', + __( 'You have already flagged this pattern.', 'wporg-patterns' ), + array( 'status' => 403 ) + ); + } + + return true; + } + + /** + * Prepares a single post for create or update. + * + * @param WP_REST_Request $request Request object. + * + * @return \stdClass|WP_Error Post object or WP_Error. + */ + protected function prepare_item_for_database( $request ) { + $schema = $this->get_item_schema(); + + $prepared_post = parent::prepare_item_for_database( $request ); + + $prepared_post->post_author = get_current_user_id(); + + if ( ! isset( $request['status'] ) ) { + $prepared_post->post_status = $schema['properties']['status']['default']; + } + + foreach ( $request['wporg-pattern-flag-reason'] as $term_id ) { + if ( ! term_exists( $term_id, FLAG_TAX ) ) { + return new WP_Error( + 'rest_invalid_term_id', + __( 'Invalid term ID.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + } + + return $prepared_post; + } + + /** + * Retrieves the post's schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['status']['default'] = PENDING_STATUS; + $schema['properties']['status']['enum'] = array( PENDING_STATUS, RESOLVED_STATUS ); + + $schema['properties']['parent'] = array( + 'description' => __( 'The ID for the parent of the object.', 'wporg-patterns' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'required' => true, + ); + + $schema['properties']['wporg-pattern-flag-reason']['required'] = true; + + return $schema; + } + + /** + * Retrieves the query params for the posts collection. + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['status']['default'] = PENDING_STATUS; + $query_params['status']['items']['enum'] = array( PENDING_STATUS, RESOLVED_STATUS, 'any' ); + + $query_params['parent'] = array( + 'description' => __( 'Limit result set to items with particular parent IDs.', 'wporg-patterns' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $query_params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'wporg-patterns' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + + return $query_params; + } + + /** + * Get the parent post, if the ID is valid. + * + * @param int $parent Supplied ID. + * + * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. + */ + protected function get_parent( $parent ) { + $error = new WP_Error( + 'rest_post_invalid_parent', + __( 'Invalid post parent ID.', 'wporg-patterns' ), + array( 'status' => 404 ) + ); + if ( (int) $parent <= 0 ) { + return $error; + } + + $parent = get_post( (int) $parent ); + if ( empty( $parent ) || empty( $parent->ID ) || $this->parent_post_type !== $parent->post_type ) { + return $error; + } + + return $parent; + } +} diff --git a/content/plugins/pattern-directory/includes/favorite.php b/content/plugins/pattern-directory/includes/favorite.php new file mode 100644 index 0000000..d5faa6a --- /dev/null +++ b/content/plugins/pattern-directory/includes/favorite.php @@ -0,0 +1,165 @@ +exists() ) { + return false; + } + + $users_favorites = get_favorites( $user ); + $already_favorited = in_array( $post->ID, $users_favorites, true ); + if ( $already_favorited ) { + return true; + } + + $success = add_user_meta( $user->ID, META_KEY, $post->ID ); + return (bool) $success; +} + +/** + * Remove a pattern from a users's favorites list. + * + * @param int|WP_Post|null $post The block pattern to unfavorite. + * @param int|WP_User|null $user The user favoriting. Optional. Default current user. + * @return boolean + */ +function remove_favorite( $post, $user = 0 ) { + $post = get_block_pattern( $post ); + $user = new \WP_User( $user ?: get_current_user_id() ); + if ( ! $post || ! $user->exists() ) { + return false; + } + + $users_favorites = get_favorites( $user ); + $already_favorited = in_array( $post->ID, $users_favorites, true ); + if ( ! $already_favorited ) { + return true; + } + + return delete_user_meta( $user->ID, META_KEY, $post->ID ); +} + +/** + * Check if a pattern is in a user's favorites. + * + * @param int|WP_Post|null $post The block pattern to look up. + * @param int|WP_User|null $user The user to check. Optional. Default current user. + * @return boolean + */ +function is_favorite( $post, $user = 0 ) { + $post = get_block_pattern( $post ); + if ( ! $post ) { + return false; + } + + $users_favorites = get_favorites( $user ); + return in_array( $post->ID, $users_favorites, true ); +} + +/** + * Get a list of the user's favorite patterns + * + * @param int|WP_User|null $user The user to check. Optional. Default current user. + * @return integer[] + */ +function get_favorites( $user = 0 ) { + $user = new \WP_User( $user ?: get_current_user_id() ); + if ( ! $user->exists() ) { + return array(); + } + $favorites = get_user_meta( $user->ID, META_KEY ) ?: array(); + + return array_map( 'absint', $favorites ); +} + +/** + * Get the cached count of how many times this pattern has been favorited. + * + * @param int|WP_Post $post The pattern to check. + * @return integer + */ +function get_favorite_count( $post = 0 ) { + $post = get_block_pattern( $post ); + if ( ! $post ) { + return false; + } + + return absint( get_post_meta( $post->ID, META_KEY, true ) ); +} + +/** + * Get a count of how many times this pattern has been favorited, directly from the users table. + * + * @param int|WP_Post $post The pattern to check. + * @return integer + */ +function get_raw_favorite_count( $post = 0 ) { + global $wpdb; + $post = get_block_pattern( $post ); + if ( ! $post ) { + return false; + } + $count = $wpdb->get_var( $wpdb->prepare( + "SELECT COUNT(*) + FROM {$wpdb->usermeta} + WHERE meta_key=%s + AND meta_value=%d", + META_KEY, + $post->ID + ) ); + + return absint( $count ); +} + +/** + * Update a given post's favorite count cache. + * + * @param mixed $post_id The post ID. + */ +function update_favorite_cache( $post_id ) { + $count = get_raw_favorite_count( $post_id ); + if ( ! is_int( $count ) ) { + return; + } + + update_post_meta( $post_id, META_KEY, $count ); +} + +/** + * Trigger the update of favorite count when a user favorites or unfavorites a pattern. + * + * @param int $mid The meta ID. + * @param int $user_id User ID for this metadata. + * @param string $meta_key Metadata key. + * @param mixed $_meta_value Metadata value. Serialized if non-scalar. Post ID(s). + */ +function trigger_favorite_cache_update( $mid, $user_id, $meta_key, $_meta_value ) { + if ( META_KEY !== $meta_key ) { + return; + } + + // This value can be an array in the delete action, so walk through each unique value and refresh the cache. + if ( is_array( $_meta_value ) ) { + $_meta_value = array_unique( $_meta_value ); + array_walk( $_meta_value, __NAMESPACE__ . '\update_favorite_cache' ); + return; + } + + update_favorite_cache( $_meta_value ); +} +add_action( 'added_user_meta', __NAMESPACE__ . '\trigger_favorite_cache_update', 10, 4 ); +add_action( 'deleted_user_meta', __NAMESPACE__ . '\trigger_favorite_cache_update', 10, 4 ); diff --git a/content/plugins/pattern-directory/includes/logging.php b/content/plugins/pattern-directory/includes/logging.php new file mode 100644 index 0000000..6ea583e --- /dev/null +++ b/content/plugins/pattern-directory/includes/logging.php @@ -0,0 +1,101 @@ +post_parent; + + if ( ! $pattern_id ) { + return; + } + + $new = get_post_status_object( $new_status ); + $user = get_user_by( 'id', $post->post_author ); + $user_handle = sprintf( + '@%s', + $user->user_login + ); + + $msg = ''; + if ( $new_status === $old_status ) { + return; + } elseif ( 'new' === $old_status && PENDING_STATUS === $new_status ) { + $msg = sprintf( + // translators: User name; + __( 'New flag submitted by %s', 'wporg-patterns' ), + esc_html( $user_handle ), + ); + } elseif ( PENDING_STATUS === $new_status ) { + $msg = sprintf( + // translators: 1. User name; 2. Post status; + __( 'Flag submitted by %1$s set to %2$s', 'wporg-patterns' ), + esc_html( $user_handle ), + esc_html( $new->label ) + ); + } elseif ( RESOLVED_STATUS === $new_status ) { + $msg = sprintf( + // translators: 1. User name; 2. Post status; + __( 'Flag submitted by %1$s marked as %2$s', 'wporg-patterns' ), + esc_html( $user_handle ), + esc_html( $new->label ) + ); + } elseif ( 'trash' === $new_status ) { + $msg = sprintf( + // translators: User name; + __( 'Flag submitted by %s moved to trash.', 'wporg-patterns' ), + esc_html( $user_handle ) + ); + } + + if ( $msg ) { + $data = array( + 'post_excerpt' => $msg, + 'post_type' => InternalNotes\LOG_POST_TYPE, + ); + + InternalNotes\create_note( $pattern_id, $data ); + } +} diff --git a/content/plugins/pattern-directory/includes/notifications.php b/content/plugins/pattern-directory/includes/notifications.php new file mode 100644 index 0000000..e61fac0 --- /dev/null +++ b/content/plugins/pattern-directory/includes/notifications.php @@ -0,0 +1,260 @@ +post_status; + $old_status = $post_before->post_status; + if ( $new_status === $old_status ) { + return; + } + + if ( 'publish' === $new_status && in_array( $old_status, array( 'pending', SPAM_STATUS, UNLISTED_STATUS ) ) ) { + notify_pattern_approved( $post ); + } elseif ( SPAM_STATUS === $new_status ) { + notify_pattern_flagged( $post ); + } elseif ( UNLISTED_STATUS === $new_status ) { + notify_pattern_unlisted( $post ); + } +} + +/** + * Notify when a pattern has been approved. + * + * @param \WP_Post $post + * + * @return void + */ +function notify_pattern_approved( $post ) { + $author = get_user_by( 'id', $post->post_author ); + if ( ! $author ) { + return; + } + + $email = $author->user_email; + $locale = get_user_locale( $author ); + + $pattern_title = get_the_title( $post ); + $pattern_url = get_permalink( $post ); + + if ( $locale ) { + switch_to_locale( $locale ); + } + + $subject = esc_html__( 'Pattern published', 'wporg-patterns' ); + + $message = sprintf( + // translators: Plaintext email message. Note the line breaks. 1. Pattern title; 2. Pattern URL; + esc_html__( 'Hello! + +Thank you for submitting your pattern, %1$s. It is now live in the Block Pattern Directory! + +%2$s', 'wporg-patterns' ), + esc_html( $pattern_title ), + esc_url_raw( $pattern_url ) + ); + + if ( $locale ) { + restore_current_locale(); + } + + send_email( $email, $subject, $message ); +} + +/** + * Notify when a pattern has been unpublished for review. + * + * This is called either when the status transitions into "spam", or when a post + * crosses the flag threshold. + * + * @param \WP_Post $post + * + * @return void + */ +function notify_pattern_flagged( $post ) { + $author = get_user_by( 'id', $post->post_author ); + if ( ! $author ) { + return; + } + + $email = $author->user_email; + $locale = get_user_locale( $author ); + + $pattern_title = get_the_title( $post ); + + if ( $locale ) { + switch_to_locale( $locale ); + } + + $reason = ''; + + if ( SPAM_STATUS === $post->post_status ) { + $spam_term = get_term_by( 'slug', '4-spam', REASON ); + $reason = wp_strip_all_tags( $spam_term->description ); + } else { + $flags = get_posts( array( + 'post_type' => FLAG, + 'post_parent' => $post->ID, + 'post_status' => PENDING_STATUS, + ) ); + if ( ! empty( $flags ) ) { + $reasons = array(); + foreach ( $flags as $flag ) { + $terms = get_the_terms( $flag, REASON ); + if ( is_array( $terms ) ) { + $reasons = array_merge( $reasons, $terms ); + } + } + $reasons = array_map( + function( \WP_Term $reason ) { + return wp_strip_all_tags( $reason->description ); + }, + $reasons + ); + $reasons = array_unique( $reasons ); + $reason = trim( implode( "\n", $reasons ) ); + } + } + + if ( ! $reason ) { + $reason = get_default_reason_description(); + } + + $subject = esc_html__( 'Pattern being reviewed', 'wporg-patterns' ); + + $message = sprintf( + // translators: Plaintext email message. Note the line breaks. 1. Pattern title; 2. Pattern URL; + esc_html__( 'Hi there! + +Thanks for submitting your pattern. Unfortunately, your pattern, %1$s, has been flagged for review due to the following reason(s): + +%2$s + +Your pattern has been unpublished from the Block Pattern Directory at this time, and will receive further review. If the pattern meets the guidelines, we will re-publish it to the Block Pattern Directory. Thanks for your patience with us volunteer reviewers!', 'wporg-patterns' ), + esc_html( $pattern_title ), + esc_html( $reason ) + ); + + if ( $locale ) { + restore_current_locale(); + } + + send_email( $email, $subject, $message ); +} + +/** + * Notify when a pattern has been unlisted. + * + * @param \WP_Post $post + * + * @return void + */ +function notify_pattern_unlisted( $post ) { + $author = get_user_by( 'id', $post->post_author ); + if ( ! $author ) { + return; + } + + $email = $author->user_email; + $locale = get_user_locale( $author ); + + $pattern_title = get_the_title( $post ); + + if ( $locale ) { + switch_to_locale( $locale ); + } + + $reasons = get_the_terms( $post, REASON ); + $reason = ''; + if ( ! empty( $reasons ) ) { + $reason_term = reset( $reasons ); + $reason = wp_strip_all_tags( $reason_term->description ); + } + + if ( ! $reason ) { + $reason = get_default_reason_description(); + } + + $subject = esc_html__( 'Pattern unlisted', 'wporg-patterns' ); + + $message = sprintf( + // translators: Plaintext email message. Note the line breaks. 1. Pattern title; 2. Pattern URL; + esc_html__( 'Hello, + +Your pattern, %1$s, has been unlisted from the Block Pattern Directory due to the following reason: + +%2$s + +If you would like to resubmit your pattern, please make sure it follows the guidelines: + +%3$s', 'wporg-patterns' ), + esc_html( $pattern_title ), + esc_html( $reason ), + 'https://wordpress.org/patterns/about/' + ); + + if ( $locale ) { + restore_current_locale(); + } + + send_email( $email, $subject, $message ); +} + +/** + * Wrapper for wp_mail. + * + * @param string $to + * @param string $subject + * @param string $message + * + * @return void + */ +function send_email( $to, $subject, $message ) { + $message = html_entity_decode( $message, ENT_QUOTES ); + + wp_mail( + $to, + $subject, + $message, + array( + 'From: WordPress Pattern Directory ', + 'Reply-To: ', + ) + ); +} diff --git a/content/plugins/pattern-directory/includes/pattern-flag-post-type.php b/content/plugins/pattern-directory/includes/pattern-flag-post-type.php new file mode 100644 index 0000000..9760490 --- /dev/null +++ b/content/plugins/pattern-directory/includes/pattern-flag-post-type.php @@ -0,0 +1,209 @@ + __( 'Block Pattern Flags', 'wporg-patterns' ), + 'singular_name' => __( 'Block Pattern Flag', 'wporg-patterns' ), + 'add_new_item' => __( 'Add New Flag', 'wporg-patterns' ), + 'edit_item' => __( 'Edit Flag', 'wporg-patterns' ), + 'new_item' => __( 'New Flag', 'wporg-patterns' ), + 'view_item' => __( 'View Flag', 'wporg-patterns' ), + 'view_items' => __( 'View Flags', 'wporg-patterns' ), + 'search_items' => __( 'Search Flags', 'wporg-patterns' ), + 'not_found' => __( 'No flags found.', 'wporg-patterns' ), + 'not_found_in_trash' => __( 'No flags found in Trash.', 'wporg-patterns' ), + 'all_items' => __( 'All Flags', 'wporg-patterns' ), + 'insert_into_item' => __( 'Insert into flag', 'wporg-patterns' ), + 'filter_items_list' => __( 'Filter flags list', 'wporg-patterns' ), + 'items_list_navigation' => __( 'Flags list navigation', 'wporg-patterns' ), + 'items_list' => __( 'Flags list', 'wporg-patterns' ), + ); + + register_post_type( + POST_TYPE, + array( + 'labels' => $post_type_labels, + 'description' => 'Flags are added to patterns by users when the pattern needs to be reviewed by a moderator.', + 'show_ui' => true, + 'show_in_menu' => 'edit.php?post_type=wporg-pattern', + 'show_in_admin_bar' => false, + 'show_in_rest' => true, + 'rest_controller_class' => '\\WordPressdotorg\\Pattern_Directory\\REST_Flags_Controller', + 'supports' => array( 'author', 'excerpt' ), + 'can_export' => false, + 'delete_with_user' => false, + ) + ); + + $taxonomy_labels = array( + 'name' => __( 'Flag Reasons', 'wporg-patterns' ), + 'singular_name' => __( 'Flag Reason', 'wporg-patterns' ), + 'search_items' => __( 'Search Reasons', 'wporg-patterns' ), + 'all_items' => __( 'All Reasons', 'wporg-patterns' ), + 'parent_item' => __( 'Parent Reason', 'wporg-patterns' ), + 'parent_item_colon' => __( 'Parent Reason:', 'wporg-patterns' ), + 'edit_item' => __( 'Edit Reason', 'wporg-patterns' ), + 'view_item' => __( 'View Reason', 'wporg-patterns' ), + 'update_item' => __( 'Update Reason', 'wporg-patterns' ), + 'add_new_item' => __( 'Add New Reason', 'wporg-patterns' ), + 'new_item_name' => __( 'New Reason', 'wporg-patterns' ), + 'separate_items_with_commas' => __( 'Separate reasons with commas', 'wporg-patterns' ), + 'add_or_remove_items' => __( 'Add or remove reasons', 'wporg-patterns' ), + 'not_found' => __( 'No reasons found.', 'wporg-patterns' ), + 'no_terms' => __( 'No reasons', 'wporg-patterns' ), + 'filter_by_item' => __( 'Filter by reason', 'wporg-patterns' ), + 'items_list_navigation' => __( 'Reasons list navigation', 'wporg-patterns' ), + 'items_list' => __( 'Reasons list', 'wporg-patterns' ), + 'back_to_items' => __( '← Go to Reasons', 'wporg-patterns' ), + ); + + register_taxonomy( + TAX_TYPE, + array( POST_TYPE, PATTERN ), // The taxonomy will also get applied to patterns when they get unlisted. + array( + 'labels' => $taxonomy_labels, + 'description' => 'Flag reason indicates why a flag was added to a pattern.', + 'public' => false, + 'hierarchical' => true, + 'show_ui' => true, + 'show_in_menu' => 'edit.php?post_type=' . PATTERN, + 'show_in_rest' => true, + 'show_tagcloud' => false, + 'show_in_quick_edit' => false, + 'show_admin_column' => true, + ) + ); + + register_post_status( + RESOLVED_STATUS, + array( + 'label' => __( 'Resolved', 'wporg-patterns' ), + 'label_count' => _n_noop( + 'Resolved (%s)', + 'Resolved (%s)', + 'wporg-patterns' + ), + 'protected' => true, + ) + ); +} + +/** + * If a pattern or flag doesn't have a reason term added, but needs to show a reason description. + * + * @return string + */ +function get_default_reason_description() { + return __( "This pattern doesn't meet the guidelines for the pattern directory.", 'wporg-patterns' ); +} + +/** + * Automatically unpublish a pattern if it receives a certain number of flags. + * + * @param int $post_ID + * @param WP_Post $post + * @param bool $update + * + * @return void + */ +function check_flag_threshold( $post_ID, $post, $update ) { + if ( $update || POST_TYPE !== get_post_type( $post ) ) { + return; + } + + $pattern = get_post( $post->post_parent ); + if ( ! $pattern ) { + return; + } + + $flag_check = new WP_Query( array( + 'post_type' => POST_TYPE, + 'post_parent' => $pattern->ID, + 'post_status' => PENDING_STATUS, + ) ); + + $threshold = absint( get_option( 'wporg-pattern-flag_threshold', 5 ) ); + + if ( $flag_check->found_posts >= $threshold ) { + wp_update_post( array( + 'ID' => $pattern->ID, + 'post_status' => PENDING_STATUS, + ) ); + + /** + * Fires after a pattern is automatically unlisted. + * + * @param WP_Post $pattern The just-unlisted pattern. + */ + do_action( 'wporg_unlist_pattern', $pattern ); + } +} + +/** + * Get a list of post IDs for patterns that have pending flags. + * + * TODO this isn't used anywhere on the front end, but maybe it should be cached? + * + * @param array $args Optional. Query args. 'orderby' and/or 'order'. + * + * @return int[] + */ +function get_pattern_ids_with_pending_flags( $args = array() ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'orderby' => 'date', + 'order' => 'desc', + ) + ); + + // For string interpolation. + $pattern = PATTERN; + $flag = POST_TYPE; + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $pattern_ids = $wpdb->get_col( + $wpdb->prepare( + " + SELECT DISTINCT patterns.ID + FROM {$wpdb->posts} patterns + JOIN {$wpdb->posts} flags ON patterns.ID = flags.post_parent + AND flags.post_type = '{$flag}' + AND flags.post_status = 'pending' + WHERE patterns.post_type = '{$pattern}' + ORDER BY %s %s + ", + $args['orderby'], + $args['order'] + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return $pattern_ids; +} diff --git a/content/plugins/pattern-directory/includes/pattern-post-type.php b/content/plugins/pattern-directory/includes/pattern-post-type.php new file mode 100644 index 0000000..f47b79b --- /dev/null +++ b/content/plugins/pattern-directory/includes/pattern-post-type.php @@ -0,0 +1,1046 @@ + array( + 'name' => _x( 'Block Pattern', 'post type general name', 'wporg-patterns' ), + 'singular_name' => _x( 'Block Pattern', 'post type singular name', 'wporg-patterns' ), + 'add_new' => _x( 'Add New', 'block pattern', 'wporg-patterns' ), + 'add_new_item' => __( 'Add New Pattern', 'wporg-patterns' ), + 'edit_item' => __( 'Edit Pattern', 'wporg-patterns' ), + 'new_item' => __( 'New Pattern', 'wporg-patterns' ), + 'view_item' => __( 'View Pattern', 'wporg-patterns' ), + 'view_items' => __( 'View Patterns', 'wporg-patterns' ), + 'search_items' => __( 'Search Patterns', 'wporg-patterns' ), + 'not_found' => __( 'No patterns found.', 'wporg-patterns' ), + 'not_found_in_trash' => __( 'No patterns found in Trash.', 'wporg-patterns' ), + 'all_items' => __( 'All Block Patterns', 'wporg-patterns' ), + 'archives' => __( 'Pattern Archives', 'wporg-patterns' ), + 'attributes' => __( 'Pattern Attributes', 'wporg-patterns' ), + 'insert_into_item' => __( 'Insert into block pattern', 'wporg-patterns' ), + 'uploaded_to_this_item' => __( 'Uploaded to this block pattern', 'wporg-patterns' ), + 'filter_items_list' => __( 'Filter patterns list', 'wporg-patterns' ), + 'items_list_navigation' => __( 'Block patterns list navigation', 'wporg-patterns' ), + 'items_list' => __( 'Block patterns list', 'wporg-patterns' ), + 'item_published' => __( 'Block pattern published.', 'wporg-patterns' ), + 'item_published_privately' => __( 'Block pattern published privately.', 'wporg-patterns' ), + 'item_reverted_to_draft' => __( 'Block pattern reverted to draft.', 'wporg-patterns' ), + 'item_scheduled' => __( 'Block pattern scheduled.', 'wporg-patterns' ), + 'item_updated' => __( 'Block pattern updated.', 'wporg-patterns' ), + ), + 'description' => 'Stores publicly shared Block Patterns (predefined block layouts, ready to insert and tweak).', + 'public' => true, + 'show_in_rest' => true, + 'rewrite' => array( 'slug' => 'pattern' ), + 'supports' => array( 'title', 'editor', 'author', 'custom-fields', 'revisions', 'wporg-internal-notes', 'wporg-log-notes' ), + 'capability_type' => array( 'pattern', 'patterns' ), + 'map_meta_cap' => true, + ) + ); + + register_taxonomy( + 'wporg-pattern-category', + POST_TYPE, + array( + 'public' => true, + 'hierarchical' => true, + 'show_in_rest' => true, + 'rest_base' => 'pattern-categories', + 'show_admin_column' => true, + 'rewrite' => array( + 'slug' => 'categories', + ), + 'query_var' => 'pattern-categories', + 'capabilities' => array( + 'assign_terms' => 'edit_patterns', + 'edit_terms' => 'edit_patterns', + ), + ) + ); + + register_taxonomy( + 'wporg-pattern-keyword', + POST_TYPE, + array( + 'public' => true, + 'hierarchical' => false, + 'show_in_rest' => true, + 'rest_base' => 'pattern-keywords', + 'show_admin_column' => true, + 'rewrite' => array( + 'slug' => 'pattern-keywords', + ), + 'capabilities' => array( + 'assign_terms' => 'edit_patterns', + 'edit_terms' => 'edit_patterns', + ), + + 'labels' => array( + 'name' => _x( 'Keywords (Internal)', 'taxonomy general name', 'wporg-patterns' ), + 'singular_name' => _x( 'Keyword', 'taxonomy singular name', 'wporg-patterns' ), + 'search_items' => __( 'Search Keywords', 'wporg-patterns' ), + 'popular_items' => __( 'Popular Keywords', 'wporg-patterns' ), + 'all_items' => __( 'All Keywords', 'wporg-patterns' ), + 'edit_item' => __( 'Edit Keyword', 'wporg-patterns' ), + 'view_item' => __( 'View Keyword', 'wporg-patterns' ), + 'update_item' => __( 'Update Keyword', 'wporg-patterns' ), + 'add_new_item' => __( 'Add New Keyword', 'wporg-patterns' ), + 'new_item_name' => __( 'New Keyword Name', 'wporg-patterns' ), + 'separate_items_with_commas' => __( 'Separate keywords with commas', 'wporg-patterns' ), + 'add_or_remove_items' => __( 'Add or remove keywords', 'wporg-patterns' ), + 'choose_from_most_used' => __( 'Choose from the most used keywords', 'wporg-patterns' ), + 'not_found' => __( 'No keywords found.', 'wporg-patterns' ), + 'no_terms' => __( 'No keywords', 'wporg-patterns' ), + 'items_list_navigation' => __( 'Keywords list navigation', 'wporg-patterns' ), + 'items_list' => __( 'Keywords list', 'wporg-patterns' ), + /* translators: Tab heading when selecting from the most used terms. */ + 'most_used' => _x( 'Most Used', 'keywords', 'wporg-patterns' ), + 'back_to_items' => __( '← Go to Keywords', 'wporg-patterns' ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_keywords', + array( + 'type' => 'string', + 'description' => 'A comma-separated list of keywords for this pattern', + 'single' => true, + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'maxLength' => 360, + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_description', + array( + 'type' => 'string', + 'description' => 'A description of the pattern', + 'single' => true, + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'maxLength' => 360, + 'required' => true, + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_viewport_width', + array( + 'type' => 'number', + 'description' => 'The width of the pattern in the block inserter.', + 'single' => true, + 'default' => 1200, + 'sanitize_callback' => 'absint', + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'minimum' => 200, + 'maximum' => 2000, + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_block_types', + array( + 'type' => 'string', + 'description' => 'A list of block types this pattern supports for transforms.', + 'single' => false, + 'sanitize_callback' => function( $value, $key, $type ) { + return preg_replace( '/[^a-z0-9-\/]/', '', $value ); + }, + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_locale', + array( + 'type' => 'string', + 'description' => 'The language used when creating this pattern.', + 'single' => true, + 'sanitize_callback' => function( $value ) { + if ( ! in_array( $value, array_keys( get_locales() ), true ) ) { + return 'en_US'; + } + + return $value; + }, + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'enum' => array_keys( get_locales() ), + 'required' => true, + 'default' => 'en_US', + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_wp_version', + array( + 'type' => 'string', + 'description' => 'The earliest WordPress version compatible with this pattern.', + 'single' => true, + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + ), + ), + ) + ); + + register_post_meta( + POST_TYPE, + 'wpop_contains_block_types', + array( + 'type' => 'string', + 'description' => 'A list of block types used in this pattern', + 'single' => true, + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'auth_callback' => __NAMESPACE__ . '\can_edit_this_pattern', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + ), + ), + ) + ); +} + +/** + * Adds extra fields to REST API responses. + */ +function register_rest_fields() { + /* + * Provide category and keyword slugs without embedding. + * + * Normally API clients would request these via `_embed` parameters, but that would returning the entire + * object, and Core only needs the slugs. We'd also have to include the `_links` field, because of a Core bug. + * + * @see https://core.trac.wordpress.org/ticket/49538 + * @see https://core.trac.wordpress.org/ticket/49985 + * + * Adding it here is faster for the server to generate, and for the client to download. It also makes the + * output easier for a human to visually parse. + */ + register_rest_field( + POST_TYPE, + 'category_slugs', + array( + 'get_callback' => function() { + $slugs = wp_list_pluck( wp_get_object_terms( get_the_ID(), 'wporg-pattern-category' ), 'slug' ); + $slugs = array_map( 'sanitize_title', $slugs ); + $slugs = array_diff( $slugs, [ 'featured' ] ); + $slugs = array_values( $slugs ); + + return $slugs; + }, + + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ) + ); + + // See `category_slugs` registration for details. + register_rest_field( + POST_TYPE, + 'keyword_slugs', + array( + 'get_callback' => function() { + $slugs = wp_list_pluck( wp_get_object_terms( get_the_ID(), 'wporg-pattern-keyword' ), 'slug' ); + + return array_map( 'sanitize_title', $slugs ); + }, + + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ) + ); + + /* + * Provide the raw content without requiring the `edit` context. + * + * We need the raw content because it contains the source code for blocks (the comment delimiters). The rendered + * content is considered a "classic block", since it lacks these. The `edit` context would return both raw and + * rendered, but it requires more permissions and potentially exposes more content than we need. + */ + register_rest_field( + POST_TYPE, + 'pattern_content', + array( + 'get_callback' => function( $response_data ) { + $pattern = get_post( $response_data['id'] ); + return decode_pattern_content( $pattern->post_content ); + }, + + 'schema' => array( + 'type' => 'string', + ), + ) + ); + + /* + * Get the author's avatar. + */ + register_rest_field( + POST_TYPE, + 'favorite_count', + array( + 'get_callback' => function() { + return get_favorite_count( get_the_ID() ); + }, + + 'schema' => array( + 'type' => 'integer', + 'default' => 0, + ), + ) + ); + + /* + * Get the author's avatar. + */ + register_rest_field( + POST_TYPE, + 'author_meta', + array( + 'get_callback' => function( $post ) { + return array( + 'name' => esc_html( get_the_author_meta( 'display_name', $post['author'] ) ), + 'url' => esc_url( home_url( '/author/' . get_the_author_meta( 'user_nicename', $post['author'] ) ) ), + 'avatar' => get_avatar_url( $post['author'], array( 'size' => 64 ) ), + ); + }, + + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + ), + 'avatar' => array( + 'type' => 'string', + ), + ), + ), + ) + ); + + // Add the parent pattern (English original) to the endpoint. + // We only need to set the schema. `WP_REST_Posts_Controller` will output the parent ID if the + // schema contains the parent property. It also checks that the ID referenced is a valid post. + register_rest_field( + POST_TYPE, + 'parent', + array( + 'schema' => array( + 'description' => __( 'The ID for the original English pattern.', 'wporg-patterns' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + + register_rest_field( + POST_TYPE, + 'unlisted_reason', + array( + 'get_callback' => function() { + $reasons = wp_get_object_terms( get_the_ID(), FLAG_REASON ); + if ( count( $reasons ) > 0 ) { + $reason = array_shift( $reasons ); + return array( + 'term_id' => absint( $reason->term_id ), + 'name' => esc_attr( $reason->name ), + 'slug' => esc_attr( $reason->slug ), + 'description' => wp_kses_post( $reason->description ), + ); + } + + return array(); + }, + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'term_id' => array( + 'type' => 'number', + ), + 'name' => array( + 'type' => 'string', + ), + 'slug' => array( + 'type' => 'string', + ), + 'description' => array( + 'type' => 'string', + ), + ), + ), + ) + ); +} + +/** + * Register custom statuses for patterns. + * + * @return void + */ +function register_post_statuses() { + register_post_status( + UNLISTED_STATUS, + array( + 'label' => _x( 'Unlisted', 'post status', 'wporg-patterns' ), + 'label_count' => _nx_noop( + 'Unlisted (%s)', + 'Unlisted (%s)', + 'post status', + 'wporg-patterns' + ), + 'public' => false, + 'protected' => true, + 'show_in_admin_all_list' => true, + ) + ); + + register_post_status( + SPAM_STATUS, + array( + 'label' => _x( 'Possible Spam', 'post status', 'wporg-patterns' ), + 'label_count' => _nx_noop( + 'Possible Spam (%s)', + 'Possible Spam (%s)', + 'post status', + 'wporg-patterns' + ), + 'public' => false, + 'protected' => true, + 'show_in_admin_all_list' => true, + ) + ); +} + +/** + * Do things when certain status transitions happen. + * + * @param string $new_status + * @param string $old_status + * @param \WP_Post $post + * + * @return void + */ +function status_transitions( $new_status, $old_status, $post ) { + if ( POST_TYPE !== get_post_type( $post ) ) { + return; + } + + // If a pattern gets relisted, remove the reason that it was originally unlisted. + if ( UNLISTED_STATUS === $old_status && UNLISTED_STATUS !== $new_status ) { + wp_delete_object_term_relationships( $post->ID, array( FLAG_REASON ) ); + } +} + +/** + * Given a post ID, parse out the block types and update the `wpop_contains_block_types` meta field. + * + * @param int $pattern_id Pattern ID. + */ +function update_contains_block_types_meta( $pattern_id ) { + $pattern = get_post( $pattern_id ); + $blocks = parse_blocks( $pattern->post_content ); + $all_blocks = _flatten_blocks( $blocks ); + + // Get the list of block names and convert it to a single string. + $block_names = wp_list_pluck( $all_blocks, 'blockName' ); + $block_names = array_filter( $block_names ); // Filter out null values (extra line breaks). + $block_names = array_unique( $block_names ); + sort( $block_names ); + $used_blocks = implode( ',', $block_names ); + + update_post_meta( $pattern_id, 'wpop_contains_block_types', $used_blocks ); +} + +/** + * Determines if the current user can edit the given pattern post. + * + * This is a callback for the `auth_{$object_type}_meta_{$meta_key}` filter, and it's used to authorize access to + * modifying post meta keys via the REST API. + * + * @param bool $allowed + * @param string $meta_key + * @param int $pattern_id + * + * @return bool + */ +function can_edit_this_pattern( $allowed, $meta_key, $pattern_id ) { + return current_user_can( 'edit_post', $pattern_id ); +} + +/** + * Enqueue scripts for the block editor. + * + * @throws Error If the build files don't exist. + */ +function enqueue_editor_assets() { + if ( function_exists( 'get_current_screen' ) && POST_TYPE !== get_current_screen()->id ) { + return; + } + + $dir = dirname( dirname( __FILE__ ) ); + + $script_asset_path = "$dir/build/pattern-post-type.asset.php"; + if ( ! file_exists( $script_asset_path ) ) { + throw new Error( 'You need to run `npm run start:directory` or `npm run build:directory` for the Pattern Directory.' ); + } + + $script_asset = require $script_asset_path; + wp_enqueue_script( + 'wporg-pattern-post-type', + plugins_url( 'build/pattern-post-type.js', dirname( __FILE__ ) ), + $script_asset['dependencies'], + $script_asset['version'], + true + ); + + wp_set_script_translations( 'wporg-pattern-post-type', 'wporg-patterns' ); + + $locales = ( is_admin() ) ? get_locales_with_english_names() : get_locales_with_native_names(); + + wp_add_inline_script( + 'wporg-pattern-post-type', + 'var wporgLocaleData = ' . wp_json_encode( $locales ) . ';', + 'before' + ); + + wp_enqueue_style( + 'wporg-pattern-post-type', + plugins_url( 'build/pattern-post-type.css', dirname( __FILE__ ) ), + array(), + $script_asset['version'], + ); +} + +/** + * Restrict the set of blocks allowed in block patterns. + * + * @param bool|array $allowed_block_types Array of block type slugs, or boolean to enable/disable all. + * @param WP_Block_Editor_Context $block_editor_context The post resource data. + * + * @return bool|array A (possibly) filtered list of block types. + */ +function remove_disallowed_blocks( $allowed_block_types, $block_editor_context ) { + $disallowed_block_types = array( + // Remove blocks that don't make sense in Block Patterns + 'core/freeform', // Classic block + 'core/legacy-widget', + 'core/more', + 'core/nextpage', + 'core/block', // Reusable blocks + 'core/shortcode', + 'core/template-part', + ); + + if ( isset( $block_editor_context->post ) && POST_TYPE === $block_editor_context->post->post_type ) { + // This can be true if all block types are allowed, so to filter them we + // need to get the list of all registered blocks first. + if ( true === $allowed_block_types ) { + $allowed_block_types = array_keys( WP_Block_Type_Registry::get_instance()->get_all_registered() ); + } + $allowed_block_types = array_diff( $allowed_block_types, $disallowed_block_types ); + + // Remove the "WordPress.org" blocks, like Global Header & Global Footer. + $allowed_block_types = array_filter( + $allowed_block_types, + function ( $block_type ) { + return 'wporg/' !== substr( $block_type, 0, 6 ); + } + ); + } + + return is_array( $allowed_block_types ) ? array_values( $allowed_block_types ) : $allowed_block_types; +} + +/** + * Disable the block directory in wp-admin for patterns. + * + * The block directory file isn't loaded on the frontend, so this is only needed for site admins who can open + * the pattern in the "real" wp-admin editor. + */ +function disable_block_directory() { + if ( is_admin() && POST_TYPE === get_post_type() ) { + remove_action( 'enqueue_block_editor_assets', 'wp_enqueue_editor_block_directory_assets' ); + remove_action( 'enqueue_block_editor_assets', 'gutenberg_enqueue_block_editor_assets_block_directory' ); + } +} + +/** + * Filter the collection parameters: + * - set a new default for per_page. + * - add a new parameter, `author_name`, for a user's nicename slug. + * - add a new parameter, `curation`, to filter between curated, community, and all patterns. + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @return array Filtered parameters. + */ +function filter_patterns_collection_params( $query_params ) { + if ( isset( $query_params['per_page'] ) ) { + // Number of patterns per page, should be multiple of 2 and 3 (for 2- and 3-column layouts). + $query_params['per_page']['default'] = 18; + } + + $query_params['author_name'] = array( + 'description' => __( 'Limit result set to patterns by a single author.', 'wporg-patterns' ), + 'type' => 'string', + 'validate_callback' => function( $value ) { + $user = get_user_by( 'slug', $value ); + return (bool) $user; + }, + ); + + $query_params['curation'] = array( + 'description' => __( 'Limit result to either curated core, community, or all patterns.', 'wporg-patterns' ), + 'type' => 'string', + 'default' => 'all', + 'enum' => array( + 'all', + 'core', + 'community', + ), + ); + + if ( isset( $query_params['orderby'] ) ) { + $query_params['orderby']['enum'][] = 'favorite_count'; + } + + $query_params['wp-version'] = array( + 'description' => __( 'The version of the requesting site, used to filter out newer patterns.', 'wporg-patterns' ), + 'type' => 'string', + ); + + $query_params['allowed_blocks'] = array( + 'description' => __( 'Filter the request to only return patterns with blocks on this list.', 'wporg-patterns' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ); + + return $query_params; +} + +/** + * Filter the arguments passed to the pattern query in the API. + * + * @param array $args Array of arguments to be passed to WP_Query. + * @param WP_REST_Request $request The REST API request. + */ +function filter_patterns_rest_query( $args, $request ) { + $locale = $request->get_param( 'locale' ); + + // Prioritise results in the requested locale. + // Does not limit to only the requested locale, so as to provide results when no translations + // exist for the locale, or we do not recognise the locale. + if ( $locale && is_string( $locale ) ) { + $args['meta_query']['orderby_locale'] = array( + 'key' => 'wpop_locale', + 'compare' => 'IN', + // Order in value determines result order + 'value' => array( $locale, 'en_US' ), + ); + } + + // Use the `author_name` passed in to the API to request patterns by an author slug, not just an ID. + if ( isset( $request['author_name'] ) ) { + $user = get_user_by( 'slug', $request['author_name'] ); + if ( $user ) { + $args['author'] = $user->ID; + } else { + $args['post__in'] = array( -1 ); + } + } + + // If `curation` is passed and either `core` or `community`, we should + // filter the result. If `curation=all`, no filtering is needed. + if ( isset( $request['curation'] ) ) { + if ( 'core' === $request['curation'] ) { + // Patterns with the core keyword. + $args['tax_query']['core_keyword'] = array( + 'taxonomy' => 'wporg-pattern-keyword', + 'field' => 'slug', + 'terms' => 'core', + 'operator' => 'IN', + ); + } else if ( 'community' === $request['curation'] ) { + // Patterns without the core keyword. + $args['tax_query']['core_keyword'] = array( + 'taxonomy' => 'wporg-pattern-keyword', + 'field' => 'slug', + 'terms' => 'core', + 'operator' => 'NOT IN', + ); + } + } + + $orderby = $request->get_param( 'orderby' ); + if ( 'favorite_count' === $orderby ) { + $args['orderby'] = 'meta_value_num'; + $args['meta_key'] = 'wporg-pattern-favorites'; + } + + // Use the passed-in version information to skip over any patterns that + // require newer block features. + // See https://github.com/WordPress/gutenberg/issues/45179. + $version = $request->get_param( 'wp-version' ); + if ( $version && preg_match( '/^\d+\.\d+/', $version, $matches ) ) { + // $version is the full WP version, for example `6.0.2` or `6.2-alpha-54642-src`. + // Parse out just the major version section, `6.0` or `6.2`, respectively, + // so that the math comparison works. + $major_version = $matches[0]; + $args['meta_query']['version'] = array( + // Fetch patterns with no version info, or only those with a lower + // or equal version. + 'relation' => 'OR', + array( + 'key' => 'wpop_wp_version', + 'compare' => '<=', + 'value' => $major_version, + ), + array( + 'key' => 'wpop_wp_version', + 'compare' => 'NOT EXISTS', + ), + ); + } + + $allowed_blocks = $request->get_param( 'allowed_blocks' ); + if ( $allowed_blocks ) { + // Only return a pattern if all contained blocks are in the allowed blocks list. + $args['meta_query']['allowed_blocks'] = array( + 'key' => 'wpop_contains_block_types', + 'compare' => 'REGEXP', + 'value' => '^((' . implode( '|', $allowed_blocks ) . '),?)+$', + ); + } + + return $args; +} + +/** + * Filters the WP_Query orderby to prioritse the locale when required. + * + * @param string $orderby The SQL orderby clause. + * @param \WP_Query $query The WP_Query object. + * @return string The SQL orderby clause altered to prioritise locales if required. + */ +function filter_orderby_locale( $orderby, $query ) { + global $wpdb; + + // If this query has the orderby_locale meta_query, sort by it. + if ( ! empty( $query->meta_query->queries['orderby_locale']['value'] ) ) { + $values = array_reverse( $query->meta_query->queries['orderby_locale']['value'] ); + $table_alias = $query->meta_query->get_clauses()['orderby_locale']['alias']; + + $field_placeholders = implode( ', ', array_pad( array(), count( $values ), '%s' ) ); + $locale_orderby = $wpdb->prepare( "FIELD( {$table_alias}.meta_value, {$field_placeholders} ) DESC", $values ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + // Order by matching the locale first, and then the queries order. + $orderby = "{$locale_orderby}, {$orderby}"; + } + + return $orderby; +} + +/** + * Get the post object of a block pattern, or false if it's not a pattern or not found. + * + * @param int|WP_Post $post + * + * @return WP_Post|false + */ +function get_block_pattern( $post ) { + $pattern = get_post( $post ); + if ( ! $pattern || POST_TYPE !== $pattern->post_type ) { + return false; + } + return $pattern; +} + +/** + * Give all logged in users caps for creating patterns and related taxonomies. + * + * This allows any user in the wp.org network to have these capabilities, without having to have an actual + * role on the pattern directory site. These caps are only given on the front end, though, because in WP Admin + * these same caps could allow unintended access. + * + * @param array $user_caps A list of primitive caps (keys) and whether user has them (boolean values). + * + * @return array + */ +function set_pattern_caps( $user_caps ) { + // Set corresponding caps for all roles. + $cap_args = array( + 'capability_type' => array( 'pattern', 'patterns' ), + 'capabilities' => array(), + 'map_meta_cap' => true, + ); + $cap_map = (array) get_post_type_capabilities( (object) $cap_args ); + + // Users should have the same permissions for patterns as posts, for example, + // if they have `edit_posts`, they should be granted `edit_patterns`, and so on. + foreach ( $user_caps as $cap => $bool ) { + if ( $bool && isset( $cap_map[ $cap ] ) ) { + $user_caps[ $cap_map[ $cap ] ] = true; + } + } + + // Set caps to allow for front end pattern creation. + if ( is_user_logged_in() && ! is_admin() ) { + $user_caps['read'] = true; + $user_caps['publish_patterns'] = true; + $user_caps['edit_patterns'] = true; + $user_caps['edit_published_patterns'] = true; + $user_caps['delete_patterns'] = true; + $user_caps['delete_published_patterns'] = true; + // Note that `edit_others_patterns` & `delete_others_patterns` are separate capabilities. + } + + return $user_caps; +} + +/** + * Set up the `view` endpoint. + * + * Technically this applies to posts too, but this is easier than a custom EP mask. + */ +function add_preview_endpoint() { + add_rewrite_endpoint( 'view', EP_PERMALINK ); +} + +/** + * When viewing a `view` page, set up the preview theme. + * + * This should switch the theme to twentytwentyone, with a white background, + * and inject the image placeholder workaround. + */ +function setup_preview_theme() { + // query_vars are not set yet, so just check the URL. + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/'; + + // Match pretty & non-pretty permalinks for unpublished patterns. + if ( preg_match( '#/view/$#', $request_uri ) || preg_match( '#[?&]view=[1|true]#', $request_uri ) ) { + add_filter( 'show_admin_bar', '__return_false', 2000 ); + + add_filter( 'template', function() { + if ( 'local' === wp_get_environment_type() ) { + return 'twentytwentythree'; + } else { + return 'core/twentytwentythree'; + } + } ); + + add_filter( 'stylesheet', function() { + if ( 'local' === wp_get_environment_type() ) { + return 'twentytwentythree'; + } else { + return 'core/twentytwentythree'; + } + } ); + + add_filter( 'wp_enqueue_scripts', function() { + wp_deregister_style( 'wp4-styles' ); + wp_deregister_style( 'wporg-global-header-footer' ); + }, 201 ); + + add_filter( 'render_block_core/gallery', __NAMESPACE__ . '\inject_placeholder_svg', 10, 2 ); + add_filter( 'render_block_core/image', __NAMESPACE__ . '\inject_placeholder_svg', 10, 2 ); + add_filter( 'render_block_core/media-text', __NAMESPACE__ . '\inject_placeholder_svg', 10, 2 ); + add_filter( 'render_block_core/video', __NAMESPACE__ . '\inject_placeholder_svg', 10, 2 ); + add_filter( 'render_block_core/site-logo', __NAMESPACE__ . '\inject_placeholder_svg', 10, 2 ); + } +} + +/** + * Inject the placehodler SVG if we find an empty media block. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @return string The updated block content. + */ +function inject_placeholder_svg( $block_content, $block ) { + $svg = ''; + $svg .= ''; + $svg .= ''; + $svg .= ''; + + // Image block, find img without `src` or with wmark.png (logo), replace with svg. + if ( preg_match( '/]*)\/?>/', $block_content, $match ) ) { + if ( ! str_contains( $match[1], 'src=' ) || str_contains( $match[1], 'wmark.png' ) ) { + $new_content = str_replace( '`, inject svg into figure. + if ( preg_match( '/(]*>)(<\/figure>)/', $block_content, $match ) ) { + $new_content = $match[1] . $svg . $match[2]; + $block_content = str_replace( $match[0], $new_content, $block_content ); + } + } + + // Gallery, find empty `
`, inject 3 fake image blocks into figure. + if ( 'core/gallery' === $block['blockName'] && preg_match( '/(]*>)(<\/figure>)/', $block_content, $match ) ) { + $image = '
' . $svg . '
'; + $new_content = $match[1] . str_repeat( $image, 3 ) . $match[2]; + $block_content = str_replace( $match[0], $new_content, $block_content ); + } + + return $block_content; +} + +/** + * If this is the `view` query, use our version of the `template-canvas.php`. + */ +function load_pattern_preview( $template ) { + global $wp_query; + + if ( ! isset( $wp_query->query_vars['view'] ) ) { + return $template; + } + + return dirname( __DIR__ ) . '/views/view.php'; +} + +/** + * Intercept the post object and decode the content. + */ +add_action( + 'the_post', + function( $post ) { + $post->post_content = decode_pattern_content( $post->post_content ); + } +); + +/** + * Process post content, replacing broken encoding & removing refs. + * + * Some image URLs have &s, which are double-encoded and sanitized to become malformed, + * for example, `https://img.rawpixel.com/s3fs-private/rawpixel_images/website_content/a010-markuss-0964.jpg?w=1200\u0026amp;h=1200\u0026amp;fit=clip\u0026amp;crop=default\u0026amp;dpr=1\u0026amp;q=75\u0026amp;vib=3\u0026amp;con=3\u0026amp;usm=15\u0026amp;cs=srgb\u0026amp;bg=F4F4F3\u0026amp;ixlib=js-2.2.1\u0026amp;s=7d494bd5db8acc2a34321c15ed18ace5`. + * + * @param string $content The raw post content. + * + * @return string + */ +function decode_pattern_content( $content ) { + // Sometimes the initial `\` is missing, so look for both versions. + $content = str_replace( [ '\u0026amp;', 'u0026amp;' ], '&', $content ); + // Remove `ref` from all content. + $content = preg_replace( '/"ref":\d+,?/', '', $content ); + return $content; +} + +/** + * Given a post, return the unlisted reason (if one exists). + * + * @param int $post_id Post ID. + * + * @return string + */ +function get_pattern_unlisted_reason( $post_id ) { + $reasons = wp_get_object_terms( get_the_ID(), FLAG_REASON ); + if ( count( $reasons ) > 0 ) { + $reason = array_shift( $reasons ); + return $reason->description; + } + + return ''; +} + +/** + * Allow the Patterns to be included in the Jetpack Sitemaps. + * + * @param array $post_types The post types to include. + * + * @return array + */ +function jetpack_sitemap_post_types( $post_types ) { + $post_types[] = POST_TYPE; + + return $post_types; +} diff --git a/content/plugins/pattern-directory/includes/pattern-validation.php b/content/plugins/pattern-directory/includes/pattern-validation.php new file mode 100644 index 0000000..2163759 --- /dev/null +++ b/content/plugins/pattern-directory/includes/pattern-validation.php @@ -0,0 +1,442 @@ +/' ); + return trim( preg_replace( $to_replace, '', $html ) ); +} + +/** + * Check if a block has been edited by the user, as opposed to an empty/placeholder block. + * + * @param array $block A parsed block object. + * @return bool Whether the block has been edited. + */ +function is_not_empty_block( $block ) { + $registry = \WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( $block['blockName'] ); + + // Most dynamic blocks don't need custom content, but there are some + // exceptions that should go through the rest of the checks. + if ( + $block_type->is_dynamic() && + ! in_array( $block['blockName'], array( 'core/image' ) ) + ) { + return true; + } + + // Paragraphs are a special case, these should never be empty. + if ( 'core/paragraph' === $block['blockName'] ) { + $block_content = strip_basic_html( $block['innerHTML'] ); + if ( empty( $block_content ) ) { + return false; + } + } + + // Exceptions - these contain no content and maybe no attributes. + $allowed_empty = [ 'core/separator', 'core/spacer' ]; + if ( in_array( $block['blockName'], $allowed_empty ) ) { + return true; + } + + // Check if the attributes are different from the default attributes. + $block_attrs = $block_type->prepare_attributes_for_render( $block['attrs'] ); + $default_attrs = $block_type->prepare_attributes_for_render( array() ); + if ( $block_attrs != $default_attrs ) { + return true; + } + + // If there are any child blocks, check those. Only return if there are real child blocks, + // otherwise continue on to check for any other content. + if ( count( $block['innerBlocks'] ) >= 1 ) { + $child_blocks = array_filter( $block['innerBlocks'], __NAMESPACE__ . '\is_not_empty_block' ); + if ( count( $child_blocks ) ) { + return true; + } + } + + $block_content = strip_basic_html( $block['innerHTML'] ); + if ( ! empty( $block_content ) ) { + return true; + } + return false; +} + +/** + * Validate the pattern content. + */ +function validate_content( $prepared_post, $request ) { + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + // If post_content does not exist, this is just an update to an existing pattern. + if ( ! isset( $prepared_post->post_content ) ) { + return $prepared_post; + } + + $content = $prepared_post->post_content; + if ( ! $content ) { + return new \WP_Error( + 'rest_pattern_empty', + __( 'Pattern content cannot be empty.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + // The editor adds in linebreaks between blocks, but parse_blocks thinks those are invalid blocks. + $content = str_replace( "\n\n", '', $content ); + $blocks = parse_blocks( $content ); + $blocks_queue = $blocks; + $all_blocks = array(); + + // Loop over all the nested blocks to flatten the block list into 1 dimension. + while ( count( $blocks_queue ) > 0 ) { // phpcs:ignore -- inline count OK. + $block = array_shift( $blocks_queue ); + array_push( $all_blocks, $block ); + if ( ! empty( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $inner_block ) { + array_push( $blocks_queue, $inner_block ); + } + } + } + + // Check that each block in the list has a blockName and is registered. + $registry = \WP_Block_Type_Registry::get_instance(); + $invalid_blocks = array_filter( $all_blocks, function( $block ) use ( $registry ) { + $block_type = $registry->get_registered( $block['blockName'] ); + return is_null( $block['blockName'] ) || is_null( $block_type ); + } ); + + if ( count( $invalid_blocks ) ) { + return new \WP_Error( + 'rest_pattern_invalid_blocks', + __( 'Pattern content contains invalid blocks. Patterns shared on the Pattern Directory can only use core blocks.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + // Next, filter out any empty blocks + $real_blocks = array_filter( $all_blocks, __NAMESPACE__ . '\is_not_empty_block' ); + + // Check that we have at least one non-empty block. + if ( ! count( $real_blocks ) ) { + return new \WP_Error( + 'rest_pattern_empty_blocks', + __( 'Pattern content contains only empty or default blocks.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + // Check that we have at least three non-empty blocks (and show a different error message). + if ( count( $real_blocks ) < 3 ) { + return new \WP_Error( + 'rest_pattern_insufficient_blocks', + __( 'Pattern content contains less than three blocks. Patterns should combine multiple blocks for interesting layouts.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + // Check that there are fewer than 75 blocks. + if ( count( $real_blocks ) > 75 ) { + return new \WP_Error( + 'rest_pattern_extra_blocks', + __( 'Pattern content contains over 75 blocks. Patterns should not replicate full pages or blog posts, try breaking your pattern into smaller submissions.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + return $prepared_post; +} + +/** + * Validate the pattern title. + */ +function validate_title( $prepared_post, $request ) { + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + $status = isset( $request['status'] ) ? $request['status'] : get_post_status( $prepared_post->ID ); + // Bypass this validation for drafts. + if ( 'draft' === $status || 'auto-draft' === $status ) { + return $prepared_post; + } + + $title = isset( $request['title'] ) ? $request['title'] : get_the_title( $prepared_post->ID ); + + // A title exists, but is empty -- invalid. + if ( isset( $title ) && empty( trim( $title ) ) ) { + return new \WP_Error( + 'rest_pattern_empty_title', + __( 'A pattern title is required.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + if ( ! is_title_valid( $title ) ) { + return new \WP_Error( + 'rest_pattern_invalid_title', + __( 'Pattern title is invalid. The pattern title should describe the pattern.', 'wporg-patterns' ), + array( 'status' => 400 ) + ); + } + + return $prepared_post; +} + +/** + * Validate the pattern status. + * + * Ensures patterns created via the API have either a non-public status (draft, unlisted), + * or they use the chosen status set in /wp-admin/options-general.php?page=wporg-pattern-creator. + */ +function validate_status( $prepared_post, $request ) { + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + $post_type = get_post_type_object( POST_TYPE ); + $target_status = isset( $request['status'] ) ? $request['status'] : ''; + $current_status = get_post_status( $prepared_post->ID ); + + // Drafts or unlisted patterns are OK. + if ( in_array( $target_status, [ 'draft', 'auto-draft', UNLISTED_STATUS ] ) ) { + return $prepared_post; + } + + // No validation needed if there's no status change. + if ( $target_status === $current_status || '' === $target_status ) { + return $prepared_post; + } + + // Skip validation if the user is a moderator. + if ( current_user_can( $post_type->cap->edit_others_posts ) ) { + return $prepared_post; + } + + $default_status = get_option( 'wporg-pattern-default_status', 'publish' ); + $valid_states = array_unique( array( 'pending', SPAM_STATUS, $default_status ) ); + + // Make sure the target status is the expected status (publish or pending). + if ( ! in_array( $target_status, $valid_states, true ) ) { + return new \WP_Error( + 'rest_pattern_invalid_status', + sprintf( + __( 'Invalid post status. Status must be %s.', 'wporg-patterns' ), + $default_status + ), + array( 'status' => 400 ) + ); + } + + // Do not allow for non-privledged users to move a spam post to another status. + if ( SPAM_STATUS === $current_status && SPAM_STATUS !== $target_status ) { + return new \WP_Error( + 'rest_pattern_invalid_status', + sprintf( + __( 'Invalid post status. Status must be %s.', 'wporg-patterns' ), + SPAM_STATUS + ), + array( 'status' => 400 ) + ); + } + + return $prepared_post; +} + +/** + * Validate the pattern doesn't appear to be spam. + */ +function validate_against_spam( $prepared_post, $request ) { + if ( is_wp_error( $prepared_post ) ) { + return $prepared_post; + } + + $target_status = isset( $request['status'] ) ? $request['status'] : ''; + + // Run spam checks for publish & pending patterns. + if ( 'publish' !== $target_status && 'pending' !== $target_status ) { + return $prepared_post; + } + + $post = get_post( $prepared_post->ID ); + + $pattern = array( + 'ID' => $post->ID, + 'post_name' => $post->post_name, + 'post_author' => $post->post_author, + 'title' => $prepared_post->post_title ?? $post->post_title, + 'content' => $prepared_post->post_content ?? $post->post_content, + 'description' => $request['meta']['wpop_description'] ?? ( $post->wpop_description ?: '' ), + 'keywords' => $request['meta']['wpop_keywords'] ?? ( $post->wpop_keywords ?: '' ), + ); + + list( $is_spam, $spam_reason ) = check_for_spam( $pattern ); + + // If it's been detected as spam, flag it as pending-review. + if ( $is_spam ) { + $prepared_post->post_status = SPAM_STATUS; + + // Add a note explaining why this post is in pending, if it's due to spam. + if ( function_exists( '\WordPressdotorg\InternalNotes\create_note' ) ) { + \WordPressdotorg\InternalNotes\create_note( + $prepared_post->ID, + array( + 'post_author' => get_user_by( 'login', 'wordpressdotorg' )->ID ?? 0, + 'post_excerpt' => $spam_reason, + ) + ); + } + } + + return $prepared_post; +} + +/** + * Helper function to check for spam. + * + * @param array $post + * @return array { + * @type boolean $is_spam + * @type string $spam_reason + * } + */ +function check_for_spam( $post ) { + // Stringify. + if ( ! class_exists( '\WordPressdotorg\Pattern_Translations\Pattern' ) ) { + // This is just a fall-back for local environments where the Translator isn't active. + // not designed to be used in production. + $strings = array( + $post['title'], + $post['description'], + wp_strip_all_tags( $post['content'] ), + $post['keywords'], + ); + } else { + $pattern = new Translations_Pattern(); + $pattern->ID = $post['ID']; + $pattern->title = $post['title']; + $pattern->name = $post['post_name']; + $pattern->description = $post['description']; + $pattern->keywords = $post['keywords']; + $pattern->html = $post['content']; + $pattern->locale = get_locale(); + + $parser = new Translations_PatternParser( $pattern ); + $strings = $parser->to_strings(); + } + + // Combine strings for ease of use. + $combined_strings = implode( "\n", $strings ); + + // Not yet detected as spam. + $is_spam = false; + $spam_reason = ''; + + // Treat Paragraph-only submissions as likely spam. + if ( ! $is_spam ) { + // Only fetches the top-level of blocks, we're only + $block_names_in_use = array_filter( + array_unique( + wp_list_pluck( + parse_blocks( $post['content'] ), + 'blockName' + ) + ) + ); + + if ( array( 'core/paragraph' ) === $block_names_in_use ) { + $is_spam = true; + $spam_reason = 'Only contains Paragraph blocks.'; + } + } + + // Run it past Akismet. + if ( ! $is_spam && is_callable( array( 'Akismet', 'rest_auto_check_comment' ) ) ) { + $author = get_user_by( 'ID', $post['post_author'] ); + if ( ! $author ) { + $author = wp_get_current_user(); + } + + $akismet_payload = array( + 'comment_post_ID' => 0, + 'comment_type' => 'pattern_submission', + // Disabled as logged in users get bonus points I think, which we don't want. + // 'user_ID' => get_current_user_id(), + 'comment_author' => $author->display_name ?: $author->user_login, + 'comment_author_email' => $author->user_email, + 'comment_author_url' => '', + 'comment_content' => $combined_strings, + 'comment_content_raw' => $post['content'], + 'permalink' => get_permalink( $post ), + ); + + $akismet = \Akismet::rest_auto_check_comment( $akismet_payload ); + if ( is_wp_error( $akismet ) ) { + $akismet = array( 'akismet_result' => 'discard' ); + } + + $is_spam = ( + isset( $akismet['akismet_result'] ) && + // true: spam, discard: 100% spam no-question. + ( 'true' === $akismet['akismet_result'] || 'discard' === $akismet['akismet_result'] ) + ); + if ( $is_spam ) { + $spam_reason = 'Akismet has detected this Pattern as spam.'; + } + } + + // Testing keyword. Case-sensitive. + if ( ! $is_spam && str_contains( $combined_strings, 'PatternDirectorySpamTest' ) ) { + $is_spam = true; + $spam_reason = 'Includes the spam trigger word: PatternDirectorySpamTest'; + } + + return array( $is_spam, $spam_reason ); +} + +/** + * Helper function to check for a valid pattern title. + * + * @param string $title + * @return boolean + */ +function is_title_valid( $title ) { + // Check title against a list of disallowed words. + // Note the space after `test ` to avoid matching "testimonial". + $disallow_list = array( 'test ', 'testing', 'my pattern', 'wordpress', 'example' ); + + if ( 'test' === strtolower( $title ) ) { + return false; + } + + foreach ( $disallow_list as $disallowed ) { + if ( false !== stripos( $title, $disallowed ) ) { + return false; + } + } + + return true; +} diff --git a/content/plugins/pattern-directory/includes/search.php b/content/plugins/pattern-directory/includes/search.php new file mode 100644 index 0000000..509eb2a --- /dev/null +++ b/content/plugins/pattern-directory/includes/search.php @@ -0,0 +1,204 @@ +is_search() && POST_TYPE === $query->get( 'post_type' ); +} + +/** + * Customize the ES query for patterns. + * + * @see `should_handle_query` has preconditions for this function. + * + * @param array $es_query_args The raw Elasticsearch query args. + * @param WP_Query $wp_query The original WP_Query object. + * + * @return array + */ +function modify_es_query_args( $es_query_args, $wp_query ) { + $user_query = $wp_query->get( 's' ); + $meta_query = $wp_query->get( 'meta_query' ); + $locales = [ 'en_US' ]; + + if ( ! empty( $meta_query['orderby_locale']['value'] ) ) { + $locales = array_unique( $meta_query['orderby_locale']['value'] ); + } + + $parser = new Jetpack_WPES_Search_Query_Parser( $wp_query, array() ); + + $must_query = [ + 'multi_match' => [ + 'query' => $user_query, + 'fields' => [ 'title_en', 'meta.wpop_description.value' ], + 'boost' => 0.1, + 'operator' => 'and', + ], + ]; + + $should_query = [ + [ + 'multi_match' => [ + 'query' => $user_query, + 'fields' => [ 'title_en' ], + 'boost' => 2, + 'type' => 'phrase', + ], + ], + + [ + 'multi_match' => [ + // The `description_en` field in the ES index is actually `post_content`, but that's not + // relevant in this context, since that's just sample content. The `wpop_description` + // field is the actual description that should be searched. + 'fields' => [ 'meta.wpop_description.value' ], + 'query' => $user_query, + 'type' => 'phrase', + ], + ], + ]; + + // Requests for a specific locale will still include `en_US` as a fallback. + if ( count( $locales ) > 1 ) { + $primary_locale = array_reduce( $locales, function( $carry, $item ) { + // This assumes there will only be 2 items in $locale. + if ( 'en_US' !== $item ) { + $carry = $item; + } + + return $carry; + } ); + + // Boost the primary locale over the `en_US` fallback. + $should_query[] = [ + 'boosting' => [ + 'positive' => [ + 'term' => [ + 'meta.wpop_locale.value.raw' => $primary_locale, + ], + ], + 'negative' => [ + 'term' => [ + 'meta.wpop_locale.value.raw' => 'en_US', + ], + ], + 'negative_boost' => 0.001, + ], + ]; + } + + $filter = [ + 'bool' => [ + 'must' => [ + [ 'term' => [ 'post_type' => 'wporg-pattern' ] ], + [ 'terms' => [ 'meta.wpop_locale.value.raw' => $locales ] ], + ], + ], + ]; + + $tax_query = $wp_query->get( 'tax_query' ); + if ( $tax_query ) { + foreach ( $tax_query as $term ) { + $taxonomy = $term['taxonomy']; + + // `wporg-pattern-flag-reason` is private. + if ( ! in_array( $taxonomy, array( 'wporg-pattern-category', 'wporg-pattern-keyword' ) ) ) { + continue; + } + + $filter['bool']['must'][] = [ + 'terms' => [ "taxonomy.$taxonomy.term_id" => $term['terms'] ], + ]; + } + } + + $parser->add_query( $must_query, 'must' ); + $parser->add_query( $should_query, 'should' ); + $parser->add_filter( $filter ); + + $es_query_args['query'] = $parser->build_query(); + $es_query_args['filter'] = $parser->build_filter(); + + $es_query_args['sort'] = [ + [ + '_score' => [ + 'order' => 'desc', + ], + ], + ]; + + return $es_query_args; +} + +/** + * Log when Jetpack does not run the query. + * + * @param string $reason + * @param array $data + */ +function log_aborted_queries( $reason, $data ) { + if ( defined( 'WPORG_SANDBOXED' ) && WPORG_SANDBOXED ) { + wp_send_json_error( array( 'jetpack_search_abort - ' . $reason, $data ) ); + } else { + trigger_error( 'jetpack_search_abort - ' . $reason . ' - ' . wp_json_encode( $data ), E_USER_WARNING ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +/** + * Log when Jetpack gets an error running the query. + * + * This filter doesn't currently work, but should in the future. + * See https://github.com/Automattic/jetpack/issues/18888 + * + * @param array $data + */ +function log_failed_queries( $data ) { + if ( defined( 'WPORG_SANDBOXED' ) && WPORG_SANDBOXED ) { + wp_send_json_error( array( 'failed_jetpack_search_query', $data ) ); + } else { + trigger_error( 'failed_jetpack_search_query - ' . wp_json_encode( $data ), E_USER_WARNING ); + } +} diff --git a/content/plugins/pattern-directory/includes/stats.php b/content/plugins/pattern-directory/includes/stats.php new file mode 100644 index 0000000..3279822 --- /dev/null +++ b/content/plugins/pattern-directory/includes/stats.php @@ -0,0 +1,481 @@ + __( 'Pattern Stats Snapshot', 'wporg-patterns' ), + 'public' => false, + 'show_in_rest' => false, + 'capability-type' => STATS_POST_TYPE, + 'capabilities' => array( + 'create_posts' => 'do_not_allow', + ), + 'supports' => array( 'title', 'custom-fields' ), + ) + ); +} + +/** + * Define the post meta fields for the snapshot CPT. + * + * ⚠️ When you change the field schema, make sure you also bump up the VERSION constant at the top of this file. + * + * @return array + */ +function get_meta_field_schema() { + return array( + 'type' => 'object', + 'properties' => array( + 'count-patterns' => array( + 'description' => __( 'The total number of pattern posts.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_publish' => array( + 'description' => __( 'The total number of published patterns.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_publish-originals' => array( + 'description' => __( 'The total number of published original patterns (not translations or remixes).', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_publish-translations' => array( + 'description' => __( 'The total number of published pattern translations.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_possible-spam' => array( + 'description' => __( 'The total number of possibly spam patterns.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_unlisted' => array( + 'description' => __( 'The total number of unlisted patterns.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_unlisted-spam' => array( + 'description' => __( 'The total number of patterns unlisted due to spam.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_favorited' => array( + 'description' => __( 'The total number of patterns with at least one favorite.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-patterns_flagged-pending' => array( + 'description' => __( 'The total number of patterns with a pending flag.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-favorites' => array( + 'description' => __( 'The total number of favorites.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-flags' => array( + 'description' => __( 'The total number of flags.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-flags_pending' => array( + 'description' => __( 'The total number of pending flags.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-flags_resolved' => array( + 'description' => __( 'The total number of resolved flags.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'count-users_with-favorite' => array( + 'description' => __( 'The total number of users with at least one favorited pattern.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'elapsed-time' => array( + 'description' => __( 'Number of milliseconds to generate the snapshot.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + 'version' => array( + 'description' => __( 'The version of the snapshot data schema.', 'wporg-patterns' ), + 'type' => 'integer', + 'single' => true, + ), + ), + ); +} + +/** + * Register the post meta fields for the snapshot CPT. + * + * @return void + */ +function register_meta_fields() { + $schema = get_meta_field_schema(); + + foreach ( $schema['properties'] as $field_name => $field_schema ) { + register_post_meta( STATS_POST_TYPE, $field_name, $field_schema ); + } +} + +/** + * Schedule the cron job to record a stats snapshot. + * + * @return void + */ +function schedule_cron_job() { + if ( defined( 'WPORG_SANDBOXED' ) && WPORG_SANDBOXED ) { + return; + } + + if ( wp_next_scheduled( PATTERN_POST_TYPE . '_record_snapshot' ) ) { + return; + } + + // Schedule a repeating "single" event to avoid having to create a custom schedule. + wp_schedule_single_event( + strtotime( 'tomorrow' ), // 00:00 UTC. + PATTERN_POST_TYPE . '_record_snapshot' + ); +} + +/** + * Generate a snapshot post. + * + * @return void + */ +function record_snapshot() { + if ( defined( 'WPORG_SANDBOXED' ) && WPORG_SANDBOXED ) { + return; + } + + $data = get_snapshot_data(); + + wp_insert_post( + array( + 'post_type' => STATS_POST_TYPE, + 'post_author' => 0, + 'post_title' => gmdate( 'Y-m-d' ), + 'post_status' => 'publish', + 'meta_input' => $data, + ), + true + ); +} + +/** + * Generate the data that will be added to a snapshot post as post meta. + * + * @return array + */ +function get_snapshot_data() { + $data = array(); + $start_ms = round( microtime( true ) * 1000 ); + $schema = get_meta_field_schema(); + + foreach ( array_keys( $schema['properties'] ) as $field_name ) { + $func = __NAMESPACE__ . '\\callback_' . str_replace( '-', '_', $field_name ); + + if ( is_callable( $func ) ) { + $data[ $field_name ] = call_user_func( $func ); + } + } + + $elapsed_ms = round( microtime( true ) * 1000 ) - $start_ms; + + $data['elapsed-time'] = (int) $elapsed_ms; + $data['version'] = VERSION; + + return $data; +} + +/** + * Count the total number of pattern posts. + * + * @return int + */ +function callback_count_patterns() { + $patterns_by_status = (array) wp_count_posts( PATTERN_POST_TYPE ); + + // Exclude auto drafts since they aren't really pattern posts yet. + unset( $patterns_by_status['auto-draft'] ); + + return (int) array_sum( $patterns_by_status ); +} + +/** + * Count the total number of published patterns. + * + * @return int + */ +function callback_count_patterns_publish() { + $patterns_by_status = wp_count_posts( PATTERN_POST_TYPE ); + + return $patterns_by_status->publish; +} + +/** + * Count the total number of published original patterns (not translations or remixes). + * + * @return int + */ +function callback_count_patterns_publish_originals() { + $args = array( + 'post_type' => PATTERN_POST_TYPE, + 'post_status' => 'publish', + 'post_parent' => 0, + 'numberposts' => 1, + ); + + $query = new \WP_Query( $args ); + + return $query->found_posts; +} + +/** + * Count the total number of published pattern translations and remixes. + * + * @return int + */ +function callback_count_patterns_publish_translations() { + $args = array( + 'post_type' => PATTERN_POST_TYPE, + 'post_status' => 'publish', + 'numberposts' => 1, + 'meta_query' => array( + array( + 'key' => 'wpop_is_translation', + 'value' => 1, + ), + ), + ); + + $query = new \WP_Query( $args ); + + return $query->found_posts; +} + +/** + * Count the total number of pending-review patterns. + * + * @return int + */ +function callback_count_patterns_possible_spam() { + $patterns_by_status = wp_count_posts( PATTERN_POST_TYPE ); + + return $patterns_by_status->{'pending-review'}; +} + +/** + * Count the total number of unlisted patterns. + * + * @return int + */ +function callback_count_patterns_unlisted() { + $patterns_by_status = wp_count_posts( PATTERN_POST_TYPE ); + + return $patterns_by_status->unlisted; +} + +/** + * Count the total number of patterns with the unlist reason "spam". + * + * @return int + */ +function callback_count_patterns_unlisted_spam() { + $args = array( + 'post_type' => PATTERN_POST_TYPE, + 'post_status' => UNLISTED_STATUS, + 'tax_query' => array( + array( + 'taxonomy' => FLAG_REASON, + 'field' => 'slug', + 'terms' => '4-spam', + ), + ), + 'numberposts' => 1, // We only need the `found_posts` value here. + ); + + $query = new \WP_Query( $args ); + + return $query->found_posts; +} + +/** + * Count the total number of patterns with at least one favorite. + * + * @return int + */ +function callback_count_patterns_favorited() { + $args = array( + 'post_type' => PATTERN_POST_TYPE, + 'post_status' => 'any', + 'meta_query' => array( + array( + 'key' => FAVORITE_META_KEY, + 'value' => 0, + 'compare' => '>', + ), + ), + 'numberposts' => 1, // We only need the `found_posts` value here. + ); + + $query = new \WP_Query( $args ); + + return $query->found_posts; +} + +/** + * Count the total number of patterns with a pending flag. + * + * @return int + */ +function callback_count_patterns_flagged_pending() { + $post_ids = get_pattern_ids_with_pending_flags(); + + return count( $post_ids ); +} + +/** + * Count the total number of favorites. + * + * @return int + */ +function callback_count_favorites() { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $count = $wpdb->get_var( $wpdb->prepare( + " + SELECT COUNT(*) + FROM {$wpdb->usermeta} + WHERE meta_key=%s + ", + FAVORITE_META_KEY, + ) ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return absint( $count ); +} + +/** + * Count the total number of flags. + * + * @return int + */ +function callback_count_flags() { + $flags_by_status = (array) wp_count_posts( FLAG_POST_TYPE ); + + return (int) array_sum( $flags_by_status ); +} + +/** + * Count the total number of pending flags. + * + * @return int + */ +function callback_count_flags_pending() { + $flags_by_status = wp_count_posts( FLAG_POST_TYPE ); + + return $flags_by_status->pending; +} + +/** + * Count the total number of resolved flags. + * + * @return int + */ +function callback_count_flags_resolved() { + $flags_by_status = wp_count_posts( FLAG_POST_TYPE ); + + return $flags_by_status->resolved; +} + +/** + * Count the total number of users with at least one favorited pattern. + * + * @return int + */ +function callback_count_users_with_favorite() { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $user_ids = $wpdb->get_col( $wpdb->prepare( + " + SELECT DISTINCT user_id + FROM {$wpdb->usermeta} + WHERE meta_key=%s + ", + FAVORITE_META_KEY, + ) ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + return count( $user_ids ); +} + +/** + * Get snapshot posts. + * + * @param array $args Optional. Query args to refine the dataset. + * @param bool $wp_query Optional. True to return the WP_Query object instead of an array of post objects. + * + * @return int[]|\WP_Post[]|\WP_Query + */ +function get_snapshots( $args = array(), $wp_query = false ) { + $args = wp_parse_args( + $args, + array( + 'orderby' => 'date', + 'order' => 'asc', + ) + ); + + $args['post_type'] = STATS_POST_TYPE; + $args['post_status'] = 'publish'; + + $query = new \WP_Query( $args ); + + if ( true === $wp_query ) { + return $query; + } + + return $query->get_posts(); +} diff --git a/content/plugins/pattern-directory/views/admin-settings.php b/content/plugins/pattern-directory/views/admin-settings.php new file mode 100644 index 0000000..aeb7883 --- /dev/null +++ b/content/plugins/pattern-directory/views/admin-settings.php @@ -0,0 +1,22 @@ + + +
+

+ +
+ +
+
diff --git a/content/plugins/pattern-directory/views/admin-stats.php b/content/plugins/pattern-directory/views/admin-stats.php new file mode 100644 index 0000000..2c936d1 --- /dev/null +++ b/content/plugins/pattern-directory/views/admin-stats.php @@ -0,0 +1,195 @@ + + + +
+

+ +

+ +

+ This page is a work in progress. Someday there might be charts! +

+ +

+ Right now +

+ + + + + + + + + + + $field_schema ) : ?> + + + + + + + +
+ Meta Key + + Description + + Value +
+ + + + + + + + + + + + Data missing. + +
+ +

+ Snapshots +

+ +

+ Snapshot frequency should be daily at around 00:00 UTC. + + + timestamp ) ) + ); + ?> + + No snapshot is currently scheduled. + + +

+ + + + + + + + + + + + + + + + +
+ Number of snapshots + + +
+ Earliest snapshot + + 0 ) : ?> + + + No data. + +
+ Latest snapshot + + 0 ) : ?> + + + No data. + +
+ + 0 ) : ?> +

+ Export +

+

+ Choose a date range: +

+
+
+
+ + +
+ +
+ + +
+
+ + + + +
+ +
diff --git a/content/plugins/pattern-directory/views/view.php b/content/plugins/pattern-directory/views/view.php new file mode 100644 index 0000000..34623fa --- /dev/null +++ b/content/plugins/pattern-directory/views/view.php @@ -0,0 +1,76 @@ + +
+ +
+ +HTML; + +$template_html = get_the_block_template_html(); +remove_site_data_filters(); + +?> +> + + + + + + +> + + + + + + + From 2e9cadf6cfc10d30f894ce66d098e413debc41b3 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:35:56 +0100 Subject: [PATCH 04/10] Add "Pattern Translations" plugin. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- .../includes/cli-commands.php | 143 +++++++++++++ .../pattern-translations/includes/cron.php | 132 ++++++++++++ .../pattern-translations/includes/i18n.php | 25 +++ .../pattern-translations/includes/makepot.php | 190 ++++++++++++++++++ .../pattern-translations/includes/parser.php | 171 ++++++++++++++++ .../includes/parsers/BasicText.php | 58 ++++++ .../includes/parsers/BlockParser.php | 109 ++++++++++ .../includes/parsers/Button.php | 54 +++++ .../includes/parsers/Heading.php | 39 ++++ .../includes/parsers/Noop.php | 12 ++ .../includes/parsers/Paragraph.php | 39 ++++ .../includes/parsers/ShortcodeBlock.php | 83 ++++++++ .../includes/parsers/TextNode.php | 34 ++++ .../pattern-translations/includes/pattern.php | 149 ++++++++++++++ .../pattern-translations.php | 150 ++++++++++++++ 15 files changed, 1388 insertions(+) create mode 100644 content/plugins/pattern-translations/includes/cli-commands.php create mode 100644 content/plugins/pattern-translations/includes/cron.php create mode 100644 content/plugins/pattern-translations/includes/i18n.php create mode 100644 content/plugins/pattern-translations/includes/makepot.php create mode 100644 content/plugins/pattern-translations/includes/parser.php create mode 100644 content/plugins/pattern-translations/includes/parsers/BasicText.php create mode 100644 content/plugins/pattern-translations/includes/parsers/BlockParser.php create mode 100644 content/plugins/pattern-translations/includes/parsers/Button.php create mode 100644 content/plugins/pattern-translations/includes/parsers/Heading.php create mode 100644 content/plugins/pattern-translations/includes/parsers/Noop.php create mode 100644 content/plugins/pattern-translations/includes/parsers/Paragraph.php create mode 100644 content/plugins/pattern-translations/includes/parsers/ShortcodeBlock.php create mode 100644 content/plugins/pattern-translations/includes/parsers/TextNode.php create mode 100644 content/plugins/pattern-translations/includes/pattern.php create mode 100644 content/plugins/pattern-translations/pattern-translations.php diff --git a/content/plugins/pattern-translations/includes/cli-commands.php b/content/plugins/pattern-translations/includes/cli-commands.php new file mode 100644 index 0000000..c9a1a40 --- /dev/null +++ b/content/plugins/pattern-translations/includes/cli-commands.php @@ -0,0 +1,143 @@ +] [--post-ids=] [--post-slugs=] [--all-posts] [--locale=] + */ + public function json( $_, $args ) { + $patterns = $this->get_patterns_or_exit( $args ); + + // Flatten parent to just being the slug. + array_walk( $patterns, function( $pattern ) { + $pattern->parent = $pattern->parent->name ?? $pattern->parent; + } ); + + WP_CLI::log( json_encode( $patterns, JSON_PRETTY_PRINT ) ); + } + + /** + * Output the combined HTML of the selected patterns, optionally localized + * + * @example wp patterns html --post=example + * @subcommand html + * @synopsis [--post=] [--post-ids=] [--post-slugs=] [--all-posts] [--locale=] + */ + public function html( $_, $args ) { + $patterns = $this->get_patterns_or_exit( $args ); + WP_CLI::log( implode( "\n\n", array_column( $patterns, 'html' ) ) ); + } + + /** + * Output the extracted strings of the selected patterns, optionally localized + * + * @example wp patterns strings --post=example + * @subcommand strings + * @synopsis [--post=] [--post-ids=] [--post-slugs=] [--all-posts] [--locale=] + */ + public function strings( $_, $args ) { + $patterns = $this->get_patterns_or_exit( $args ); + + $strings = []; + + foreach ( $patterns as $pattern ) { + $parser = new PatternParser( $pattern ); + $strings = array_merge( $strings, $parser->to_strings() ); + } + + WP_CLI::log( implode( "\n", array_unique( $strings ) ) ); + } + + /** + * Output a .pot file for tranlsation + * + * @example wp patterns makepot + * @subcommand makepot + * @synopsis [--post=] [--post-ids=] [--post-slugs=] [--all-posts] + */ + public function makepot( $_, $args ) { + $patterns = $this->get_patterns_or_exit( $args ); + + $makepot = new PatternMakepot( $patterns ); + + $po = $makepot->makepo(); + + WP_CLI::log( $po->export() ); + exit( 0 ); + } + + /** + * Import the selected patterns strings into glotpress + * + * @example wp --url=https://translate.wordpress.org/ patterns glotpress-import + * @subcommand glotpress-import + * @synopsis [--post=] [--post-ids=] [--post-slugs=] [--all-posts] [--save] + */ + public function glotpress_import( $_, $args ) { + $patterns = $this->get_patterns_or_exit( $args ); + + $makepot = new PatternMakepot( $patterns ); + WP_CLI::log( $makepot->import( isset( $args['save'] ) ) ); + exit( 0 ); + } + + /** + * All patterns commands accept an array of patterns and all commands leverage the same pattern selection flags. + */ + private function get_patterns_or_exit( $args ) { + $post = $args['post'] ?? false; + $post_ids = $args['post-ids'] ?? false; + $post_slugs = $args['post-slugs'] ?? false; + $all_posts = isset( $args['all-posts'] ); + $locale = $args['locale'] ?? false; + + $query = []; + + if ( false !== $post && ctype_digit( $post ) ) { + $query['p'] = $post; + } elseif ( false !== $post && ! ctype_digit( $post ) ) { + $query['name'] = $post; + } elseif ( false !== $post_ids ) { + $post_ids = explode( ',', $post_ids ); + $post_ids = array_filter( $post_ids, 'ctype_digit' ); + $query['post__in'] = $post_ids; + $query['orderby'] = 'post__in'; // Ensure mostly repeatable - todo? refactor this code into lib so its testable + } elseif ( false !== $post_slugs ) { + $query['post_name__in'] = explode( ',', $post_slugs ); + $query['orderby'] = 'post_name__in'; // Ensure mostly repeatable + } elseif ( $all_posts ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElseif -- empty state OK. + // send it all + } else { + WP_CLI::error( 'A post selector is required, did you mean to add --all-posts to this command? ' ); + exit( 1 ); + } + + $query['post_status'] = 'publish'; + + $patterns = Pattern::get_patterns( $query ); + + if ( empty( $patterns ) ) { + WP_CLI::error( 'No patterns found for args: ' . json_encode( $args ) . ', query: ' . json_encode( $query ) ); + exit( 1 ); + } + + if ( $locale ) { + $patterns = translate_patterns_to( $patterns, $locale ); + } + + return $patterns; + } + +} diff --git a/content/plugins/pattern-translations/includes/cron.php b/content/plugins/pattern-translations/includes/cron.php new file mode 100644 index 0000000..f910c28 --- /dev/null +++ b/content/plugins/pattern-translations/includes/cron.php @@ -0,0 +1,132 @@ +import( true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} +add_action( 'pattern_import_to_glotpress', __NAMESPACE__ . '\pattern_import_to_glotpress' ); + +/** + * Sync/Create translated patterns of GlotPress translated patterns. + * + * This creates the "forked" patterns of a parent pattern when translations are available. + * This queues sub-tasks which each process a CHUNK_SIZE group of patterns, to avoid memory exhaustion. + * These subtasks are spread between now and the next time this cron is expected to run. + * + * @param int[] $pattern_ids Optional. An array of Pattern IDs to process. + * If not provided, queues sub-tasks if in cron context, else processes all patterns. + */ +function pattern_import_translations_to_directory( $pattern_ids = array() ) { + if ( ! $pattern_ids ) { + $pattern_ids = Pattern::get_patterns( [ 'fields' => 'ids' ] ); + + if ( wp_doing_cron() ) { + // Chunk the patterns to avoid memory exhaustion. + $timestamp = time(); + $chunks = array_chunk( $pattern_ids, CHUNK_SIZE ); + // Spread out the sub-tasks over the entire twicedaily period. + $delay = floor( ( 12 * HOUR_IN_SECONDS ) / count( $chunks ) ); + foreach ( $chunks as $chunk ) { + wp_schedule_single_event( $timestamp, current_action(), array( $chunk ) ); + + $timestamp += $delay; + } + + printf( "Queued %d cron jobs of %d Patterns each.\n", count( $pattern_ids ) / CHUNK_SIZE, CHUNK_SIZE ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return; + } + } + + // See https://github.com/WordPress/gutenberg/issues/59300 + remove_action( 'registered_post_type', 'gutenberg_block_core_navigation_link_register_post_type_variation' ); + remove_action( 'registered_taxonomy', 'gutenberg_block_core_navigation_link_register_taxonomy_variation' ); + + // Raise the memory limit for this process to at least 512M. + add_filter( + 'cron_memory_limit', + function() { + return '512M'; + } + ); + wp_raise_memory_limit( 'cron' ); + + $locales = get_locales(); + + printf( "Processing %d Patterns in %d locales.\n", count( $pattern_ids ), count( $locales ) ); + + foreach ( $pattern_ids as $i => $pattern_id ) { + $pattern = Pattern::from_post( get_post( $pattern_id ) ); + + echo "{$i}. Processing {$pattern->name} / '{$pattern->title}'..\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + foreach ( $locales as $gp_locale ) { + $locale = $gp_locale->wp_locale; + if ( ! $locale || 'en_US' === $locale ) { + continue; + } + + $translated = $pattern->to_locale( $locale ); + if ( $translated ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo "\t{$locale} - " . ( $translated->ID ? 'Updating' : 'Creating' ) . " Translated pattern.\n"; + create_or_update_translated_pattern( $translated ); + } else { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo "\t{$locale} - No Translations exist yet.\n"; + // TODO: Note: There may exist a translated pattern using old strings. + // Considering this as an edge-case that is unlikely and we don't + // need to handle. Serving old Translated template is better in this case. + } + } + + // Clear memory-heavy variables after each iteration of Patterns, to avoid object cache memory exhaustion. + clear_memory_heavy_variables(); + } +} +add_action( 'pattern_import_translations_to_directory', __NAMESPACE__ . '\pattern_import_translations_to_directory' ); + +/** + * Clear caches for memory management. + * + * @static + * @global \wpdb $wpdb + * @global \WP_Object_Cache $wp_object_cache + */ +function clear_memory_heavy_variables() { + global $wpdb, $wp_object_cache; + + $wpdb->queries = []; + + if ( is_object( $wp_object_cache ) ) { + $wp_object_cache->cache = []; + $wp_object_cache->group_ops = []; + $wp_object_cache->memcache_debug = []; + } +} diff --git a/content/plugins/pattern-translations/includes/i18n.php b/content/plugins/pattern-translations/includes/i18n.php new file mode 100644 index 0000000..bb5152e --- /dev/null +++ b/content/plugins/pattern-translations/includes/i18n.php @@ -0,0 +1,25 @@ +to_locale( $locale ) ?: $pattern; + }, $patterns ); +} diff --git a/content/plugins/pattern-translations/includes/makepot.php b/content/plugins/pattern-translations/includes/makepot.php new file mode 100644 index 0000000..e5fc0ef --- /dev/null +++ b/content/plugins/pattern-translations/includes/makepot.php @@ -0,0 +1,190 @@ +patterns = $patterns; + } + + public function makepot( $revision_time = null ) : string { + return $this->makepo( $revision_time, $comment )->export(); + } + + public function makepo( $revision_time = null ) : \PO { + require_once ABSPATH . '/wp-includes/pomo/po.php'; + + $po = new \PO(); + + $po->set_header( 'PO-Revision-Date', gmdate( 'Y-m-d H:i:s', $revision_time ?? time() ) . '+0000' ); + $po->set_header( 'MIME-Version', '1.0' ); + $po->set_header( 'Content-Type', 'text/plain; charset=UTF-8' ); + $po->set_header( 'Content-Transfer-Encoding', '8bit' ); + $po->set_header( 'X-Generator', 'wp_cli_patterns_makepot' ); + + foreach ( $this->entries() as $entry ) { + $po->add_entry( $entry ); + } + + return $po; + } + + public function entries() : array { + $entries = []; + + foreach ( $this->patterns as $pattern ) { + + $parser = new PatternParser( $pattern ); + + foreach ( $parser->to_strings() as $string ) { + if ( ! isset( $entries[ $string ] ) ) { + $entries[ $string ] = new \Translation_Entry( + [ + 'singular' => $string, + 'extracted_comments' => "Found in the '{$pattern->title}' pattern.", + 'references' => [], + ] + ); + } + + if ( ! empty( $pattern->source_url ) && ! in_array( $pattern->source_url, $entries[ $string ]->references ) ) { + $entries[ $string ]->references[] = $pattern->source_url; + } + } + } + + return array_values( $entries ); + } + + public function import( $save = false ) { + // Avoid attempting to import strings when no patterns are found. + // This is a precautionary check to ensure we don't accidentally remove all translations. + if ( empty( $this->patterns ) ) { + return 'No patterns found: skipping import.'; + } + + // Load GlotPress for the API. + switch_to_blog( WPORG_TRANSLATE_BLOGID ); + $this->load_glotpress(); + + $project = \GP::$project->by_path( GLOTPRESS_PROJECT ); + if ( ! $project ) { + return 'Project not found!'; + } + + $po = $this->makepo(); + + if ( true === $save ) { + add_filter( 'gp_import_project_originals', [ $this, 'extend_imported_originals' ], 10, 3 ); + list( $added, $existing, $fuzzied, $obsoleted, $error ) = \GP::$original->import_for_project( $project, $po ); + remove_filter( 'gp_import_project_originals', [ $this, 'extend_imported_originals' ], 10, 3 ); + + $notice = sprintf( + '%1$s new strings added, %2$s updated, %3$s fuzzied, and %4$s obsoleted.', + $added, + $existing, + $fuzzied, + $obsoleted + ); + + if ( $error ) { + $notice .= ' ' . sprintf( + '%s new string(s) were not imported due to an error.', + $error + ); + } + + restore_current_blog(); + + return $notice; + } else { + restore_current_blog(); + + return sprintf( 'dry-run: %s translations would be imported using the --save flag', count( $po->entries ) ); + } + } + + /** + * In the patterns case we import patterns in separate runs and can't rely on the full set of strings + * being imported at the same time. + * + * We want to leave most originals active and only obsolete things when they're really no longer in use. + */ + public static function extend_imported_originals( $po, $project, $originals_by_key ) { + // Only prevent dropping originals for the patterns project + if ( GLOTPRESS_PROJECT !== $project->path ) { + return $po; + } + + // Let's get all the references that we are importing. + $import_references = array(); + if ( ! empty( $po->entries ) ) { + $import_references = array_merge( ... array_column( $po->entries, 'references' ) ); + } + + foreach ( $originals_by_key as $entry_key => $original ) { + // We can skip doing anything with currently obsolete originals, these will be added back by our PO as we expect + if ( '-obsolete' === $original->status ) { + continue; + } + // else we need to merge the current original into our PO so that it doesn't get obsoleted unless its expected + + // Remove import_references from the current original references, + // the corrected import references will be added back below, or the original is supposed to be going obsolete + $original_references = array_diff( explode( ' ', $original->references ), $import_references ); + if ( empty( $original_references ) ) { + continue; + } + + // If we're importing this original again, merge any references we're not currently pulling in + if ( isset( $po->entries[ $entry_key ] ) ) { + $po->entries[ $entry_key ]->references = array_merge( $original_references, $po->entries[ $entry_key ]->references ); + } else { + // otherwise, merge the original into the po as a new entry + $po->add_entry( + new \Translation_Entry( + [ + 'context' => $original->context, + 'singular' => $original->singular, + 'plural' => $original->plural, + 'extracted_comments' => $original->comment, + 'references' => $original_references, // Only include references not covered by our import + ] + ) + ); + } + + // Changes to reference ordering triggers updates we can avoid + sort( $po->entries[ $entry_key ]->references ); + } + + return $po; + } + + /** + * Load GlotPress so that we can interact with the GlotPress APIs. + */ + public function load_glotpress() { + // TODO: Figure out how to properly do the following stuff. + // Maybe this needs to be run in the context of translate.w.org + // and switch_to_site( PATTERN_DIRECTORY ) instead? But post type + // would not be registered still. + // Maybe this should be a two-part operation, Export all strings to + // .po file, then import into GlotPress as an additional call. + + $GLOBALS['gp_table_prefix'] = GLOTPRESS_TABLE_PREFIX; + + // Load any GlotPress plugins as needed. + $plugins = get_option( 'active_plugins', [] ); + array_walk( $plugins, function( $plugin ) { + include_once trailingslashit( WP_PLUGIN_DIR ) . $plugin; + } ); + + // Run the GlotPress init routines. + if ( ! did_action( 'gp_init' ) ) { + gp_init(); + } + } +} diff --git a/content/plugins/pattern-translations/includes/parser.php b/content/plugins/pattern-translations/includes/parser.php new file mode 100644 index 0000000..6e25b91 --- /dev/null +++ b/content/plugins/pattern-translations/includes/parser.php @@ -0,0 +1,171 @@ +pattern = $pattern; + + $this->parsers = [ + // Blocks that have custom parsers. + 'core/paragraph' => new Parsers\Paragraph(), + 'core/heading' => new Parsers\Heading(), + 'core/button' => new Parsers\Button(), + 'core/spacer' => new Parsers\Noop(), + + // Common core blocks that use the default parser. + 'core/buttons' => new Parsers\BasicText(), + 'core/list' => new Parsers\BasicText(), + 'core/column' => new Parsers\BasicText(), + 'core/columns' => new Parsers\BasicText(), + 'core/cover' => new Parsers\BasicText(), + 'core/group' => new Parsers\BasicText(), + 'core/image' => new Parsers\BasicText(), + 'core/media-text' => new Parsers\BasicText(), + 'core/separator' => new Parsers\BasicText(), + 'core/social-link' => new Parsers\BasicText(), + ]; + + $this->fallback = new Parsers\BasicText(); + } + + public function block_parser_to_strings( array $block ) : array { + $parser = $this->parsers[ $block['blockName'] ] ?? $this->fallback; + + $strings = $parser->to_strings( $block ); + + foreach ( $block['innerBlocks'] as $inner_block ) { + $strings = array_merge( $strings, $this->block_parser_to_strings( $inner_block ) ); + } + + return $strings; + } + + public function block_parser_replace_strings( array &$block, array $replacements ) : array { + $parser = $this->parsers[ $block['blockName'] ] ?? $this->fallback; + $block = $parser->replace_strings( $block, $replacements ); + + foreach ( $block['innerBlocks'] as &$inner_block ) { + $inner_block = $this->block_parser_replace_strings( $inner_block, $replacements ); + } + + return $block; + } + + public function to_strings() : array { + $blocks = parse_blocks( $this->pattern->html ); + + $strings = []; + + if ( ! empty( $this->pattern->title ) ) { + $strings = [ $this->pattern->title ]; + } + + if ( ! empty( $this->pattern->description ) ) { + $strings[] = $this->pattern->description; + } + + if ( ! empty( $this->pattern->keywords ) ) { + $keywords = explode( ', ', $this->pattern->keywords ); + $strings = array_merge( $strings, $keywords ); + } + + foreach ( $blocks as $block ) { + $strings = array_merge( $strings, $this->block_parser_to_strings( $block ) ); + } + + return array_unique( $strings ); + } + + public function replace_strings_with_kses( array $replacements ) : Pattern { + // Sanitize replacement strings before injecting them into blocks and block attributes. + $sanitized_replacements = $replacements; + foreach ( $sanitized_replacements as &$replacement ) { + $replacement = wp_kses_post( $replacement ); + } + return $this->replace_strings( $sanitized_replacements ); + } + + public function replace_strings( array $replacements ) : Pattern { + $translated = clone $this->pattern; + $translated->title = $replacements[ $translated->title ] ?? $translated->title; + $translated->description = $replacements[ $translated->description ] ?? $translated->description; + + $translated_keywords = []; + foreach ( explode( ', ', $translated->keywords ) as $keyword ) { + $translated_keywords[] = $replacements[ $keyword ] ?? $keyword; + } + $translated->keywords = implode( ', ', $translated_keywords ); + + $blocks = parse_blocks( $translated->html ); + + foreach ( $blocks as &$block ) { + $block = $this->block_parser_replace_strings( $block, $replacements ); + } + + // If we pass `serialize_blocks` a block that includes unicode characters in the + // attributes, these attributes will be encoded with a unicode escape character, e.g. + // "subscribePlaceholder":"😀" becomes "subscribePlaceholder":"\ud83d\ude00". + // After we get the serialized blocks back from `serialize_blocks` we need to convert these + // characters back to their unicode form so that we don't break blocks in the editor. + $translated->html = $this->decode_unicode_characters( serialize_blocks( $blocks ) ); + + return $translated; + } + + /** + * Decode a string containing unicode escape sequences. + * Excludes decoding characters not allowed within block attributes. + * + * @param string $string A string containing serialized blocks. + * @return string A string containing decoded unicode characters. + */ + public function decode_unicode_characters( string $string ): string { + + // In WordPress core, `serialize_block_attributes` intentionally leaves some characters + // in the block attributes encoded in their unicode form. These are characters that would + // interfere with characters in block comments e.g. consider potential values entered + // in the placeholder attribute: + // Reference: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/blocks.php#L367 + + $excluded_characters = [ + '\\u002d\\u002d', // '--' + '\\u003c', // '<' + '\\u003e', // '>' + '\\u0026', // '&' + '\\u0022', // '"' + ]; + + // Match any uninterrupted sequence of \u escaped unicode characters. + $decoded_string = preg_replace_callback( + '#(\\\\u[a-zA-Z0-9]{4})+#', + function ( $matches ) use ( $excluded_characters ) { + // If we encounter any excluded characters, don't decode this match. + foreach ( $excluded_characters as $excluded_character ) { + if ( false !== mb_stripos( $matches[0], $excluded_character ) ) { + return $matches[0]; + } + } + // If we didn't encounter excluded characters, use json_decode to do the heavy lifting. + return json_decode( '"' . $matches[0] . '"' ); + }, + $string + ); + + return $decoded_string; + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/BasicText.php b/content/plugins/pattern-translations/includes/parsers/BasicText.php new file mode 100644 index 0000000..43477bb --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/BasicText.php @@ -0,0 +1,58 @@ +get_dom( $block['innerHTML'] ); + $xpath = new \DOMXPath( $dom ); + + $strings = []; + + foreach ( $xpath->query( $this->text_nodes_xpath_query() ) as $text ) { + if ( trim( $text->nodeValue ) ) { + $strings[] = $text->nodeValue; + } + } + + return $strings; + } + + public function replace_strings( array $block, array $replacements ) : array { + $dom = $this->get_dom( $block['innerHTML'] ); + $xpath = new \DOMXPath( $dom ); + $xpath_query = $this->text_nodes_xpath_query(); + + foreach ( $xpath->query( $xpath_query ) as $text ) { + if ( trim( $text->nodeValue ) && isset( $replacements[ $text->nodeValue ] ) ) { + $text->parentNode->replaceChild( $dom->createCDATASection( $replacements[ $text->nodeValue ] ), $text ); + } + } + + $block['innerHTML'] = $this->removeHtml( $dom->saveHTML() ); + + foreach ( $block['innerContent'] as &$inner_content ) { + if ( is_string( $inner_content ) ) { + $dom = $this->get_dom( $inner_content ); + $xpath = new \DOMXPath( $dom ); + + $text_nodes = $xpath->query( $xpath_query ); + + // Only update text matches that are found outside of HTML tags. + // This approach does not use $dom->saveHTML because innerContent includes + // unclosed HTML tags, and saveHTML adds extra closed tags. + foreach ( $text_nodes as $text ) { + if ( trim( $text->nodeValue ) && isset( $replacements[ $text->nodeValue ] ) ) { + $regex = '#(<([^>]*)>)?' . preg_quote( $text->nodeValue, '/' ) . '(<([^>]*)>)?#is'; + $inner_content = preg_replace( $regex, '${1}' . $replacements[ $text->nodeValue ] . '${3}', $inner_content ); + } + } + } + } + + return $block; + } + +} diff --git a/content/plugins/pattern-translations/includes/parsers/BlockParser.php b/content/plugins/pattern-translations/includes/parsers/BlockParser.php new file mode 100644 index 0000000..b1edd68 --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/BlockParser.php @@ -0,0 +1,109 @@ +$html"; + } + + private function removeHtml( string $html ) : string { + return preg_replace( + [ + '/^\s*<\/head>/sm', + // $dom->saveHTML() can have a trailing newline after the closing , match to the real end of the document. + '/<\/body><\/html>\s*$/sm', + ], + '', + $html + ); + } + + private function get_dom( string $html ) : \DOMDocument { + $previous = libxml_use_internal_errors( true ); + $dom = new \DomDocument(); + $dom->loadHTML( $this->addHtml( $html ), LIBXML_HTML_NODEFDTD | LIBXML_COMPACT ); + libxml_clear_errors(); + libxml_use_internal_errors( $previous ); + return $dom; + } + // phpcs:enable WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid +} + +trait GetSetAttribute { + private function get_attribute( string $attribute_name, array $block ) : array { + if ( isset( $block['attrs'][ $attribute_name ] ) && is_string( $block['attrs'][ $attribute_name ] ) ) { + return [ $block['attrs'][ $attribute_name ] ]; + } + return []; + } + + private function set_attribute( string $attribute_name, array &$block, array $replacements ) { + if ( isset( $block['attrs'][ $attribute_name ] ) && is_string( $block['attrs'][ $attribute_name ] ) ) { + if ( isset( $replacements[ $block['attrs'][ $attribute_name ] ] ) ) { + $block['attrs'][ $attribute_name ] = $replacements[ $block['attrs'][ $attribute_name ] ]; + } + } + } +} + +trait SwapTags { + private $safe_tags = [ + 'strong', + 'em', + ]; + + private function encode_tags( string $raw_html ) : string { + foreach ( $this->safe_tags as $tag ) { + $raw_html = preg_replace( + '#(<' . $tag . '([^>]*)>)(.*)()#', + '{' . $tag . '$2' . '}$3' . '{/' . $tag . '}', // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found + $raw_html + ); + } + return $raw_html; + } + + private function decode_tags( string $encoded_html ) : string { + foreach ( $this->safe_tags as $tag ) { + $encoded_html = preg_replace( + '#({' . $tag . '([^}]*)})(.*)({/' . $tag . '})#', + '<' . $tag . '$2' . '>$3' . '', // phpcs:ignore Generic.Strings.UnnecessaryStringConcat.Found + $encoded_html + ); + } + return $encoded_html; + } +} + +trait TextNodesXPath { + private $xpaths = [ + '//text()', // Visible Text nodes. + '//img/@alt', // Image alt="" text. + '//*/@title', // title="" text. + ]; + + protected function text_nodes_xpath_query() { + return implode( ' | ', $this->xpaths ); + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/Button.php b/content/plugins/pattern-translations/includes/parsers/Button.php new file mode 100644 index 0000000..65f52d5 --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/Button.php @@ -0,0 +1,54 @@ +get_attribute( 'placeholder', $block ); + + $encoded_html = $this->encode_tags( $block['innerHTML'] ); + + $dom = $this->get_dom( $encoded_html ); + $xpath = new \DOMXPath( $dom ); + + foreach ( $xpath->query( $this->text_nodes_xpath_query() ) as $text ) { + if ( trim( $text->nodeValue ) ) { + $strings[] = $this->decode_tags( $text->nodeValue ); + } + } + + return $strings; + } + + public function replace_strings( array $block, array $replacements ) : array { + $this->set_attribute( 'placeholder', $block, $replacements ); + + $encoded_html = $this->encode_tags( $block['innerHTML'] ); + + $dom = $this->get_dom( $encoded_html ); + $xpath = new \DOMXPath( $dom ); + + foreach ( $xpath->query( $this->text_nodes_xpath_query() ) as $text ) { + if ( trim( $text->nodeValue ) && isset( $replacements[ $this->decode_tags( $text->nodeValue ) ] ) ) { + $text->parentNode->replaceChild( + $dom->createCDATASection( + $this->encode_tags( + $replacements[ $this->decode_tags( $text->nodeValue ) ] + ) + ), + $text + ); + } + } + + $decoded_html = $this->decode_tags( $this->removeHtml( $dom->saveHTML() ) ); + $block['innerHTML'] = $decoded_html; + $block['innerContent'] = [ $decoded_html ]; + + return $block; + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/Heading.php b/content/plugins/pattern-translations/includes/parsers/Heading.php new file mode 100644 index 0000000..a313b8a --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/Heading.php @@ -0,0 +1,39 @@ +get_attribute( 'placeholder', $block ); + + if ( preg_match( '/]*>(.+)<\/h[1-6]>/is', $block['innerHTML'], $matches ) ) { + if ( ! empty( $matches[1] ) ) { + $strings[] = $matches[1]; + } + } + + return $strings; + } + + // todo: this needs a fix to properly rebuild innerContent - see ParagraphParserTest + public function replace_strings( array $block, array $replacements ) : array { + $this->set_attribute( 'placeholder', $block, $replacements ); + + $html = $block['innerHTML']; + + foreach ( $this->to_strings( $block ) as $original ) { + if ( ! empty( $original ) && isset( $replacements[ $original ] ) ) { + $regex = '#(]*>)(' . preg_quote( $original, '/' ) . ')(<\/h[1-6]>)#is'; + $html = preg_replace( $regex, '${1}' . addcslashes( $replacements[ $original ], '\\$' ) . '${3}', $html ); + } + } + + $block['innerHTML'] = $html; + $block['innerContent'] = [ $html ]; + + return $block; + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/Noop.php b/content/plugins/pattern-translations/includes/parsers/Noop.php new file mode 100644 index 0000000..752403c --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/Noop.php @@ -0,0 +1,12 @@ +get_attribute( 'placeholder', $block ); + + $matches = []; + + if ( preg_match( '/]*>(.+)<\/p>/is', $block['innerHTML'], $matches ) ) { + if ( ! empty( $matches[1] ) ) { + $strings[] = $matches[1]; + } + } + + return $strings; + } + + // todo: this needs a fix to properly rebuild innerContent - see ParagraphParserTest + public function replace_strings( array $block, array $replacements ) : array { + $this->set_attribute( 'placeholder', $block, $replacements ); + + $html = $block['innerHTML']; + + foreach ( $this->to_strings( $block ) as $original ) { + if ( ! empty( $original ) && isset( $replacements[ $original ] ) ) { + $regex = '#(]*>)(' . preg_quote( $original, '/' ) . ')(<\/p>)#is'; + $html = preg_replace( $regex, '${1}' . addcslashes( $replacements[ $original ], '\\$' ) . '${3}', $html ); + } + } + + $block['innerHTML'] = $html; + $block['innerContent'] = [ $html ]; + + return $block; + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/ShortcodeBlock.php b/content/plugins/pattern-translations/includes/parsers/ShortcodeBlock.php new file mode 100644 index 0000000..18aaefc --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/ShortcodeBlock.php @@ -0,0 +1,83 @@ +attribute_names = $attribute_names; + } + + public function to_strings( array $block ) : array { + $strings = []; + foreach ( $this->attribute_names as $attribute_name ) { + $strings = array_merge( $strings, $this->get_attribute( $attribute_name, $block ) ); + } + + return $strings; + } + + public function replace_strings( array $block, array $replacements ) : array { + foreach ( $this->attribute_names as $attribute_name ) { + $this->set_attribute( $attribute_name, $block, $replacements ); + + foreach ( $block['innerContent'] as $i => &$inner_content ) { + if ( is_string( $inner_content ) ) { + $shortcode_param_regex = '/(\b' . $this->snake_case( $attribute_name ) . ')="(.*?)("\n?)/'; + + $block['innerContent'][ $i ] = preg_replace_callback( + $shortcode_param_regex, + function( $matches ) use ( $replacements ) { + return $this->preg_replace_gutenberg_attributes_handler( $matches, $replacements ); + }, + $inner_content + ); + } + } + } + + $regex = '/\b(\w*?)="(.*?)(")/'; + $block['innerHTML'] = preg_replace_callback( + $regex, + function( $matches ) use ( $replacements ) { + return $this->preg_replace_gutenberg_attributes_handler( $matches, $replacements ); + }, + $block['innerHTML'] + ); + return $block; + } + + // phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + protected function snake_case( $camelCaseString ) { + return ltrim( + preg_replace_callback( + '/([A-Z]+)/', + function( $matches ) { + return '_' . strtolower( $matches[1] ); }, + $camelCaseString + ), + '_' + ); + } + // phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase + + public function preg_replace_gutenberg_attributes_handler( array $matches, array $replacements ) { + $current_value = $matches[2]; + + if ( ! isset( $replacements[ $current_value ] ) ) { + return $matches[0]; + } + + $new_value = $replacements[ $current_value ]; + $property = $matches[1]; + return "$property=\"$new_value" . $matches[3]; + } +} diff --git a/content/plugins/pattern-translations/includes/parsers/TextNode.php b/content/plugins/pattern-translations/includes/parsers/TextNode.php new file mode 100644 index 0000000..1aec708 --- /dev/null +++ b/content/plugins/pattern-translations/includes/parsers/TextNode.php @@ -0,0 +1,34 @@ +get_dom( serialize_block( $block ) ); + $xpath = new \DOMXPath( $dom ); + + $strings = []; + + foreach ( $xpath->query( '//text()' ) as $text ) { + if ( trim( $text->nodeValue ) ) { + $strings[] = $text->nodeValue; + } + } + + return $strings; + } + + public function replace_strings( array $block, array $replacements ) : array { + $dom = $this->get_dom( serialize_block( $block ) ); + $xpath = new \DOMXPath( $dom ); + + foreach ( $xpath->query( '//text()' ) as $text ) { + if ( trim( $text->nodeValue ) && isset( $replacements[ $text->nodeValue ] ) ) { + $text->parentNode->replaceChild( $dom->createCDATASection( $replacements[ $text->nodeValue ] ), $text ); + } + } + + return parse_blocks( $this->removeHtml( $dom->saveHTML() ) )[0] ?? []; + } +} diff --git a/content/plugins/pattern-translations/includes/pattern.php b/content/plugins/pattern-translations/includes/pattern.php new file mode 100644 index 0000000..148fce1 --- /dev/null +++ b/content/plugins/pattern-translations/includes/pattern.php @@ -0,0 +1,149 @@ +locale ) { + if ( $this->parent && 'en_US' === $this->parent->locale ) { + $parent = $this->parent; + } else { + $parent = self::from_post( get_post( $this->ID ) ); + } + } else { + $parent = $this; + } + $translated = clone $parent; + $translated->parent = $parent; + + // to convert from a Translated Pattern to en_US. + if ( 'en_US' === $locale ) { + $translated->parent = false; + return $translated; + } + + switch_to_locale( $locale ); + + $parser = new PatternParser( $translated ); + + $translations = []; + $translated = false; + foreach ( $parser->to_strings() as $string ) { + $translations[ $string ] = apply_filters( 'gettext', GlotPress_Translate_Bridge::translate( $string, GLOTPRESS_PROJECT ), 'wporg-pattern' ); + + // Consider any string change to be a translation. + if ( $string !== $translations[ $string ] ) { + $translated = true; + } + } + + restore_current_locale(); + + // Are there any translations? + if ( ! $translated ) { + return false; + } + + $translated = $parser->replace_strings_with_kses( $translations ); + $translated->locale = $locale; + // Reset the ID. + $translated->ID = 0; + + // Find the actual post ID of the translated pattern + $children = get_posts( [ + 'post_parent' => $parent->ID, + 'post_type' => POST_TYPE, + 'post_status' => 'any', + 'meta_query' => [ + [ + 'key' => 'wpop_locale', + 'value' => $locale, + ], + ], + ] ); + if ( $children ) { + $post = array_shift( $children ); + $translated->ID = $post->ID; + $translated->name = $post->post_name; // ??? + } + + return $translated; + } + + /** + * Create a new Pattern object from a WP_Post object for translation purposes. + * + * @param \WP_Post $post The post object. + * @return Pattern The Pattern object. + */ + public static function from_post( \WP_Post $post ) : Pattern { + $pattern = new Pattern(); + $pattern->ID = $post->ID; + $pattern->title = $post->post_title; + $pattern->name = $post->post_name; + $pattern->description = $post->wpop_description; + $pattern->keywords = $post->wpop_keywords; + $pattern->html = $post->post_content; + $pattern->source_url = get_permalink( $post ); + $pattern->locale = 'en_US'; + + return $pattern; + } + + /** + * Fetch an array of Pattern objects based on a WP_Query query. + * + * @param array $args The WP_Query args. + * @return array An array of Pattern objects. + */ + public static function get_patterns( array $args = [] ) : array { + $defaults = [ + 'post_type' => POST_TYPE, + // Note: This must be set for cli context, in isolated test context this is defaulted to 'publish' + // Prevents unexpected patterns in translations + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => [ + 'post_date' => 'DESC', + ], + // Only select en_US patterns. + 'meta_query' => [ + [ + 'key' => 'wpop_locale', + 'value' => 'en_US', + ], + ], + ]; + + $options = wp_parse_args( $args, $defaults ); + + $query = new \WP_Query(); + $patterns = $query->query( $options ); + + wp_reset_postdata(); + + if ( 'ids' !== $query->get( 'fields' ) ) { + $patterns = array_map( [ self::class, 'from_post' ], $patterns ); + } + + return $patterns; + } +} diff --git a/content/plugins/pattern-translations/pattern-translations.php b/content/plugins/pattern-translations/pattern-translations.php new file mode 100644 index 0000000..d01b1be --- /dev/null +++ b/content/plugins/pattern-translations/pattern-translations.php @@ -0,0 +1,150 @@ + Translation Context, see pattern-directory/bin/i18n.php + 'wporg-pattern-category' => 'Categories term name', + 'wporg-pattern-flag-reason' => 'Flag Reasons term name', +]; + +require __DIR__ . '/includes/pattern.php'; +require __DIR__ . '/includes/parser.php'; +require __DIR__ . '/includes/i18n.php'; +require __DIR__ . '/includes/makepot.php'; +require __DIR__ . '/includes/cron.php'; + +if ( defined( 'WP_CLI' ) ) { + require __DIR__ . '/includes/cli-commands.php'; +} + +/** + * Creates or updates a localised pattern. + */ +function create_or_update_translated_pattern( Pattern $pattern ) { + $parent = false; + if ( $pattern->parent ) { + $parent = get_post( $pattern->parent->ID ); + } + + $args = [ + 'ID' => $pattern->ID, + 'post_type' => POST_TYPE, + 'post_title' => $pattern->title, + 'post_name' => $pattern->ID ? $pattern->name : ( $pattern->name . '-' . $pattern->locale ), // TODO: Translate the slug? + 'post_date' => $parent->post_date ?? '', + 'post_content' => $pattern->html, + 'post_parent' => $pattern->parent->ID ?? 0, + 'post_author' => $parent->post_author ?? 0, + 'post_status' => $parent->post_status ?? 'pending', + 'meta_input' => [ + 'wpop_description' => $pattern->description, + 'wpop_locale' => $pattern->locale, + 'wpop_keywords' => $pattern->keywords, + 'wpop_viewport_width' => $parent->wpop_viewport_width, + 'wpop_block_types' => $parent->wpop_block_types, + 'wpop_contains_block_types' => $parent->wpop_contains_block_types, + 'wpop_wp_version' => $parent->wpop_wp_version, + 'wpop_is_translation' => true, + ], + ]; + + if ( ! $args['ID'] ) { + unset( $args['ID'] ); + } + + $post_id = wp_insert_post( $args, true ); + + // Copy the terms from the parent if required. + if ( $post_id && ! is_wp_error( $post_id ) && $pattern->parent ) { + foreach ( [ 'wporg-pattern-category', 'wporg-pattern-keyword' ] as $taxonomy ) { + $term_ids = wp_get_object_terms( $pattern->parent->ID, $taxonomy, [ 'fields' => 'ids' ] ); + wp_set_object_terms( $post_id, $term_ids, $taxonomy ); + } + } + + return $post_id; +} + +/** + * Translate term names into the current site locale. + * + * @param WP_Term $term The WP_Term object being loaded. + */ +function translate_term( $term ) { + if ( + is_admin() || + // Not get_user_locale(), as we respect the displayed site locale. + 'en_US' === get_locale() || + // Only certain translated taxonomies + ! isset( TRANSLATED_TAXONOMIES[ $term->taxonomy ] ) + ) { + return $term; + } + + $i18n_context = TRANSLATED_TAXONOMIES[ $term->taxonomy ]; + $term->name = esc_html( translate_with_gettext_context( html_entity_decode( $term->name ), $i18n_context, 'wporg-patterns' ) ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext + + return $term; +} +add_filter( 'get_term', __NAMESPACE__ . '\translate_term' ); + +/** + * Translate the title of pages. + * + * @param string $title The current title, ignored. + * @param int $post_id The post_id of the page. + * @return string Possibly translated page title. + */ +function translate_page_title( $title, $post_id = null ) { + $post = get_post( $post_id ); + + if ( $post && 'page' === $post->post_type ) { + $title = translate_with_gettext_context( $post->post_title, 'Page title', 'wporg-patterns' ); // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText + } + + return $title; +} +add_filter( 'the_title', __NAMESPACE__ . '\translate_page_title', 1, 2 ); +add_filter( 'single_post_title', __NAMESPACE__ . '\translate_page_title', 1, 2 ); + +/** + * Set the correct locale context for API endpoints. + * + * For api.wordpress.org requests, the `locale` GET parameter is respected if set. Defaults to en_US otherwise. + * For REST API requests, the `_locale=user` GET parameter is ignored for authenticated requests, causing the rest to default to the Site locale. + */ +function locale( $locale ) { + // When being requested through api.wordpress.org, respect the query variable. + if ( + defined( 'WPORG_IS_API' ) && + WPORG_IS_API && + ! empty( $_GET['locale'] ) && + is_string( $_GET['locale'] ) && + sanitize_locale_name( $_GET['locale'] ) === $_GET['locale'] + ) { + return $_GET['locale']; + } + + // Respect the site locale otherwise for rest api queries. + // This is used to prevent `?_locale=user` returning non-translated details on localised sites. + if ( + wp_is_json_request() && + isset( $_GET['_locale'] ) && + 'user' === $_GET['_locale'] + ) { + $_GET['_locale'] = 'site'; + } + + return $locale; +} +add_filter( 'locale', __NAMESPACE__ . '\locale' ); From 5302029396d5ff394373cc7b246de9d53a7babb7 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:57:36 +0100 Subject: [PATCH 05/10] Locales: Define `$root_slug` property. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/mu-plugins/locales/locales.php | 1 + 1 file changed, 1 insertion(+) diff --git a/content/mu-plugins/locales/locales.php b/content/mu-plugins/locales/locales.php index 6e41f2c..9babbf8 100644 --- a/content/mu-plugins/locales/locales.php +++ b/content/mu-plugins/locales/locales.php @@ -9,6 +9,7 @@ if ( ! class_exists( 'GP_Locale' ) ) : class GP_Locale { + public $root_slug; public $english_name; public $native_name; public $text_direction = 'ltr'; From 3b05d33ea11c071cb622b6038cf39fa7c292aad3 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:57:51 +0100 Subject: [PATCH 06/10] Locales: Fix encoding on native name. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/mu-plugins/locales/locales.php | 316 ++++++++++++------------- 1 file changed, 158 insertions(+), 158 deletions(-) diff --git a/content/mu-plugins/locales/locales.php b/content/mu-plugins/locales/locales.php index 9babbf8..19d5a01 100644 --- a/content/mu-plugins/locales/locales.php +++ b/content/mu-plugins/locales/locales.php @@ -166,7 +166,7 @@ public function __construct() { $am = new GP_Locale(); $am->english_name = 'Amharic'; - $am->native_name = 'አማርኛ'; + $am->native_name = 'አማርኛ'; $am->lang_code_iso_639_1 = 'am'; $am->lang_code_iso_639_2 = 'amh'; $am->country_code = 'et'; @@ -177,7 +177,7 @@ public function __construct() { $an = new GP_Locale(); $an->english_name = 'Aragonese'; - $an->native_name = 'Aragonés'; + $an->native_name = 'Aragonés'; $an->lang_code_iso_639_1 = 'an'; $an->lang_code_iso_639_2 = 'arg'; $an->lang_code_iso_639_3 = 'arg'; @@ -187,7 +187,7 @@ public function __construct() { $ar = new GP_Locale(); $ar->english_name = 'Arabic'; - $ar->native_name = 'العربية'; + $ar->native_name = 'العربية'; $ar->lang_code_iso_639_1 = 'ar'; $ar->lang_code_iso_639_2 = 'ara'; $ar->wp_locale = 'ar'; @@ -202,7 +202,7 @@ public function __construct() { $arq = new GP_Locale(); $arq->english_name = 'Algerian Arabic'; - $arq->native_name = 'الدارجة الجزايرية'; + $arq->native_name = 'الدارجة الجزايرية'; $arq->lang_code_iso_639_1 = 'ar'; $arq->lang_code_iso_639_3 = 'arq'; $arq->country_code = 'dz'; @@ -215,7 +215,7 @@ public function __construct() { $ary = new GP_Locale(); $ary->english_name = 'Moroccan Arabic'; - $ary->native_name = 'العربية المغربية'; + $ary->native_name = 'العربية المغربية'; $ary->lang_code_iso_639_1 = 'ar'; $ary->lang_code_iso_639_3 = 'ary'; $ary->country_code = 'ma'; @@ -228,7 +228,7 @@ public function __construct() { $as = new GP_Locale(); $as->english_name = 'Assamese'; - $as->native_name = 'অসমীয়া'; + $as->native_name = 'অসমীয়া'; $as->lang_code_iso_639_1 = 'as'; $as->lang_code_iso_639_2 = 'asm'; $as->lang_code_iso_639_3 = 'asm'; @@ -249,7 +249,7 @@ public function __construct() { $av = new GP_Locale(); $av->english_name = 'Avaric'; - $av->native_name = 'авар мацӀ'; + $av->native_name = 'авар мацӀ'; $av->lang_code_iso_639_1 = 'av'; $av->lang_code_iso_639_2 = 'ava'; $av->slug = 'av'; @@ -268,7 +268,7 @@ public function __construct() { $az = new GP_Locale(); $az->english_name = 'Azerbaijani'; - $az->native_name = 'AzÉ™rbaycan dili'; + $az->native_name = 'Azərbaycan dili'; $az->lang_code_iso_639_1 = 'az'; $az->lang_code_iso_639_2 = 'aze'; $az->country_code = 'az'; @@ -279,7 +279,7 @@ public function __construct() { $azb = new GP_Locale(); $azb->english_name = 'South Azerbaijani'; - $azb->native_name = 'گؤنئی آذربایجان'; + $azb->native_name = 'گؤنئی آذربایجان'; $azb->lang_code_iso_639_1 = 'az'; $azb->lang_code_iso_639_3 = 'azb'; $azb->country_code = 'ir'; @@ -290,7 +290,7 @@ public function __construct() { $az_tr = new GP_Locale(); $az_tr->english_name = 'Azerbaijani (Turkey)'; - $az_tr->native_name = 'AzÉ™rbaycan TürkcÉ™si'; + $az_tr->native_name = 'Azərbaycan Türkcəsi'; $az_tr->lang_code_iso_639_1 = 'az'; $az_tr->lang_code_iso_639_2 = 'aze'; $az_tr->country_code = 'tr'; @@ -299,7 +299,7 @@ public function __construct() { $ba = new GP_Locale(); $ba->english_name = 'Bashkir'; - $ba->native_name = 'башҡорт теле'; + $ba->native_name = 'башҡорт теле'; $ba->lang_code_iso_639_1 = 'ba'; $ba->lang_code_iso_639_2 = 'bak'; $ba->wp_locale = 'ba'; @@ -308,7 +308,7 @@ public function __construct() { $bal = new GP_Locale(); $bal->english_name = 'Catalan (Balear)'; - $bal->native_name = 'Català (Balear)'; + $bal->native_name = 'Català (Balear)'; $bal->lang_code_iso_639_2 = 'bal'; $bal->country_code = 'es'; $bal->wp_locale = 'bal'; @@ -316,7 +316,7 @@ public function __construct() { $bcc = new GP_Locale(); $bcc->english_name = 'Balochi Southern'; - $bcc->native_name = 'بلوچی مکرانی'; + $bcc->native_name = 'بلوچی مکرانی'; $bcc->lang_code_iso_639_3 = 'bcc'; $bcc->country_code = 'pk'; $bcc->wp_locale = 'bcc'; @@ -328,7 +328,7 @@ public function __construct() { $be = new GP_Locale(); $be->english_name = 'Belarusian'; - $be->native_name = 'Беларуская мова'; + $be->native_name = 'Беларуская мова'; $be->lang_code_iso_639_1 = 'be'; $be->lang_code_iso_639_2 = 'bel'; $be->country_code = 'by'; @@ -342,7 +342,7 @@ public function __construct() { $bg = new GP_Locale(); $bg->english_name = 'Bulgarian'; - $bg->native_name = 'Български'; + $bg->native_name = 'Български'; $bg->lang_code_iso_639_1 = 'bg'; $bg->lang_code_iso_639_2 = 'bul'; $bg->country_code = 'bg'; @@ -354,7 +354,7 @@ public function __construct() { $bgn = new GP_Locale(); $bgn->english_name = 'Western Balochi'; - $bgn->native_name = 'بلوچی‎'; + $bgn->native_name = 'بلوچی‎'; $bgn->lang_code_iso_639_3 = 'bgn'; $bgn->country_code = 'pk'; $bgn->wp_locale = 'bgn'; @@ -364,7 +364,7 @@ public function __construct() { $bh = new GP_Locale(); $bh->english_name = 'Bihari'; - $bh->native_name = 'भोजपुरी'; + $bh->native_name = 'भोजपुरी'; $bh->lang_code_iso_639_1 = 'bh'; $bh->lang_code_iso_639_2 = 'bih'; $bh->slug = 'bh'; @@ -372,7 +372,7 @@ public function __construct() { $bho = new GP_Locale(); $bho->english_name = 'Bhojpuri'; - $bho->native_name = 'भोजपुरी'; + $bho->native_name = 'भोजपुरी'; $bho->lang_code_iso_639_3 = 'bho'; $bho->country_code = 'in'; $bho->wp_locale = 'bho'; @@ -396,7 +396,7 @@ public function __construct() { $bn_bd = new GP_Locale(); $bn_bd->english_name = 'Bengali (Bangladesh)'; - $bn_bd->native_name = 'বাংলা'; + $bn_bd->native_name = 'বাংলা'; $bn_bd->lang_code_iso_639_1 = 'bn'; $bn_bd->country_code = 'bd'; $bn_bd->wp_locale = 'bn_BD'; @@ -406,7 +406,7 @@ public function __construct() { $bn_in = new GP_Locale(); $bn_in->english_name = 'Bengali (India)'; - $bn_in->native_name = 'বাংলা (ভারত)'; + $bn_in->native_name = 'বাংলা (ভারত)'; $bn_in->lang_code_iso_639_1 = 'bn'; $bn_in->country_code = 'in'; $bn_in->wp_locale = 'bn_IN'; @@ -419,7 +419,7 @@ public function __construct() { $bo = new GP_Locale(); $bo->english_name = 'Tibetan'; - $bo->native_name = 'བོད་ཡིག'; + $bo->native_name = 'བོད་ཡིག'; $bo->lang_code_iso_639_1 = 'bo'; $bo->lang_code_iso_639_2 = 'tib'; $bo->wp_locale = 'bo'; @@ -444,7 +444,7 @@ public function __construct() { $brx = new GP_Locale(); $brx->english_name = 'Bodo'; - $brx->native_name = 'बोडो‎'; + $brx->native_name = 'बोडो‎'; $brx->lang_code_iso_639_3 = 'brx'; $brx->country_code = 'in'; $brx->wp_locale = 'brx'; @@ -466,7 +466,7 @@ public function __construct() { $ca = new GP_Locale(); $ca->english_name = 'Catalan'; - $ca->native_name = 'Català '; + $ca->native_name = 'Català'; $ca->lang_code_iso_639_1 = 'ca'; $ca->lang_code_iso_639_2 = 'cat'; $ca->wp_locale = 'ca'; @@ -476,7 +476,7 @@ public function __construct() { $ca_valencia = new GP_Locale(); $ca_valencia->english_name = 'Catalan (Valencian)'; - $ca_valencia->native_name = 'Català (Valencià )'; + $ca_valencia->native_name = 'Català (Valencià)'; $ca_valencia->lang_code_iso_639_1 = 'ca'; $ca_valencia->lang_code_iso_639_2 = 'cat'; $ca_valencia->wp_locale = 'ca_valencia'; @@ -486,7 +486,7 @@ public function __construct() { $ce = new GP_Locale(); $ce->english_name = 'Chechen'; - $ce->native_name = 'Нохчийн мотт'; + $ce->native_name = 'Нохчийн мотт'; $ce->lang_code_iso_639_1 = 'ce'; $ce->lang_code_iso_639_2 = 'che'; $ce->slug = 'ce'; @@ -511,7 +511,7 @@ public function __construct() { $ckb = new GP_Locale(); $ckb->english_name = 'Kurdish (Sorani)'; - $ckb->native_name = 'كوردی‎'; + $ckb->native_name = 'كوردی‎'; $ckb->lang_code_iso_639_1 = 'ku'; $ckb->lang_code_iso_639_3 = 'ckb'; $ckb->country_code = 'iq'; @@ -544,7 +544,7 @@ public function __construct() { $cr = new GP_Locale(); $cr->english_name = 'Cree'; - $cr->native_name = 'ᓀᐦᐃᔭᐍᐏᐣ'; + $cr->native_name = 'ᓀᐦᐃᔭᐍᐏᐣ'; $cr->lang_code_iso_639_1 = 'cr'; $cr->lang_code_iso_639_2 = 'cre'; $cr->country_code = 'ca'; @@ -553,7 +553,7 @@ public function __construct() { $cs = new GP_Locale(); $cs->english_name = 'Czech'; - $cs->native_name = 'ÄŒeÅ¡tina'; + $cs->native_name = 'Čeština'; $cs->lang_code_iso_639_1 = 'cs'; $cs->lang_code_iso_639_2 = 'ces'; $cs->country_code = 'cz'; @@ -566,7 +566,7 @@ public function __construct() { $csb = new GP_Locale(); $csb->english_name = 'Kashubian'; - $csb->native_name = 'Kaszëbsczi'; + $csb->native_name = 'Kaszëbsczi'; $csb->lang_code_iso_639_2 = 'csb'; $csb->slug = 'csb'; $csb->nplurals = 3; @@ -574,7 +574,7 @@ public function __construct() { $cu = new GP_Locale(); $cu->english_name = 'Church Slavic'; - $cu->native_name = 'ѩзыкъ словѣньскъ'; + $cu->native_name = 'ѩзыкъ словѣньскъ'; $cu->lang_code_iso_639_1 = 'cu'; $cu->lang_code_iso_639_2 = 'chu'; $cu->slug = 'cu'; @@ -582,7 +582,7 @@ public function __construct() { $cv = new GP_Locale(); $cv->english_name = 'Chuvash'; - $cv->native_name = 'чӑваш чӗлхи'; + $cv->native_name = 'чӑваш чӗлхи'; $cv->lang_code_iso_639_1 = 'cv'; $cv->lang_code_iso_639_2 = 'chv'; $cv->country_code = 'ru'; @@ -632,7 +632,7 @@ public function __construct() { $de_at = new GP_Locale(); $de_at->english_name = 'German (Austria)'; - $de_at->native_name = 'Deutsch (Österreich)'; + $de_at->native_name = 'Deutsch (Österreich)'; $de_at->lang_code_iso_639_1 = 'de'; $de_at->country_code = 'at'; $de_at->wp_locale = 'de_AT'; @@ -657,7 +657,7 @@ public function __construct() { $dsb = new GP_Locale(); $dsb->english_name = 'Lower Sorbian'; - $dsb->native_name = 'Dolnoserbšćina'; + $dsb->native_name = 'Dolnoserbšćina'; $dsb->lang_code_iso_639_2 = 'dsb'; $dsb->lang_code_iso_639_3 = 'dsb'; $dsb->country_code = 'de'; @@ -668,7 +668,7 @@ public function __construct() { $dv = new GP_Locale(); $dv->english_name = 'Dhivehi'; - $dv->native_name = 'Þ‹Þ¨ÞˆÞ¬Þ€Þ¨'; + $dv->native_name = 'ދިވެހި'; $dv->lang_code_iso_639_1 = 'dv'; $dv->lang_code_iso_639_2 = 'div'; $dv->country_code = 'mv'; @@ -679,7 +679,7 @@ public function __construct() { $dzo = new GP_Locale(); $dzo->english_name = 'Dzongkha'; - $dzo->native_name = 'རྫོང་ཁ'; + $dzo->native_name = 'རྫོང་ཁ'; $dzo->lang_code_iso_639_1 = 'dz'; $dzo->lang_code_iso_639_2 = 'dzo'; $dzo->country_code = 'bt'; @@ -691,7 +691,7 @@ public function __construct() { $ewe = new GP_Locale(); $ewe->english_name = 'Ewe'; - $ewe->native_name = 'EÊ‹egbe'; + $ewe->native_name = 'Eʋegbe'; $ewe->lang_code_iso_639_1 = 'ee'; $ewe->lang_code_iso_639_2 = 'ewe'; $ewe->lang_code_iso_639_3 = 'ewe'; @@ -701,7 +701,7 @@ public function __construct() { $el = new GP_Locale(); $el->english_name = 'Greek'; - $el->native_name = 'Ελληνικά'; + $el->native_name = 'Ελληνικά'; $el->lang_code_iso_639_1 = 'el'; $el->lang_code_iso_639_2 = 'ell'; $el->country_code = 'gr'; @@ -816,7 +816,7 @@ public function __construct() { $es = new GP_Locale(); $es->english_name = 'Spanish (Spain)'; - $es->native_name = 'Español'; + $es->native_name = 'Español'; $es->lang_code_iso_639_1 = 'es'; $es->lang_code_iso_639_2 = 'spa'; $es->lang_code_iso_639_3 = 'spa'; @@ -828,7 +828,7 @@ public function __construct() { $es_ar = new GP_Locale(); $es_ar->english_name = 'Spanish (Argentina)'; - $es_ar->native_name = 'Español de Argentina'; + $es_ar->native_name = 'Español de Argentina'; $es_ar->lang_code_iso_639_1 = 'es'; $es_ar->lang_code_iso_639_2 = 'spa'; $es_ar->lang_code_iso_639_3 = 'spa'; @@ -840,7 +840,7 @@ public function __construct() { $es_cl = new GP_Locale(); $es_cl->english_name = 'Spanish (Chile)'; - $es_cl->native_name = 'Español de Chile'; + $es_cl->native_name = 'Español de Chile'; $es_cl->lang_code_iso_639_1 = 'es'; $es_cl->lang_code_iso_639_2 = 'spa'; $es_cl->lang_code_iso_639_3 = 'spa'; @@ -852,7 +852,7 @@ public function __construct() { $es_co = new GP_Locale(); $es_co->english_name = 'Spanish (Colombia)'; - $es_co->native_name = 'Español de Colombia'; + $es_co->native_name = 'Español de Colombia'; $es_co->lang_code_iso_639_1 = 'es'; $es_co->lang_code_iso_639_2 = 'spa'; $es_co->lang_code_iso_639_3 = 'spa'; @@ -864,7 +864,7 @@ public function __construct() { $es_cr = new GP_Locale(); $es_cr->english_name = 'Spanish (Costa Rica)'; - $es_cr->native_name = 'Español de Costa Rica'; + $es_cr->native_name = 'Español de Costa Rica'; $es_cr->lang_code_iso_639_1 = 'es'; $es_cr->lang_code_iso_639_2 = 'spa'; $es_cr->lang_code_iso_639_3 = 'spa'; @@ -876,7 +876,7 @@ public function __construct() { $es_do = new GP_Locale(); $es_do->english_name = 'Spanish (Dominican Republic)'; - $es_do->native_name = 'Español de República Dominicana'; + $es_do->native_name = 'Español de República Dominicana'; $es_do->lang_code_iso_639_1 = 'es'; $es_do->lang_code_iso_639_2 = 'spa'; $es_do->lang_code_iso_639_3 = 'spa'; @@ -888,7 +888,7 @@ public function __construct() { $es_ec = new GP_Locale(); $es_ec->english_name = 'Spanish (Ecuador)'; - $es_ec->native_name = 'Español de Ecuador'; + $es_ec->native_name = 'Español de Ecuador'; $es_ec->lang_code_iso_639_1 = 'es'; $es_ec->lang_code_iso_639_2 = 'spa'; $es_ec->lang_code_iso_639_3 = 'spa'; @@ -900,7 +900,7 @@ public function __construct() { $es_gt = new GP_Locale(); $es_gt->english_name = 'Spanish (Guatemala)'; - $es_gt->native_name = 'Español de Guatemala'; + $es_gt->native_name = 'Español de Guatemala'; $es_gt->lang_code_iso_639_1 = 'es'; $es_gt->lang_code_iso_639_2 = 'spa'; $es_gt->lang_code_iso_639_3 = 'spa'; @@ -912,7 +912,7 @@ public function __construct() { $es_hn = new GP_Locale(); $es_hn->english_name = 'Spanish (Honduras)'; - $es_hn->native_name = 'Español de Honduras'; + $es_hn->native_name = 'Español de Honduras'; $es_hn->lang_code_iso_639_1 = 'es'; $es_hn->lang_code_iso_639_2 = 'spa'; $es_hn->lang_code_iso_639_3 = 'spa'; @@ -924,7 +924,7 @@ public function __construct() { $es_mx = new GP_Locale(); $es_mx->english_name = 'Spanish (Mexico)'; - $es_mx->native_name = 'Español de México'; + $es_mx->native_name = 'Español de México'; $es_mx->lang_code_iso_639_1 = 'es'; $es_mx->lang_code_iso_639_2 = 'spa'; $es_mx->lang_code_iso_639_3 = 'spa'; @@ -936,7 +936,7 @@ public function __construct() { $es_pa = new GP_Locale(); $es_pa->english_name = 'Spanish (Panama)'; - $es_pa->native_name = 'Español de Panamá'; + $es_pa->native_name = 'Español de Panamá'; $es_pa->lang_code_iso_639_1 = 'es'; $es_pa->lang_code_iso_639_2 = 'spa'; $es_pa->lang_code_iso_639_3 = 'spa'; @@ -947,7 +947,7 @@ public function __construct() { $es_pe = new GP_Locale(); $es_pe->english_name = 'Spanish (Peru)'; - $es_pe->native_name = 'Español de Perú'; + $es_pe->native_name = 'Español de Perú'; $es_pe->lang_code_iso_639_1 = 'es'; $es_pe->lang_code_iso_639_2 = 'spa'; $es_pe->lang_code_iso_639_3 = 'spa'; @@ -959,7 +959,7 @@ public function __construct() { $es_pr = new GP_Locale(); $es_pr->english_name = 'Spanish (Puerto Rico)'; - $es_pr->native_name = 'Español de Puerto Rico'; + $es_pr->native_name = 'Español de Puerto Rico'; $es_pr->lang_code_iso_639_1 = 'es'; $es_pr->lang_code_iso_639_2 = 'spa'; $es_pr->lang_code_iso_639_3 = 'spa'; @@ -971,7 +971,7 @@ public function __construct() { $es_us = new GP_Locale(); $es_us->english_name = 'Spanish (US)'; - $es_us->native_name = 'Español de los Estados Unidos'; + $es_us->native_name = 'Español de los Estados Unidos'; $es_us->lang_code_iso_639_1 = 'es'; $es_us->lang_code_iso_639_2 = 'spa'; $es_us->lang_code_iso_639_3 = 'spa'; @@ -981,7 +981,7 @@ public function __construct() { $es_uy = new GP_Locale(); $es_uy->english_name = 'Spanish (Uruguay)'; - $es_uy->native_name = 'Español de Uruguay'; + $es_uy->native_name = 'Español de Uruguay'; $es_uy->lang_code_iso_639_1 = 'es'; $es_uy->lang_code_iso_639_2 = 'spa'; $es_uy->lang_code_iso_639_3 = 'spa'; @@ -993,7 +993,7 @@ public function __construct() { $es_ve = new GP_Locale(); $es_ve->english_name = 'Spanish (Venezuela)'; - $es_ve->native_name = 'Español de Venezuela'; + $es_ve->native_name = 'Español de Venezuela'; $es_ve->lang_code_iso_639_1 = 'es'; $es_ve->lang_code_iso_639_2 = 'spa'; $es_ve->lang_code_iso_639_3 = 'spa'; @@ -1027,7 +1027,7 @@ public function __construct() { $fa = new GP_Locale(); $fa->english_name = 'Persian'; - $fa->native_name = 'فارسی'; + $fa->native_name = 'فارسی'; $fa->lang_code_iso_639_1 = 'fa'; $fa->lang_code_iso_639_2 = 'fas'; $fa->wp_locale = 'fa_IR'; @@ -1041,7 +1041,7 @@ public function __construct() { $fa_af = new GP_Locale(); $fa_af->english_name = 'Persian (Afghanistan)'; - $fa_af->native_name = '(فارسی (افغانستان'; + $fa_af->native_name = '(فارسی (افغانستان'; $fa_af->lang_code_iso_639_1 = 'fa'; $fa_af->lang_code_iso_639_2 = 'fas'; $fa_af->country_code = 'af'; @@ -1083,7 +1083,7 @@ public function __construct() { $fo = new GP_Locale(); $fo->english_name = 'Faroese'; - $fo->native_name = 'Føroyskt'; + $fo->native_name = 'Føroyskt'; $fo->lang_code_iso_639_1 = 'fo'; $fo->lang_code_iso_639_2 = 'fao'; $fo->country_code = 'fo'; @@ -1093,7 +1093,7 @@ public function __construct() { $fon = new GP_Locale(); $fon->english_name = 'Fon'; - $fon->native_name = 'fɔ̀ngbè'; + $fon->native_name = 'fɔ̀ngbè'; $fon->lang_code_iso_639_2 = 'fon'; $fon->lang_code_iso_639_3 = 'fon'; $fon->country_code = 'bj'; @@ -1102,7 +1102,7 @@ public function __construct() { $fr = new GP_Locale(); $fr->english_name = 'French (France)'; - $fr->native_name = 'Français'; + $fr->native_name = 'Français'; $fr->lang_code_iso_639_1 = 'fr'; $fr->country_code = 'fr'; $fr->wp_locale = 'fr_FR'; @@ -1114,7 +1114,7 @@ public function __construct() { $fr_be = new GP_Locale(); $fr_be->english_name = 'French (Belgium)'; - $fr_be->native_name = 'Français de Belgique'; + $fr_be->native_name = 'Français de Belgique'; $fr_be->lang_code_iso_639_1 = 'fr'; $fr_be->lang_code_iso_639_2 = 'fra'; $fr_be->country_code = 'be'; @@ -1125,7 +1125,7 @@ public function __construct() { $fr_ca = new GP_Locale(); $fr_ca->english_name = 'French (Canada)'; - $fr_ca->native_name = 'Français du Canada'; + $fr_ca->native_name = 'Français du Canada'; $fr_ca->lang_code_iso_639_1 = 'fr'; $fr_ca->lang_code_iso_639_2 = 'fra'; $fr_ca->country_code = 'ca'; @@ -1137,7 +1137,7 @@ public function __construct() { $fr_ch = new GP_Locale(); $fr_ch->english_name = 'French (Switzerland)'; - $fr_ch->native_name = 'Français de Suisse'; + $fr_ch->native_name = 'Français de Suisse'; $fr_ch->lang_code_iso_639_1 = 'fr'; $fr_ch->lang_code_iso_639_2 = 'fra'; $fr_ch->country_code = 'ch'; @@ -1209,7 +1209,7 @@ public function __construct() { $gd = new GP_Locale(); $gd->english_name = 'Scottish Gaelic'; - $gd->native_name = 'Gà idhlig'; + $gd->native_name = 'Gàidhlig'; $gd->lang_code_iso_639_1 = 'gd'; $gd->lang_code_iso_639_2 = 'gla'; $gd->lang_code_iso_639_3 = 'gla'; @@ -1232,15 +1232,15 @@ public function __construct() { $gl->facebook_locale = 'gl_ES'; $gn = new GP_Locale(); - $gn->english_name = 'Guaraní'; - $gn->native_name = 'Avañe\'ẽ'; + $gn->english_name = 'Guaraní'; + $gn->native_name = 'Avañe\'ẽ'; $gn->lang_code_iso_639_1 = 'gn'; $gn->lang_code_iso_639_2 = 'grn'; $gn->slug = 'gn'; $gsw = new GP_Locale(); $gsw->english_name = 'Swiss German'; - $gsw->native_name = 'Schwyzerdütsch'; + $gsw->native_name = 'Schwyzerdütsch'; $gsw->lang_code_iso_639_2 = 'gsw'; $gsw->lang_code_iso_639_3 = 'gsw'; $gsw->country_code = 'ch'; @@ -1248,7 +1248,7 @@ public function __construct() { $gu = new GP_Locale(); $gu->english_name = 'Gujarati'; - $gu->native_name = 'ગુજરાતી'; + $gu->native_name = 'ગુજરાતી'; $gu->lang_code_iso_639_1 = 'gu'; $gu->lang_code_iso_639_2 = 'guj'; $gu->wp_locale = 'gu'; @@ -1259,7 +1259,7 @@ public function __construct() { $ha = new GP_Locale(); $ha->english_name = 'Hausa (Arabic)'; - $ha->native_name = 'هَوُسَ'; + $ha->native_name = 'هَوُسَ'; $ha->lang_code_iso_639_1 = 'ha'; $ha->lang_code_iso_639_2 = 'hau'; $ha->slug = 'ha'; @@ -1291,7 +1291,7 @@ public function __construct() { $haw = new GP_Locale(); $haw->english_name = 'Hawaiian'; - $haw->native_name = 'ÅŒlelo HawaiÊ»i'; + $haw->native_name = 'Ōlelo Hawaiʻi'; $haw->lang_code_iso_639_2 = 'haw'; $haw->country_code = 'us'; $haw->wp_locale = 'haw_US'; @@ -1299,7 +1299,7 @@ public function __construct() { $haz = new GP_Locale(); $haz->english_name = 'Hazaragi'; - $haz->native_name = 'هزاره Ú¯ÛŒ'; + $haz->native_name = 'هزاره گی'; $haz->lang_code_iso_639_3 = 'haz'; $haz->country_code = 'af'; $haz->wp_locale = 'haz'; @@ -1309,7 +1309,7 @@ public function __construct() { $he = new GP_Locale(); $he->english_name = 'Hebrew'; - $he->native_name = 'עִבְרִית'; + $he->native_name = 'עִבְרִית'; $he->lang_code_iso_639_1 = 'he'; $he->country_code = 'il'; $he->wp_locale = 'he_IL'; @@ -1321,7 +1321,7 @@ public function __construct() { $hi = new GP_Locale(); $hi->english_name = 'Hindi'; - $hi->native_name = 'हिन्दी'; + $hi->native_name = 'हिन्दी'; $hi->lang_code_iso_639_1 = 'hi'; $hi->lang_code_iso_639_2 = 'hin'; $hi->country_code = 'in'; @@ -1346,7 +1346,7 @@ public function __construct() { $hsb = new GP_Locale(); $hsb->english_name = 'Upper Sorbian'; - $hsb->native_name = 'Hornjoserbšćina'; + $hsb->native_name = 'Hornjoserbšćina'; $hsb->lang_code_iso_639_2 = 'hsb'; $hsb->lang_code_iso_639_3 = 'hsb'; $hsb->country_code = 'de'; @@ -1368,7 +1368,7 @@ public function __construct() { $hy = new GP_Locale(); $hy->english_name = 'Armenian'; - $hy->native_name = 'Õ€Õ¡ÕµÕ¥Ö€Õ¥Õ¶'; + $hy->native_name = 'Հայերեն'; $hy->lang_code_iso_639_1 = 'hy'; $hy->lang_code_iso_639_2 = 'hye'; $hy->country_code = 'am'; @@ -1387,7 +1387,7 @@ public function __construct() { $ibo = new GP_Locale(); $ibo->english_name = 'Igbo'; - $ibo->native_name = 'Asụsụ Igbo'; + $ibo->native_name = 'Asụsụ Igbo'; $ibo->lang_code_iso_639_1 = 'ig'; $ibo->lang_code_iso_639_2 = 'ibo'; $ibo->lang_code_iso_639_3 = 'ibo'; @@ -1422,7 +1422,7 @@ public function __construct() { $ike = new GP_Locale(); $ike->english_name = 'Inuktitut'; - $ike->native_name = 'ᐃᓄᒃᑎᑐᑦ'; + $ike->native_name = 'ᐃᓄᒃᑎᑐᑦ'; $ike->lang_code_iso_639_1 = 'iu'; $ike->lang_code_iso_639_2 = 'iku'; $ike->country_code = 'ca'; @@ -1440,7 +1440,7 @@ public function __construct() { $is = new GP_Locale(); $is->english_name = 'Icelandic'; - $is->native_name = 'Íslenska'; + $is->native_name = 'Íslenska'; $is->lang_code_iso_639_1 = 'is'; $is->lang_code_iso_639_2 = 'isl'; $is->country_code = 'is'; @@ -1464,7 +1464,7 @@ public function __construct() { $ja = new GP_Locale(); $ja->english_name = 'Japanese'; - $ja->native_name = '日本語'; + $ja->native_name = '日本語'; $ja->lang_code_iso_639_1 = 'ja'; $ja->country_code = 'jp'; $ja->wp_locale = 'ja'; @@ -1489,7 +1489,7 @@ public function __construct() { $ka = new GP_Locale(); $ka->english_name = 'Georgian'; - $ka->native_name = 'ქართული'; + $ka->native_name = 'ქართული'; $ka->lang_code_iso_639_1 = 'ka'; $ka->lang_code_iso_639_2 = 'kat'; $ka->country_code = 'ge'; @@ -1544,7 +1544,7 @@ public function __construct() { $kk = new GP_Locale(); $kk->english_name = 'Kazakh'; - $kk->native_name = 'Қазақ тілі'; + $kk->native_name = 'Қазақ тілі'; $kk->lang_code_iso_639_1 = 'kk'; $kk->lang_code_iso_639_2 = 'kaz'; $kk->country_code = 'kz'; @@ -1556,7 +1556,7 @@ public function __construct() { $km = new GP_Locale(); $km->english_name = 'Khmer'; - $km->native_name = 'ភាសាខ្មែរ'; + $km->native_name = 'ភាសាខ្មែរ'; $km->lang_code_iso_639_1 = 'km'; $km->lang_code_iso_639_2 = 'khm'; $km->country_code = 'kh'; @@ -1571,7 +1571,7 @@ public function __construct() { $kmr = new GP_Locale(); $kmr->english_name = 'Kurdish (Kurmanji)'; - $kmr->native_name = 'Kurdî'; + $kmr->native_name = 'Kurdî'; $kmr->lang_code_iso_639_1 = 'ku'; $kmr->lang_code_iso_639_3 = 'kmr'; $kmr->country_code = 'tr'; @@ -1581,7 +1581,7 @@ public function __construct() { $kn = new GP_Locale(); $kn->english_name = 'Kannada'; - $kn->native_name = 'ಕನ್ನಡ'; + $kn->native_name = 'ಕನ್ನಡ'; $kn->lang_code_iso_639_1 = 'kn'; $kn->lang_code_iso_639_2 = 'kan'; $kn->country_code = 'in'; @@ -1593,7 +1593,7 @@ public function __construct() { $ko = new GP_Locale(); $ko->english_name = 'Korean'; - $ko->native_name = '한국어'; + $ko->native_name = '한국어'; $ko->lang_code_iso_639_1 = 'ko'; $ko->lang_code_iso_639_2 = 'kor'; $ko->country_code = 'kr'; @@ -1607,7 +1607,7 @@ public function __construct() { $ks = new GP_Locale(); $ks->english_name = 'Kashmiri'; - $ks->native_name = 'कश्मीरी'; + $ks->native_name = 'कश्मीरी'; $ks->lang_code_iso_639_1 = 'ks'; $ks->lang_code_iso_639_2 = 'kas'; $ks->slug = 'ks'; @@ -1615,7 +1615,7 @@ public function __construct() { $kir = new GP_Locale(); $kir->english_name = 'Kyrgyz'; - $kir->native_name = 'Кыргызча'; + $kir->native_name = 'Кыргызча'; $kir->lang_code_iso_639_1 = 'ky'; $kir->lang_code_iso_639_2 = 'kir'; $kir->lang_code_iso_639_3 = 'kir'; @@ -1638,7 +1638,7 @@ public function __construct() { $lb = new GP_Locale(); $lb->english_name = 'Luxembourgish'; - $lb->native_name = 'Lëtzebuergesch'; + $lb->native_name = 'Lëtzebuergesch'; $lb->lang_code_iso_639_1 = 'lb'; $lb->country_code = 'lu'; $lb->wp_locale = 'lb_LU'; @@ -1657,7 +1657,7 @@ public function __construct() { $lij = new GP_Locale(); $lij->english_name = 'Ligurian'; - $lij->native_name = 'Lìgure'; + $lij->native_name = 'Lìgure'; $lij->lang_code_iso_639_3 = 'lij'; $lij->country_code = 'it'; $lij->wp_locale = 'lij'; @@ -1685,7 +1685,7 @@ public function __construct() { $lo = new GP_Locale(); $lo->english_name = 'Lao'; - $lo->native_name = 'ພາສາລາວ'; + $lo->native_name = 'ພາສາລາວ'; $lo->lang_code_iso_639_1 = 'lo'; $lo->lang_code_iso_639_2 = 'lao'; $lo->country_code = 'la'; @@ -1699,7 +1699,7 @@ public function __construct() { $lt = new GP_Locale(); $lt->english_name = 'Lithuanian'; - $lt->native_name = 'Lietuvių kalba'; + $lt->native_name = 'Lietuvių kalba'; $lt->lang_code_iso_639_1 = 'lt'; $lt->lang_code_iso_639_2 = 'lit'; $lt->country_code = 'lt'; @@ -1722,7 +1722,7 @@ public function __construct() { $lv = new GP_Locale(); $lv->english_name = 'Latvian'; - $lv->native_name = 'LatvieÅ¡u valoda'; + $lv->native_name = 'Latviešu valoda'; $lv->lang_code_iso_639_1 = 'lv'; $lv->lang_code_iso_639_2 = 'lav'; $lv->country_code = 'lv'; @@ -1735,7 +1735,7 @@ public function __construct() { $mai = new GP_Locale(); $mai->english_name = 'Maithili'; - $mai->native_name = 'मैथिली'; + $mai->native_name = 'मैथिली'; $mai->lang_code_iso_639_2 = 'mai'; $mai->lang_code_iso_639_3 = 'mai'; $mai->country_code = 'in'; @@ -1775,7 +1775,7 @@ public function __construct() { $mhr = new GP_Locale(); $mhr->english_name = 'Mari (Meadow)'; - $mhr->native_name = 'Олык марий'; + $mhr->native_name = 'Олык марий'; $mhr->lang_code_iso_639_3 = 'mhr'; $mhr->country_code = 'ru'; $mhr->slug = 'mhr'; @@ -1783,7 +1783,7 @@ public function __construct() { $mk = new GP_Locale(); $mk->english_name = 'Macedonian'; - $mk->native_name = 'Македонски јазик'; + $mk->native_name = 'Македонски јазик'; $mk->lang_code_iso_639_1 = 'mk'; $mk->lang_code_iso_639_2 = 'mkd'; $mk->country_code = 'mk'; @@ -1797,7 +1797,7 @@ public function __construct() { $ml = new GP_Locale(); $ml->english_name = 'Malayalam'; - $ml->native_name = 'മലയാളം'; + $ml->native_name = 'മലയാളം'; $ml->lang_code_iso_639_1 = 'ml'; $ml->lang_code_iso_639_2 = 'mal'; $ml->country_code = 'in'; @@ -1823,7 +1823,7 @@ public function __construct() { $mn = new GP_Locale(); $mn->english_name = 'Mongolian'; - $mn->native_name = 'Монгол'; + $mn->native_name = 'Монгол'; $mn->lang_code_iso_639_1 = 'mn'; $mn->lang_code_iso_639_2 = 'mon'; $mn->country_code = 'mn'; @@ -1835,7 +1835,7 @@ public function __construct() { $mr = new GP_Locale(); $mr->english_name = 'Marathi'; - $mr->native_name = 'मराठी'; + $mr->native_name = 'मराठी'; $mr->lang_code_iso_639_1 = 'mr'; $mr->lang_code_iso_639_2 = 'mar'; $mr->wp_locale = 'mr'; @@ -1846,7 +1846,7 @@ public function __construct() { $mri = new GP_Locale(); $mri->english_name = 'Maori'; - $mri->native_name = 'Te Reo Māori'; + $mri->native_name = 'Te Reo Māori'; $mri->lang_code_iso_639_1 = 'mi'; $mri->lang_code_iso_639_3 = 'mri'; $mri->country_code = 'nz'; @@ -1858,7 +1858,7 @@ public function __construct() { $mrj = new GP_Locale(); $mrj->english_name = 'Mari (Hill)'; - $mrj->native_name = 'Кырык мары'; + $mrj->native_name = 'Кырык мары'; $mrj->lang_code_iso_639_3 = 'mrj'; $mrj->country_code = 'ru'; $mrj->slug = 'mrj'; @@ -1878,13 +1878,13 @@ public function __construct() { $mwl = new GP_Locale(); $mwl->english_name = 'Mirandese'; - $mwl->native_name = 'Mirandés'; + $mwl->native_name = 'Mirandés'; $mwl->lang_code_iso_639_2 = 'mwl'; $mwl->slug = 'mwl'; $my = new GP_Locale(); $my->english_name = 'Myanmar (Burmese)'; - $my->native_name = 'ဗမာစာ'; + $my->native_name = 'ဗမာစာ'; $my->lang_code_iso_639_1 = 'my'; $my->lang_code_iso_639_2 = 'mya'; $my->country_code = 'mm'; @@ -1895,7 +1895,7 @@ public function __construct() { $ne = new GP_Locale(); $ne->english_name = 'Nepali'; - $ne->native_name = 'नेपाली'; + $ne->native_name = 'नेपाली'; $ne->lang_code_iso_639_1 = 'ne'; $ne->lang_code_iso_639_2 = 'nep'; $ne->country_code = 'np'; @@ -1906,8 +1906,8 @@ public function __construct() { $ne->alphabet = 'devanagari'; $nb = new GP_Locale(); - $nb->english_name = 'Norwegian (BokmÃ¥l)'; - $nb->native_name = 'Norsk bokmÃ¥l'; + $nb->english_name = 'Norwegian (Bokmål)'; + $nb->native_name = 'Norsk bokmål'; $nb->lang_code_iso_639_1 = 'nb'; $nb->lang_code_iso_639_2 = 'nob'; $nb->country_code = 'no'; @@ -1936,7 +1936,7 @@ public function __construct() { $nl_be = new GP_Locale(); $nl_be->english_name = 'Dutch (Belgium)'; - $nl_be->native_name = 'Nederlands (België)'; + $nl_be->native_name = 'Nederlands (België)'; $nl_be->lang_code_iso_639_1 = 'nl'; $nl_be->lang_code_iso_639_2 = 'nld'; $nl_be->country_code = 'be'; @@ -1965,8 +1965,8 @@ public function __construct() { $nn->facebook_locale = 'nn_NO'; $nqo = new GP_Locale(); - $nqo->english_name = 'N’ko'; - $nqo->native_name = 'ߒߞߏ'; + $nqo->english_name = 'N’ko'; + $nqo->native_name = 'ߒߞߏ'; $nqo->lang_code_iso_639_2 = 'nqo'; $nqo->lang_code_iso_639_3 = 'nqo'; $nqo->country_code = 'gn'; @@ -2005,7 +2005,7 @@ public function __construct() { $ory = new GP_Locale(); $ory->english_name = 'Oriya'; - $ory->native_name = 'ଓଡ଼ିଆ'; + $ory->native_name = 'ଓଡ଼ିଆ'; $ory->lang_code_iso_639_1 = 'or'; $ory->lang_code_iso_639_2 = 'ory'; $ory->country_code = 'in'; @@ -2016,7 +2016,7 @@ public function __construct() { $os = new GP_Locale(); $os->english_name = 'Ossetic'; - $os->native_name = 'Ирон'; + $os->native_name = 'Ирон'; $os->lang_code_iso_639_1 = 'os'; $os->lang_code_iso_639_2 = 'oss'; $os->wp_locale = 'os'; @@ -2025,7 +2025,7 @@ public function __construct() { $pa = new GP_Locale(); $pa->english_name = 'Panjabi (India)'; - $pa->native_name = 'ਪੰਜਾਬੀ'; + $pa->native_name = 'ਪੰਜਾਬੀ'; $pa->lang_code_iso_639_1 = 'pa'; $pa->lang_code_iso_639_2 = 'pan'; $pa->country_code = 'in'; @@ -2039,7 +2039,7 @@ public function __construct() { $pa_pk = new GP_Locale(); $pa_pk->english_name = 'Punjabi (Pakistan)'; - $pa_pk->native_name = 'پنجابی'; + $pa_pk->native_name = 'پنجابی'; $pa_pk->lang_code_iso_639_1 = 'pa'; $pa_pk->lang_code_iso_639_2 = 'pan'; $pa_pk->country_code = 'pk'; @@ -2051,7 +2051,7 @@ public function __construct() { $pa_pk->alphabet = 'shahmukhi'; $pap_cw = new GP_Locale(); - $pap_cw->english_name = 'Papiamento (Curaçao and Bonaire)'; + $pap_cw->english_name = 'Papiamento (Curaçao and Bonaire)'; $pap_cw->native_name = 'Papiamentu'; $pap_cw->lang_code_iso_639_2 = 'pap'; $pap_cw->lang_code_iso_639_3 = 'pap'; @@ -2070,7 +2070,7 @@ public function __construct() { $pcd = new GP_Locale(); $pcd->english_name = 'Picard'; - $pcd->native_name = 'Ch’ti'; + $pcd->native_name = 'Ch’ti'; $pcd->lang_code_iso_639_3 = 'pcd'; $pcd->country_code = 'fr'; $pcd->wp_locale = 'pcd'; @@ -2110,7 +2110,7 @@ public function __construct() { $pt = new GP_Locale(); $pt->english_name = 'Portuguese (Portugal)'; - $pt->native_name = 'Português'; + $pt->native_name = 'Português'; $pt->lang_code_iso_639_1 = 'pt'; $pt->country_code = 'pt'; $pt->wp_locale = 'pt_PT'; @@ -2120,14 +2120,14 @@ public function __construct() { $pt_ao90 = clone $pt; $pt_ao90->english_name = 'Portuguese (Portugal, AO90)'; - $pt_ao90->native_name = 'Português (AO90)'; + $pt_ao90->native_name = 'Português (AO90)'; $pt_ao90->slug = 'pt/ao90'; $pt_ao90->wp_locale = 'pt_PT_ao90'; $pt_ao90->root_slug = $pt->slug; $pt_ao = new GP_Locale(); $pt_ao->english_name = 'Portuguese (Angola)'; - $pt_ao->native_name = 'Português de Angola'; + $pt_ao->native_name = 'Português de Angola'; $pt_ao->lang_code_iso_639_1 = 'pt'; $pt_ao->country_code = 'ao'; $pt_ao->wp_locale = 'pt_AO'; @@ -2135,7 +2135,7 @@ public function __construct() { $pt_br = new GP_Locale(); $pt_br->english_name = 'Portuguese (Brazil)'; - $pt_br->native_name = 'Português do Brasil'; + $pt_br->native_name = 'Português do Brasil'; $pt_br->lang_code_iso_639_1 = 'pt'; $pt_br->lang_code_iso_639_2 = 'por'; $pt_br->country_code = 'br'; @@ -2148,7 +2148,7 @@ public function __construct() { $ps = new GP_Locale(); $ps->english_name = 'Pashto'; - $ps->native_name = 'پښتو'; + $ps->native_name = 'پښتو'; $ps->lang_code_iso_639_1 = 'ps'; $ps->lang_code_iso_639_2 = 'pus'; $ps->country_code = 'af'; @@ -2160,7 +2160,7 @@ public function __construct() { $rhg = new GP_Locale(); $rhg->english_name = 'Rohingya'; - $rhg->native_name = 'Ruáinga'; + $rhg->native_name = 'Ruáinga'; $rhg->lang_code_iso_639_3 = 'rhg'; $rhg->country_code = 'mm'; $rhg->wp_locale = 'rhg'; @@ -2177,7 +2177,7 @@ public function __construct() { $ro = new GP_Locale(); $ro->english_name = 'Romanian'; - $ro->native_name = 'Română'; + $ro->native_name = 'Română'; $ro->lang_code_iso_639_1 = 'ro'; $ro->lang_code_iso_639_2 = 'ron'; $ro->country_code = 'ro'; @@ -2200,7 +2200,7 @@ public function __construct() { $ru = new GP_Locale(); $ru->english_name = 'Russian'; - $ru->native_name = 'Русский'; + $ru->native_name = 'Русский'; $ru->lang_code_iso_639_1 = 'ru'; $ru->lang_code_iso_639_2 = 'rus'; $ru->country_code = 'ru'; @@ -2214,7 +2214,7 @@ public function __construct() { $rue = new GP_Locale(); $rue->english_name = 'Rusyn'; - $rue->native_name = 'Русиньскый'; + $rue->native_name = 'Русиньскый'; $rue->lang_code_iso_639_3 = 'rue'; $rue->slug = 'rue'; $rue->nplurals = 3; @@ -2223,7 +2223,7 @@ public function __construct() { $rup = new GP_Locale(); $rup->english_name = 'Aromanian'; - $rup->native_name = 'Armãneashce'; + $rup->native_name = 'Armãneashce'; $rup->lang_code_iso_639_2 = 'rup'; $rup->lang_code_iso_639_3 = 'rup'; $rup->country_code = 'mk'; @@ -2231,7 +2231,7 @@ public function __construct() { $sah = new GP_Locale(); $sah->english_name = 'Sakha'; - $sah->native_name = 'Сахалыы'; + $sah->native_name = 'Сахалыы'; $sah->lang_code_iso_639_2 = 'sah'; $sah->lang_code_iso_639_3 = 'sah'; $sah->country_code = 'ru'; @@ -2241,7 +2241,7 @@ public function __construct() { $sa_in = new GP_Locale(); $sa_in->english_name = 'Sanskrit'; - $sa_in->native_name = 'भारतम्'; + $sa_in->native_name = 'भारतम्'; $sa_in->lang_code_iso_639_1 = 'sa'; $sa_in->lang_code_iso_639_2 = 'san'; $sa_in->lang_code_iso_639_3 = 'san'; @@ -2261,7 +2261,7 @@ public function __construct() { $si = new GP_Locale(); $si->english_name = 'Sinhala'; - $si->native_name = 'සිංහල'; + $si->native_name = 'සිංහල'; $si->lang_code_iso_639_1 = 'si'; $si->lang_code_iso_639_2 = 'sin'; $si->country_code = 'lk'; @@ -2273,7 +2273,7 @@ public function __construct() { $sk = new GP_Locale(); $sk->english_name = 'Slovak'; - $sk->native_name = 'Slovenčina'; + $sk->native_name = 'Slovenčina'; $sk->lang_code_iso_639_1 = 'sk'; $sk->lang_code_iso_639_2 = 'slk'; $sk->country_code = 'sk'; @@ -2286,7 +2286,7 @@ public function __construct() { $skr = new GP_Locale(); $skr->english_name = 'Saraiki'; - $skr->native_name = 'سرائیکی'; + $skr->native_name = 'سرائیکی'; $skr->lang_code_iso_639_3 = 'skr'; $skr->country_code = 'pk'; $skr->wp_locale = 'skr'; @@ -2296,7 +2296,7 @@ public function __construct() { $sl = new GP_Locale(); $sl->english_name = 'Slovenian'; - $sl->native_name = 'Slovenščina'; + $sl->native_name = 'Slovenščina'; $sl->lang_code_iso_639_1 = 'sl'; $sl->lang_code_iso_639_2 = 'slv'; $sl->country_code = 'si'; @@ -2318,7 +2318,7 @@ public function __construct() { $snd = new GP_Locale(); $snd->english_name = 'Sindhi'; - $snd->native_name = 'سنڌي'; + $snd->native_name = 'سنڌي'; $snd->lang_code_iso_639_1 = 'sd'; $snd->lang_code_iso_639_2 = 'snd'; $snd->lang_code_iso_639_3 = 'snd'; @@ -2353,7 +2353,7 @@ public function __construct() { $sq_xk = new GP_Locale(); $sq_xk->english_name = 'Shqip (Kosovo)'; - $sq_xk->native_name = 'Për Kosovën Shqip'; + $sq_xk->native_name = 'Për Kosovën Shqip'; $sq_xk->lang_code_iso_639_1 = 'sq'; $sq_xk->country_code = 'xk'; // Temporary country code until Kosovo is assigned an ISO code. $sq_xk->wp_locale = 'sq_XK'; @@ -2361,7 +2361,7 @@ public function __construct() { $sr = new GP_Locale(); $sr->english_name = 'Serbian'; - $sr->native_name = 'Српски језик'; + $sr->native_name = 'Српски језик'; $sr->lang_code_iso_639_1 = 'sr'; $sr->lang_code_iso_639_2 = 'srp'; $sr->country_code = 'rs'; @@ -2444,7 +2444,7 @@ public function __construct() { $szl = new GP_Locale(); $szl->english_name = 'Silesian'; - $szl->native_name = 'Åšlōnskŏ gŏdka'; + $szl->native_name = 'Ślōnskŏ gŏdka'; $szl->lang_code_iso_639_3 = 'szl'; $szl->country_code = 'pl'; $szl->wp_locale = 'szl'; @@ -2455,7 +2455,7 @@ public function __construct() { $ta = new GP_Locale(); $ta->english_name = 'Tamil'; - $ta->native_name = 'தமிழ்'; + $ta->native_name = 'தமிழ்'; $ta->lang_code_iso_639_1 = 'ta'; $ta->lang_code_iso_639_2 = 'tam'; $ta->country_code = 'in'; @@ -2467,7 +2467,7 @@ public function __construct() { $ta_lk = new GP_Locale(); $ta_lk->english_name = 'Tamil (Sri Lanka)'; - $ta_lk->native_name = 'தமிழ்'; + $ta_lk->native_name = 'தமிழ்'; $ta_lk->lang_code_iso_639_1 = 'ta'; $ta_lk->lang_code_iso_639_2 = 'tam'; $ta_lk->country_code = 'lk'; @@ -2490,7 +2490,7 @@ public function __construct() { $te = new GP_Locale(); $te->english_name = 'Telugu'; - $te->native_name = 'తెలుగు'; + $te->native_name = 'తెలుగు'; $te->lang_code_iso_639_1 = 'te'; $te->lang_code_iso_639_2 = 'tel'; $te->wp_locale = 'te'; @@ -2501,7 +2501,7 @@ public function __construct() { $tg = new GP_Locale(); $tg->english_name = 'Tajik'; - $tg->native_name = 'Тоҷикӣ'; + $tg->native_name = 'Тоҷикӣ'; $tg->lang_code_iso_639_1 = 'tg'; $tg->lang_code_iso_639_2 = 'tgk'; $tg->country_code = 'tj'; @@ -2513,7 +2513,7 @@ public function __construct() { $th = new GP_Locale(); $th->english_name = 'Thai'; - $th->native_name = 'ไทย'; + $th->native_name = 'ไทย'; $th->lang_code_iso_639_1 = 'th'; $th->lang_code_iso_639_2 = 'tha'; $th->wp_locale = 'th'; @@ -2527,7 +2527,7 @@ public function __construct() { $tir = new GP_Locale(); $tir->english_name = 'Tigrinya'; - $tir->native_name = 'ትግርኛ'; + $tir->native_name = 'ትግርኛ'; $tir->lang_code_iso_639_1 = 'ti'; $tir->lang_code_iso_639_2 = 'tir'; $tir->country_code = 'er'; @@ -2559,7 +2559,7 @@ public function __construct() { $tr = new GP_Locale(); $tr->english_name = 'Turkish'; - $tr->native_name = 'Türkçe'; + $tr->native_name = 'Türkçe'; $tr->lang_code_iso_639_1 = 'tr'; $tr->lang_code_iso_639_2 = 'tur'; $tr->country_code = 'tr'; @@ -2572,7 +2572,7 @@ public function __construct() { $tt_ru = new GP_Locale(); $tt_ru->english_name = 'Tatar'; - $tt_ru->native_name = 'Татар теле'; + $tt_ru->native_name = 'Татар теле'; $tt_ru->lang_code_iso_639_1 = 'tt'; $tt_ru->lang_code_iso_639_2 = 'tat'; $tt_ru->country_code = 'ru'; @@ -2585,7 +2585,7 @@ public function __construct() { $tuk = new GP_Locale(); $tuk->english_name = 'Turkmen'; - $tuk->native_name = 'Türkmençe'; + $tuk->native_name = 'Türkmençe'; $tuk->lang_code_iso_639_1 = 'tk'; $tuk->lang_code_iso_639_2 = 'tuk'; $tuk->country_code = 'tm'; @@ -2605,7 +2605,7 @@ public function __construct() { $tzm = new GP_Locale(); $tzm->english_name = 'Tamazight (Central Atlas)'; - $tzm->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; + $tzm->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; $tzm->lang_code_iso_639_2 = 'tzm'; $tzm->country_code = 'ma'; $tzm->wp_locale = 'tzm'; @@ -2616,14 +2616,14 @@ public function __construct() { $udm = new GP_Locale(); $udm->english_name = 'Udmurt'; - $udm->native_name = 'Удмурт кыл'; + $udm->native_name = 'Удмурт кыл'; $udm->lang_code_iso_639_2 = 'udm'; $udm->slug = 'udm'; $udm->alphabet = 'cyrillic'; $ug = new GP_Locale(); $ug->english_name = 'Uighur'; - $ug->native_name = 'ئۇيغۇرچە'; + $ug->native_name = 'ئۇيغۇرچە'; $ug->lang_code_iso_639_1 = 'ug'; $ug->lang_code_iso_639_2 = 'uig'; $ug->country_code = 'cn'; @@ -2634,7 +2634,7 @@ public function __construct() { $uk = new GP_Locale(); $uk->english_name = 'Ukrainian'; - $uk->native_name = 'Українська'; + $uk->native_name = 'Українська'; $uk->lang_code_iso_639_1 = 'uk'; $uk->lang_code_iso_639_2 = 'ukr'; $uk->country_code = 'ua'; @@ -2648,7 +2648,7 @@ public function __construct() { $ur = new GP_Locale(); $ur->english_name = 'Urdu'; - $ur->native_name = 'اردو'; + $ur->native_name = 'اردو'; $ur->lang_code_iso_639_1 = 'ur'; $ur->lang_code_iso_639_2 = 'urd'; $ur->country_code = 'pk'; @@ -2661,7 +2661,7 @@ public function __construct() { $uz = new GP_Locale(); $uz->english_name = 'Uzbek'; - $uz->native_name = 'O‘zbekcha'; + $uz->native_name = 'O‘zbekcha'; $uz->lang_code_iso_639_1 = 'uz'; $uz->lang_code_iso_639_2 = 'uzb'; $uz->country_code = 'uz'; @@ -2674,7 +2674,7 @@ public function __construct() { $vec = new GP_Locale(); $vec->english_name = 'Venetian'; - $vec->native_name = 'Vèneto'; + $vec->native_name = 'Vèneto'; $vec->lang_code_iso_639_2 = 'roa'; $vec->lang_code_iso_639_3 = 'vec'; $vec->country_code = 'it'; @@ -2683,7 +2683,7 @@ public function __construct() { $vi = new GP_Locale(); $vi->english_name = 'Vietnamese'; - $vi->native_name = 'Tiếng Việt'; + $vi->native_name = 'Tiếng Việt'; $vi->lang_code_iso_639_1 = 'vi'; $vi->lang_code_iso_639_2 = 'vie'; $vi->country_code = 'vn'; @@ -2728,7 +2728,7 @@ public function __construct() { $xmf = new GP_Locale(); $xmf->english_name = 'Mingrelian'; - $xmf->native_name = 'მარგალური ნინა'; + $xmf->native_name = 'მარგალური ნინა'; $xmf->lang_code_iso_639_3 = 'xmf'; $xmf->country_code = 'ge'; $xmf->slug = 'xmf'; @@ -2736,7 +2736,7 @@ public function __construct() { $yi = new GP_Locale(); $yi->english_name = 'Yiddish'; - $yi->native_name = 'ייִדיש'; + $yi->native_name = 'ייִדיש'; $yi->lang_code_iso_639_1 = 'yi'; $yi->lang_code_iso_639_2 = 'yid'; $yi->slug = 'yi'; @@ -2746,7 +2746,7 @@ public function __construct() { $yor = new GP_Locale(); $yor->english_name = 'Yoruba'; - $yor->native_name = 'Yorùbá'; + $yor->native_name = 'Yorùbá'; $yor->lang_code_iso_639_1 = 'yo'; $yor->lang_code_iso_639_2 = 'yor'; $yor->lang_code_iso_639_3 = 'yor'; @@ -2758,7 +2758,7 @@ public function __construct() { $zgh = new GP_Locale(); $zgh->english_name = 'Tamazight'; - $zgh->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; + $zgh->native_name = 'ⵜⴰⵎⴰⵣⵉⵖⵜ'; $zgh->lang_code_iso_639_2 = 'zgh'; $zgh->lang_code_iso_639_3 = 'zgh'; $zgh->country_code = 'ma'; @@ -2770,7 +2770,7 @@ public function __construct() { $zh = new GP_Locale(); $zh->english_name = 'Chinese'; - $zh->native_name = '中文'; + $zh->native_name = '中文'; $zh->lang_code_iso_639_1 = 'zh'; $zh->lang_code_iso_639_2 = 'zho'; $zh->slug = 'zh'; @@ -2780,7 +2780,7 @@ public function __construct() { $zh_cn = new GP_Locale(); $zh_cn->english_name = 'Chinese (China)'; - $zh_cn->native_name = '简体中文'; + $zh_cn->native_name = '简体中文'; $zh_cn->lang_code_iso_639_1 = 'zh'; $zh_cn->lang_code_iso_639_2 = 'zho'; $zh_cn->country_code = 'cn'; @@ -2795,7 +2795,7 @@ public function __construct() { $zh_hk = new GP_Locale(); $zh_hk->english_name = 'Chinese (Hong Kong)'; - $zh_hk->native_name = '香港中文'; + $zh_hk->native_name = '香港中文'; $zh_hk->lang_code_iso_639_1 = 'zh'; $zh_hk->lang_code_iso_639_2 = 'zho'; $zh_hk->country_code = 'hk'; @@ -2809,7 +2809,7 @@ public function __construct() { $zh_sg = new GP_Locale(); $zh_sg->english_name = 'Chinese (Singapore)'; - $zh_sg->native_name = '中文'; + $zh_sg->native_name = '中文'; $zh_sg->lang_code_iso_639_1 = 'zh'; $zh_sg->lang_code_iso_639_2 = 'zho'; $zh_sg->country_code = 'sg'; @@ -2822,7 +2822,7 @@ public function __construct() { $zh_tw = new GP_Locale(); $zh_tw->english_name = 'Chinese (Taiwan)'; - $zh_tw->native_name = '繁體中文'; + $zh_tw->native_name = '繁體中文'; $zh_tw->lang_code_iso_639_1 = 'zh'; $zh_tw->lang_code_iso_639_2 = 'zho'; $zh_tw->country_code = 'tw'; From 12a4aea02a4f5390f52b42cf0d1ad2bf90b845a3 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:39:59 +0100 Subject: [PATCH 07/10] REST: Use post meta, not user meta, for the author's data. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- .../pattern-directory/includes/pattern-post-type.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/content/plugins/pattern-directory/includes/pattern-post-type.php b/content/plugins/pattern-directory/includes/pattern-post-type.php index f47b79b..21d6857 100644 --- a/content/plugins/pattern-directory/includes/pattern-post-type.php +++ b/content/plugins/pattern-directory/includes/pattern-post-type.php @@ -377,10 +377,15 @@ function register_rest_fields() { 'author_meta', array( 'get_callback' => function( $post ) { + $author_meta = get_post_meta( $post['id'], 'author_meta', true ); + if ( $author_meta === false ) { + return false; + } + return array( - 'name' => esc_html( get_the_author_meta( 'display_name', $post['author'] ) ), - 'url' => esc_url( home_url( '/author/' . get_the_author_meta( 'user_nicename', $post['author'] ) ) ), - 'avatar' => get_avatar_url( $post['author'], array( 'size' => 64 ) ), + 'name' => esc_html( $author_meta['name'] ?? '' ), + 'url' => esc_url( $author_meta['url'] ?? '' ), + 'avatar' => esc_url( $author_meta['avatar'] ?? '' ), ); }, From 8d2793cf6764294b82e16957796d1a395564e827 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Wed, 21 May 2025 21:40:15 +0100 Subject: [PATCH 08/10] Patterns: Use a constant for the patterns URL. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/plugins/pattern-directory/bin/set-core-pattern.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/plugins/pattern-directory/bin/set-core-pattern.php b/content/plugins/pattern-directory/bin/set-core-pattern.php index a5f1f70..3cce402 100644 --- a/content/plugins/pattern-directory/bin/set-core-pattern.php +++ b/content/plugins/pattern-directory/bin/set-core-pattern.php @@ -26,7 +26,7 @@ $opts = getopt( '', array( 'post:', 'url:', 'abspath:', 'block_types:' ) ); if ( empty( $opts['url'] ) ) { - $opts['url'] = 'https://wordpress.org/patterns/'; + $opts['url'] = defined( 'BLOCK_PATTERNS_URL' ) ? BLOCK_PATTERNS_URL : 'https://wordpress.org/patterns/'; } if ( empty( $opts['abspath'] ) && false !== strpos( __DIR__, 'wp-content' ) ) { From f884c6d51bb9807eb58756528387d7b71f68b148 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Thu, 22 May 2025 02:25:44 +0100 Subject: [PATCH 09/10] Patterns: Replace some more patterns URL references. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/plugins/pattern-directory/bin/check-spam.php | 2 +- .../pattern-directory/bin/update-contains-block-types.php | 2 +- content/plugins/pattern-directory/includes/notifications.php | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/content/plugins/pattern-directory/bin/check-spam.php b/content/plugins/pattern-directory/bin/check-spam.php index 6b6883b..c341923 100644 --- a/content/plugins/pattern-directory/bin/check-spam.php +++ b/content/plugins/pattern-directory/bin/check-spam.php @@ -13,7 +13,7 @@ $opts = getopt( '', array( 'post:', 'url:', 'abspath:', 'post_status:', 'per_page:', 'all', 'apply', 'verbose' ) ); if ( empty( $opts['url'] ) ) { - $opts['url'] = 'https://wordpress.org/patterns/'; + $opts['url'] = defined( 'BLOCK_PATTERNS_URL' ) ? BLOCK_PATTERNS_URL : 'https://wordpress.org/patterns/'; } if ( empty( $opts['abspath'] ) && false !== strpos( __DIR__, 'wp-content' ) ) { diff --git a/content/plugins/pattern-directory/bin/update-contains-block-types.php b/content/plugins/pattern-directory/bin/update-contains-block-types.php index 3f6299f..8f8dd7a 100644 --- a/content/plugins/pattern-directory/bin/update-contains-block-types.php +++ b/content/plugins/pattern-directory/bin/update-contains-block-types.php @@ -21,7 +21,7 @@ $opts = getopt( '', array( 'post:', 'url:', 'abspath:', 'per_page:', 'all', 'apply', 'verbose' ) ); if ( empty( $opts['url'] ) ) { - $opts['url'] = 'https://wordpress.org/patterns/'; + $opts['url'] = defined( 'BLOCK_PATTERNS_URL' ) ? BLOCK_PATTERNS_URL : 'https://wordpress.org/patterns/'; } if ( empty( $opts['abspath'] ) && false !== strpos( __DIR__, 'wp-content' ) ) { diff --git a/content/plugins/pattern-directory/includes/notifications.php b/content/plugins/pattern-directory/includes/notifications.php index e61fac0..8c1ffb0 100644 --- a/content/plugins/pattern-directory/includes/notifications.php +++ b/content/plugins/pattern-directory/includes/notifications.php @@ -213,6 +213,8 @@ function notify_pattern_unlisted( $post ) { $subject = esc_html__( 'Pattern unlisted', 'wporg-patterns' ); + $block_patterns_url = defined( 'BLOCK_PATTERNS_URL' ) ? BLOCK_PATTERNS_URL : 'https://wordpress.org/patterns/'; + $message = sprintf( // translators: Plaintext email message. Note the line breaks. 1. Pattern title; 2. Pattern URL; esc_html__( 'Hello, @@ -226,7 +228,7 @@ function notify_pattern_unlisted( $post ) { %3$s', 'wporg-patterns' ), esc_html( $pattern_title ), esc_html( $reason ), - 'https://wordpress.org/patterns/about/' + $block_patterns_url . 'about/' ); if ( $locale ) { From 2758253f19dbcd6b163f8cb3f9d8374912b1b155 Mon Sep 17 00:00:00 2001 From: costdev <79332690+costdev@users.noreply.github.com> Date: Thu, 22 May 2025 02:31:19 +0100 Subject: [PATCH 10/10] Patterns: Replace another patterns URL reference. Signed-off-by: costdev <79332690+costdev@users.noreply.github.com> --- content/plugins/pattern-directory/bin/block-stats.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/plugins/pattern-directory/bin/block-stats.php b/content/plugins/pattern-directory/bin/block-stats.php index 14aeb01..79bf035 100644 --- a/content/plugins/pattern-directory/bin/block-stats.php +++ b/content/plugins/pattern-directory/bin/block-stats.php @@ -20,7 +20,7 @@ $opts = getopt( '', array( 'url:', 'abspath:', 'post_status:', 'verbose' ) ); if ( empty( $opts['url'] ) ) { - $opts['url'] = 'https://wordpress.org/patterns/'; + $opts['url'] = defined( 'BLOCK_PATTERNS_URL' ) ? BLOCK_PATTERNS_URL : 'https://wordpress.org/patterns/'; } if ( empty( $opts['abspath'] ) && false !== strpos( __DIR__, 'wp-content' ) ) {