Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.89% covered (success)
91.89%
34 / 37
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ArticleCountryFiltersRegistry
91.89% covered (success)
91.89%
34 / 37
50.00% covered (danger)
50.00%
2 / 4
11.06
0.00% covered (danger)
0.00%
0 / 1
 getCountries
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 getSupportedCountryCodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGroupedCountryCodes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLocalizedRegionsAndCountries
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
1<?php
2
3declare( strict_types = 1 );
4
5namespace MediaWiki\Extension\WikimediaMessages;
6
7use Collator;
8use MediaWiki\Extension\CLDR\CountryNames;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Message\Message;
11use UnexpectedValueException;
12
13class ArticleCountryFiltersRegistry {
14
15    // From https://gitlab.wikimedia.org/repos/movement-insights/canonical-data/-/raw/main/country/countries.tsv
16    // Extracted on 2025-04-17
17    private const SUPPORTED_COUNTRY_CODES = [
18        'AF', 'AX', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG',
19        'AR', 'AM', 'AW', 'AU', 'AT', 'AZ', 'BS', 'BH', 'BD', 'BB',
20        'BY', 'BE', 'BZ', 'BJ', 'BM', 'BT', 'BO', 'BQ', 'BA', 'BW',
21        'BV', 'BR', 'IO', 'VG', 'BN', 'BG', 'BF', 'BI', 'KH', 'CM',
22        'CA', 'CV', 'KY', 'CF', 'TD', 'CL', 'CN', 'CX', 'CC', 'CO',
23        'KM', 'CK', 'CR', 'HR', 'CU', 'CW', 'CY', 'CZ', 'CD', 'DK',
24        'DJ', 'DM', 'DO', 'TL', 'EC', 'EG', 'SV', 'GQ', 'ER', 'EE',
25        'SZ', 'ET', 'FK', 'FO', 'FM', 'FJ', 'FI', 'FR', 'GF', 'PF',
26        'TF', 'GA', 'GM', 'GE', 'DE', 'GH', 'GI', 'GR', 'GL', 'GD',
27        'GP', 'GU', 'GT', 'GG', 'GN', 'GW', 'GY', 'HT', 'HM', 'HN',
28        'HK', 'HU', 'IS', 'IN', 'ID', 'IR', 'IQ', 'IE', 'IM', 'IL',
29        'IT', 'CI', 'JM', 'JP', 'JE', 'JO', 'KZ', 'KE', 'KI', 'XK',
30        'KW', 'KG', 'LA', 'LV', 'LB', 'LS', 'LR', 'LY', 'LI', 'LT',
31        'LU', 'MO', 'MG', 'MW', 'MY', 'MV', 'ML', 'MT', 'MH', 'MQ',
32        'MR', 'MU', 'YT', 'MX', 'MD', 'MC', 'MN', 'ME', 'MS', 'MA',
33        'MZ', 'MM', 'NA', 'NR', 'NP', 'NL', 'NC', 'NZ', 'NI', 'NE',
34        'NG', 'NU', 'NF', 'KP', 'MK', 'MP', 'NO', 'OM', 'PK', 'PW',
35        'PS', 'PA', 'PG', 'PY', 'PE', 'PH', 'PN', 'PL', 'PT', 'PR',
36        'QA', 'CG', 'RE', 'RO', 'RU', 'RW', 'BL', 'SH', 'KN', 'LC',
37        'MF', 'PM', 'VC', 'WS', 'SM', 'ST', 'SA', 'SN', 'RS', 'SC',
38        'SL', 'SG', 'SX', 'SK', 'SI', 'SB', 'SO', 'ZA', 'GS', 'KR',
39        'SS', 'ES', 'LK', 'SD', 'SR', 'SJ', 'SE', 'CH', 'SY', 'TW',
40        'TJ', 'TZ', 'TH', 'TG', 'TK', 'TO', 'TT', 'TN', 'TR', 'TM',
41        'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'VI', 'UY',
42        'UZ', 'VU', 'VA', 'VE', 'VN', 'WF', 'EH', 'YE', 'ZM', 'ZW',
43    ];
44
45    // SUPPORTED_COUNTRY_CODES grouped by region manually.
46    // Region labels are reused from the article topic filters.
47    // In each group, 'countries' is a map of iso-a2 to iso-a3.
48    // iso-a2 is used for localization via CLDR.
49    // iso-a3 can be used with the articlecountry search keyword.
50    private const GROUPED_COUNTRY_CODES = [
51        'asia' => [
52            'msgKey' => 'wikimedia-articletopics-topic-asia',
53            'countries' => [
54                // Afghanistan
55                'AF' => 'AFG',
56                // Armenia
57                'AM' => 'ARM',
58                // Azerbaijan
59                'AZ' => 'AZE',
60                // Bahrain
61                'BH' => 'BHR',
62                // Bangladesh
63                'BD' => 'BGD',
64                // Bhutan
65                'BT' => 'BTN',
66                // Brunei
67                'BN' => 'BRN',
68                // Cambodia
69                'KH' => 'KHM',
70                // China
71                'CN' => 'CHN',
72                // Christmas Island
73                'CX' => 'CXR',
74                // Cocos (Keeling) Islands
75                'CC' => 'CCK',
76                // Georgia
77                'GE' => 'GEO',
78                // India
79                'IN' => 'IND',
80                // Indonesia
81                'ID' => 'IDN',
82                // Iran
83                'IR' => 'IRN',
84                // Iraq
85                'IQ' => 'IRQ',
86                // Israel
87                'IL' => 'ISR',
88                // Japan
89                'JP' => 'JPN',
90                // Kazakhstan
91                'KZ' => 'KAZ',
92                // Kyrgyzstan
93                'KG' => 'KGZ',
94                // North Korea
95                'KP' => 'PRK',
96                // South Korea
97                'KR' => 'KOR',
98                // Kuwait
99                'KW' => 'KWT',
100                // Laos
101                'LA' => 'LAO',
102                // Lebanon
103                'LB' => 'LBN',
104                // Macau
105                'MO' => 'MAC',
106                // Jordan
107                'JO' => 'JOR',
108                // Mongolia
109                'MN' => 'MNG',
110                // Myanmar
111                'MM' => 'MMR',
112                // Maldives
113                'MV' => 'MDV',
114                // Malaysia
115                'MY' => 'MYS',
116                // Nepal
117                'NP' => 'NPL',
118                // Oman
119                'OM' => 'OMN',
120                // Pakistan
121                'PK' => 'PAK',
122                // Philippines
123                'PH' => 'PHL',
124                // Qatar
125                'QA' => 'QAT',
126                // Saudi Arabia
127                'SA' => 'SAU',
128                // Svalbard and Jan Mayen
129                'SJ' => 'SJM',
130                // Singapore
131                'SG' => 'SGP',
132                // Sri Lanka
133                'LK' => 'LKA',
134                // Syria
135                'SY' => 'SYR',
136                // Tajikistan
137                'TJ' => 'TJK',
138                // Thailand
139                'TH' => 'THA',
140                // Timor-Leste
141                'TL' => 'TLS',
142                // Turkmenistan
143                'TM' => 'TKM',
144                // Turkey
145                'TR' => 'TUR',
146                // Uzbekistan
147                'UZ' => 'UZB',
148                // Vietnam
149                'VN' => 'VNM',
150                // Yemen
151                'YE' => 'YEM',
152                // Taiwan
153                'TW' => 'TWN',
154                // United Arab Emirates
155                'AE' => 'ARE',
156                // Hong Kong
157                'HK' => 'HKG',
158                // Palestine
159                'PS' => 'PSE',
160                // British Indian Ocean Territory
161                'IO' => 'IOT',
162            ],
163        ],
164        'africa' => [
165            'msgKey' => 'wikimedia-articletopics-topic-africa',
166            'countries' => [
167                // Algeria
168                'DZ' => 'DZA',
169                // Angola
170                'AO' => 'AGO',
171                // Benin
172                'BJ' => 'BEN',
173                // Botswana
174                'BW' => 'BWA',
175                // Burkina Faso
176                'BF' => 'BFA',
177                // Burundi
178                'BI' => 'BDI',
179                // Cameroon
180                'CM' => 'CMR',
181                // Cape Verde
182                'CV' => 'CPV',
183                // Central African Republic
184                'CF' => 'CAF',
185                // Chad
186                'TD' => 'TCD',
187                // Comoros
188                'KM' => 'COM',
189                // Democratic Republic of the Congo
190                'CD' => 'COD',
191                // Republic of the Congo
192                'CG' => 'COG',
193                // Côte d'Ivoire
194                'CI' => 'CIV',
195                // Djibouti
196                'DJ' => 'DJI',
197                // Egypt
198                'EG' => 'EGY',
199                // Equatorial Guinea
200                'GQ' => 'GNQ',
201                // Eritrea
202                'ER' => 'ERI',
203                // Eswatini
204                'SZ' => 'SWZ',
205                // Ethiopia
206                'ET' => 'ETH',
207                // Gabon
208                'GA' => 'GAB',
209                // Gambia
210                'GM' => 'GMB',
211                // Ghana
212                'GH' => 'GHA',
213                // Guinea
214                'GN' => 'GIN',
215                // Guinea-Bissau
216                'GW' => 'GNB',
217                // Kenya
218                'KE' => 'KEN',
219                // Lesotho
220                'LS' => 'LSO',
221                // Liberia
222                'LR' => 'LBR',
223                // Libya
224                'LY' => 'LBY',
225                // Madagascar
226                'MG' => 'MDG',
227                // Malawi
228                'MW' => 'MWI',
229                // Mali
230                'ML' => 'MLI',
231                // Mauritania
232                'MR' => 'MRT',
233                // Mauritius
234                'MU' => 'MUS',
235                // Mayotte
236                'YT' => 'MYT',
237                // Morocco
238                'MA' => 'MAR',
239                // Mozambique
240                'MZ' => 'MOZ',
241                // Namibia
242                'NA' => 'NAM',
243                // Niger
244                'NE' => 'NER',
245                // Nigeria
246                'NG' => 'NGA',
247                // Rwanda
248                'RW' => 'RWA',
249                // Réunion
250                'RE' => 'REU',
251                // Sao Tome and Principe
252                'ST' => 'STP',
253                // Senegal
254                'SN' => 'SEN',
255                // Seychelles
256                'SC' => 'SYC',
257                // Sierra Leone
258                'SL' => 'SLE',
259                // Somalia
260                'SO' => 'SOM',
261                // South Africa
262                'ZA' => 'ZAF',
263                // South Sudan
264                'SS' => 'SSD',
265                // Sudan
266                'SD' => 'SDN',
267                // Tanzania
268                'TZ' => 'TZA',
269                // Togo
270                'TG' => 'TGO',
271                // Tunisia
272                'TN' => 'TUN',
273                // Uganda
274                'UG' => 'UGA',
275                // Western Sahara
276                'EH' => 'ESH',
277                // Zambia
278                'ZM' => 'ZMB',
279                // Zimbabwe
280                'ZW' => 'ZWE',
281            ],
282        ],
283        'north-america' => [
284            'msgKey' => 'wikimedia-articletopics-topic-north-america',
285            'countries' => [
286                // Canada
287                'CA' => 'CAN',
288                // United States
289                'US' => 'USA',
290                // Mexico
291                'MX' => 'MEX',
292                // Cuba
293                'CU' => 'CUB',
294                // Haiti
295                'HT' => 'HTI',
296                // Jamaica
297                'JM' => 'JAM',
298                // Bahamas
299                'BS' => 'BHS',
300                // Barbados
301                'BB' => 'BRB',
302                // Antigua and Barbuda
303                'AG' => 'ATG',
304                // Dominica
305                'DM' => 'DMA',
306                // Dominican Republic
307                'DO' => 'DOM',
308                // Greenland
309                'GL' => 'GRL',
310                // Puerto Rico
311                'PR' => 'PRI',
312                // Anguilla
313                'AI' => 'AIA',
314                // Aruba
315                'AW' => 'ABW',
316                // Bermuda
317                'BM' => 'BMU',
318                // Bonaire, Sint Eustatius and Saba
319                'BQ' => 'BES',
320                // British Virgin Islands
321                'VG' => 'VGB',
322                // Cayman Islands
323                'KY' => 'CYM',
324                // Curaçao
325                'CW' => 'CUW',
326                // Grenada
327                'GD' => 'GRD',
328                // Guadeloupe
329                'GP' => 'GLP',
330                // Martinique
331                'MQ' => 'MTQ',
332                // Montserrat
333                'MS' => 'MSR',
334                // Saint Barthélemy
335                'BL' => 'BLM',
336                // Saint Helena, Ascension and Tristan da Cunha
337                'SH' => 'SHN',
338                // Saint Kitts and Nevis
339                'KN' => 'KNA',
340                // Saint Lucia
341                'LC' => 'LCA',
342                // Saint Martin
343                'MF' => 'MAF',
344                // Saint Pierre and Miquelon
345                'PM' => 'SPM',
346                // Saint Vincent and the Grenadines
347                'VC' => 'VCT',
348                // Sint Maarten
349                'SX' => 'SXM',
350                // Turks and Caicos Islands
351                'TC' => 'TCA',
352                // Virgin Islands (U.S.)
353                'VI' => 'VIR',
354            ],
355        ],
356        'south-america' => [
357            'msgKey' => 'wikimedia-articletopics-topic-south-america',
358            'countries' => [
359                // Argentina
360                'AR' => 'ARG',
361                // Bolivia
362                'BO' => 'BOL',
363                // Brazil
364                'BR' => 'BRA',
365                // Chile
366                'CL' => 'CHL',
367                // Colombia
368                'CO' => 'COL',
369                // Guyana
370                'GY' => 'GUY',
371                // Paraguay
372                'PY' => 'PRY',
373                // Peru
374                'PE' => 'PER',
375                // Suriname
376                'SR' => 'SUR',
377                // Trinidad and Tobago
378                'TT' => 'TTO',
379                // Uruguay
380                'UY' => 'URY',
381                // Venezuela
382                'VE' => 'VEN',
383                // Ecuador
384                'EC' => 'ECU',
385                // Falkland Islands
386                'FK' => 'FLK',
387                // French Guiana
388                'GF' => 'GUF',
389                // South Georgia and the South Sandwich Islands
390                'GS' => 'SGS',
391            ],
392        ],
393        'europe' => [
394            'msgKey' => 'wikimedia-articletopics-topic-europe',
395            'countries' => [
396                // Ã…land Islands
397                'AX' => 'ALA',
398                // Albania
399                'AL' => 'ALB',
400                // Andorra
401                'AD' => 'AND',
402                // Austria
403                'AT' => 'AUT',
404                // Belarus
405                'BY' => 'BLR',
406                // Belgium
407                'BE' => 'BEL',
408                // Bosnia and Herzegovina
409                'BA' => 'BIH',
410                // Bulgaria
411                'BG' => 'BGR',
412                // Croatia
413                'HR' => 'HRV',
414                // Cyprus
415                'CY' => 'CYP',
416                // Czech Republic
417                'CZ' => 'CZE',
418                // Denmark
419                'DK' => 'DNK',
420                // Estonia
421                'EE' => 'EST',
422                // Faroe Islands
423                'FO' => 'FRO',
424                // Finland
425                'FI' => 'FIN',
426                // France
427                'FR' => 'FRA',
428                // Germany
429                'DE' => 'DEU',
430                // Gibraltar
431                'GI' => 'GIB',
432                // Greece
433                'GR' => 'GRC',
434                // Guernsey
435                'GG' => 'GGY',
436                // Hungary
437                'HU' => 'HUN',
438                // Iceland
439                'IS' => 'ISL',
440                // Ireland
441                'IE' => 'IRL',
442                // Isle of Man
443                'IM' => 'IMN',
444                // Italy
445                'IT' => 'ITA',
446                // Jersey
447                'JE' => 'JEY',
448                // Latvia
449                'LV' => 'LVA',
450                // Lithuania
451                'LT' => 'LTU',
452                // Liechtenstein
453                'LI' => 'LIE',
454                // Luxembourg
455                'LU' => 'LUX',
456                // Malta
457                'MT' => 'MLT',
458                // Moldova
459                'MD' => 'MDA',
460                // Monaco
461                'MC' => 'MCO',
462                // North Macedonia
463                'MK' => 'MKD',
464                // Montenegro
465                'ME' => 'MNE',
466                // Netherlands
467                'NL' => 'NLD',
468                // Norway
469                'NO' => 'NOR',
470                // Poland
471                'PL' => 'POL',
472                // Portugal
473                'PT' => 'PRT',
474                // Romania
475                'RO' => 'ROU',
476                // Russia
477                'RU' => 'RUS',
478                // Serbia
479                'RS' => 'SRB',
480                // Kosovo
481                'XK' => 'XKX',
482                // Slovakia
483                'SK' => 'SVK',
484                // Slovenia
485                'SI' => 'SVN',
486                // Spain
487                'ES' => 'ESP',
488                // Sweden
489                'SE' => 'SWE',
490                // Switzerland
491                'CH' => 'CHE',
492                // Ukraine
493                'UA' => 'UKR',
494                // United Kingdom
495                'GB' => 'GBR',
496                // San Marino
497                'SM' => 'SMR',
498                // Vatican City
499                'VA' => 'VAT',
500            ],
501        ],
502        'oceania' => [
503            'msgKey' => 'wikimedia-articletopics-topic-oceania',
504            'countries' => [
505                // Australia
506                'AU' => 'AUS',
507                // New Zealand
508                'NZ' => 'NZL',
509                // Fiji
510                'FJ' => 'FJI',
511                // Papua New Guinea
512                'PG' => 'PNG',
513                // Solomon Islands
514                'SB' => 'SLB',
515                // Vanuatu
516                'VU' => 'VUT',
517                // Micronesia
518                'FM' => 'FSM',
519                // Marshall Islands
520                'MH' => 'MHL',
521                // Palau
522                'PW' => 'PLW',
523                // Kiribati
524                'KI' => 'KIR',
525                // Tuvalu
526                'TV' => 'TUV',
527                // Tonga
528                'TO' => 'TON',
529                // Niue
530                'NU' => 'NIU',
531                // New Caledonia
532                'NC' => 'NCL',
533                // French Polynesia
534                'PF' => 'PYF',
535                // Tokelau
536                'TK' => 'TKL',
537                // American Samoa
538                'AS' => 'ASM',
539                // Cook Islands
540                'CK' => 'COK',
541                // Guam
542                'GU' => 'GUM',
543                // Nauru
544                'NR' => 'NRU',
545                // Norfolk Island
546                'NF' => 'NFK',
547                // Northern Mariana Islands
548                'MP' => 'MNP',
549                // Samoa
550                'WS' => 'WSM',
551                // United States Minor Outlying Islands
552                'UM' => 'UMI',
553                // Wallis and Futuna
554                'WF' => 'WLF',
555                // Pitcairn Islands
556                'PN' => 'PCN',
557                // Antarctica
558                'AQ' => 'ATA',
559                // Bouvet Island
560                'BV' => 'BVT',
561                // French Southern Territories
562                'TF' => 'ATF',
563                // Heard Island and McDonald Islands
564                'HM' => 'HMD',
565            ],
566        ],
567        'central-america' => [
568            'msgKey' => 'wikimedia-articletopics-topic-central-america',
569            'countries' => [
570                // Belize
571                'BZ' => 'BLZ',
572                // Costa Rica
573                'CR' => 'CRI',
574                // El Salvador
575                'SV' => 'SLV',
576                // Guatemala
577                'GT' => 'GTM',
578                // Honduras
579                'HN' => 'HND',
580                // Nicaragua
581                'NI' => 'NIC',
582                // Panama
583                'PA' => 'PAN',
584            ],
585        ],
586    ];
587
588    /**
589     * Get a list of country codes with localized labels.
590     *
591     * @param array $countryCodes
592     * @param array $cldrA2toLabelMap
593     * @param Collator $collator
594     * @throws UnexpectedValueException When a requested country code is not supported.
595     * @return array [
596     *         [
597     *             'id' => 'afg',
598     *             'label' => 'Afghanistan',
599     *         ],
600     *         ...
601     * ]
602     */
603    private static function getCountries(
604        array $countryCodes,
605        array $cldrA2toLabelMap,
606        Collator $collator
607    ): array {
608        $countries = [];
609
610        foreach ( $countryCodes as $a2 => $a3 ) {
611            if ( !in_array( $a2, self::SUPPORTED_COUNTRY_CODES, true ) ) {
612                throw new UnexpectedValueException( "Unsupported country code: $a2" );
613            }
614
615            if ( !isset( $cldrA2toLabelMap[$a2] ) ) {
616                // warning: missing localized label
617                continue;
618            }
619            $countries[] = [
620                'id' => strtolower( $a3 ),
621                'label' => $cldrA2toLabelMap[$a2],
622            ];
623        }
624
625        usort(
626            $countries,
627            static fn ( array $a, array $b ) => $collator->compare( $a['label'], $b['label'] )
628        );
629
630        return $countries;
631    }
632
633    /**
634     * Get the list of supported country codes.
635     *
636     * @return array
637     */
638    public static function getSupportedCountryCodes(): array {
639        return self::SUPPORTED_COUNTRY_CODES;
640    }
641
642    /**
643     * Get the grouped country codes by region.
644     *
645     * @return array
646     */
647    public static function getGroupedCountryCodes(): array {
648        return self::GROUPED_COUNTRY_CODES;
649    }
650
651    /**
652     * Get a list of regions and their countries, with localized labels.
653     *
654     * @param string $languageCode
655     * @return array [
656     *         [
657     *             'id' => 'asia',
658     *             'label' => 'Asia',
659     *             'countries' => [
660     *                 [
661     *                     'id' => 'afg',
662     *                     'label' => 'Afghanistan',
663     *                 ],
664     *                 ...
665     *             ],
666     *         ],
667     *         ...
668     * ]
669     */
670    public static function getLocalizedRegionsAndCountries( string $languageCode ): array {
671        $regions = [];
672        $services = MediaWikiServices::getInstance();
673        $collator = Collator::create( $languageCode ) ?: Collator::create( 'root' );
674
675        if ( !(
676            $services->getExtensionRegistry()->isLoaded( 'cldr' )
677            || $services->getExtensionRegistry()->isLoaded( 'CLDR' )
678        ) ) {
679            // todo: warning: CLDR extension not loaded
680            return [];
681        }
682
683        $cldrA2toLabelMap = CountryNames::getNames( $languageCode );
684
685        $language = $services->getLanguageFactory()->getLanguage( $languageCode );
686
687        foreach ( self::GROUPED_COUNTRY_CODES as $region => $regionInfo ) {
688            $labelMsg = new Message( $regionInfo['msgKey'], [], $language );
689            $regions[] = [
690                'id' => $region,
691                'label' => $labelMsg->text(),
692                'countries' => self::getCountries( $regionInfo['countries'], $cldrA2toLabelMap, $collator ),
693            ];
694        }
695
696        usort(
697            $regions,
698            static fn ( array $a, array $b ) => $collator->compare( $a['label'], $b['label'] )
699        );
700
701        return $regions;
702    }
703}