array( name, dialling code, fewest digits, most digits, trunk prefix ) * * "Fewest" and "most" are counted WITHOUT the dialling code and WITHOUT the * trunk prefix, because both are stripped before counting anyway. For Serbia * that means 8 to 9 digits, which is what "60 123 4567" and "11 234 567" have. * * The trunk prefix is the zero that is dialled inside the country when calling * from within it ("060..."), and disappears when calling from abroad ("+381 * 60..."). In Russia it is an eight. Italy has NONE, because there the zero * belongs to the number itself and is not stripped: a Rome number is "06 * 1234567" both locally and from abroad. * * Countries that are not on the list are not an error. For them validation runs * by the looser rule in `fallback()`, because it is better to accept a number we * do not know than to wrongly turn away a person who wants to reach us. * * Copyright (c) 2026 Quantum, Serbia. https://2ranq.com * * This program is free software: you can redistribute it and change it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 2 of the License, or any later version. The full * text is in LICENSE.txt, next to this file. * * @package Ranq_Forms */ defined( 'ABSPATH' ) || exit; /** * Ranq_Forms_Phone_Countries */ class Ranq_Forms_Phone_Countries { /** * The whole list, keyed by the country letter code. * * The order is by country name, because that is the order in the picker on * the site. * * @var array|null */ private static $list = null; /** * The country used when nothing else is known. * * The United States, because the plugin goes to WordPress.org and the * audience there is mostly American. */ const DEFAULT_COUNTRY = 'US'; /** * Every country. * * @return array */ public static function all() { if ( null !== self::$list ) { return self::$list; } $list = array( 'AL' => array( 'Albania', '355', 8, 9, '0' ), 'DZ' => array( 'Algeria', '213', 8, 9, '0' ), 'AR' => array( 'Argentina', '54', 10, 11, '0' ), 'AU' => array( 'Australia', '61', 9, 9, '0' ), 'AT' => array( 'Austria', '43', 6, 13, '0' ), 'BH' => array( 'Bahrain', '973', 8, 8, '' ), 'BD' => array( 'Bangladesh', '880', 8, 10, '0' ), 'BY' => array( 'Belarus', '375', 9, 9, '8' ), 'BE' => array( 'Belgium', '32', 8, 9, '0' ), 'BO' => array( 'Bolivia', '591', 8, 8, '' ), 'BA' => array( 'Bosnia and Herzegovina', '387', 8, 9, '0' ), 'BR' => array( 'Brazil', '55', 10, 11, '0' ), 'BG' => array( 'Bulgaria', '359', 8, 9, '0' ), 'CA' => array( 'Canada', '1', 10, 10, '' ), 'CL' => array( 'Chile', '56', 9, 9, '0' ), 'CN' => array( 'China', '86', 7, 12, '0' ), 'CO' => array( 'Colombia', '57', 10, 10, '0' ), 'HR' => array( 'Croatia', '385', 8, 9, '0' ), 'CY' => array( 'Cyprus', '357', 8, 8, '' ), 'CZ' => array( 'Czechia', '420', 9, 9, '' ), 'DK' => array( 'Denmark', '45', 8, 8, '' ), 'EC' => array( 'Ecuador', '593', 8, 9, '0' ), 'EG' => array( 'Egypt', '20', 8, 10, '0' ), 'EE' => array( 'Estonia', '372', 7, 8, '' ), 'ET' => array( 'Ethiopia', '251', 9, 9, '0' ), 'FI' => array( 'Finland', '358', 6, 12, '0' ), 'FR' => array( 'France', '33', 9, 9, '0' ), 'DE' => array( 'Germany', '49', 6, 13, '0' ), 'GH' => array( 'Ghana', '233', 9, 9, '0' ), 'GR' => array( 'Greece', '30', 10, 10, '' ), 'HK' => array( 'Hong Kong', '852', 8, 8, '' ), 'HU' => array( 'Hungary', '36', 8, 9, '06' ), 'IN' => array( 'India', '91', 10, 10, '0' ), 'ID' => array( 'Indonesia', '62', 8, 12, '0' ), 'IQ' => array( 'Iraq', '964', 8, 10, '0' ), 'IE' => array( 'Ireland', '353', 7, 9, '0' ), 'IL' => array( 'Israel', '972', 8, 9, '0' ), 'IT' => array( 'Italy', '39', 6, 11, '' ), 'JP' => array( 'Japan', '81', 9, 10, '0' ), 'JO' => array( 'Jordan', '962', 8, 9, '0' ), 'KE' => array( 'Kenya', '254', 9, 9, '0' ), 'KW' => array( 'Kuwait', '965', 8, 8, '' ), 'LV' => array( 'Latvia', '371', 8, 8, '' ), 'LB' => array( 'Lebanon', '961', 7, 8, '0' ), 'LT' => array( 'Lithuania', '370', 8, 8, '8' ), 'LU' => array( 'Luxembourg', '352', 6, 11, '' ), 'MY' => array( 'Malaysia', '60', 7, 10, '0' ), 'MT' => array( 'Malta', '356', 8, 8, '' ), 'MX' => array( 'Mexico', '52', 10, 10, '0' ), 'MD' => array( 'Moldova', '373', 8, 8, '0' ), 'ME' => array( 'Montenegro', '382', 8, 9, '0' ), 'MA' => array( 'Morocco', '212', 9, 9, '0' ), 'NL' => array( 'Netherlands', '31', 9, 9, '0' ), 'NZ' => array( 'New Zealand', '64', 8, 10, '0' ), 'NG' => array( 'Nigeria', '234', 7, 10, '0' ), 'MK' => array( 'North Macedonia', '389', 8, 8, '0' ), 'NO' => array( 'Norway', '47', 8, 8, '' ), 'OM' => array( 'Oman', '968', 8, 8, '' ), 'PK' => array( 'Pakistan', '92', 10, 10, '0' ), 'PY' => array( 'Paraguay', '595', 9, 9, '0' ), 'PE' => array( 'Peru', '51', 8, 9, '0' ), 'PH' => array( 'Philippines', '63', 8, 10, '0' ), 'PL' => array( 'Poland', '48', 9, 9, '' ), 'PT' => array( 'Portugal', '351', 9, 9, '' ), 'QA' => array( 'Qatar', '974', 8, 8, '' ), 'RO' => array( 'Romania', '40', 9, 9, '0' ), 'RU' => array( 'Russia', '7', 10, 10, '8' ), 'SA' => array( 'Saudi Arabia', '966', 9, 9, '0' ), 'RS' => array( 'Serbia', '381', 8, 9, '0' ), 'SG' => array( 'Singapore', '65', 8, 8, '' ), 'SK' => array( 'Slovakia', '421', 9, 9, '0' ), 'SI' => array( 'Slovenia', '386', 8, 8, '0' ), 'ZA' => array( 'South Africa', '27', 9, 9, '0' ), 'KR' => array( 'South Korea', '82', 8, 10, '0' ), 'ES' => array( 'Spain', '34', 9, 9, '' ), 'SE' => array( 'Sweden', '46', 7, 10, '0' ), 'CH' => array( 'Switzerland', '41', 9, 9, '0' ), 'TW' => array( 'Taiwan', '886', 8, 9, '0' ), 'TZ' => array( 'Tanzania', '255', 9, 9, '0' ), 'TH' => array( 'Thailand', '66', 8, 9, '0' ), 'TN' => array( 'Tunisia', '216', 8, 8, '' ), 'TR' => array( 'Turkey', '90', 10, 10, '0' ), 'UG' => array( 'Uganda', '256', 9, 9, '0' ), 'UA' => array( 'Ukraine', '380', 9, 9, '0' ), 'AE' => array( 'United Arab Emirates', '971', 9, 9, '0' ), 'GB' => array( 'United Kingdom', '44', 9, 10, '0' ), 'US' => array( 'United States', '1', 10, 10, '' ), 'UY' => array( 'Uruguay', '598', 8, 8, '0' ), 'VE' => array( 'Venezuela', '58', 10, 10, '0' ), 'VN' => array( 'Vietnam', '84', 9, 10, '0' ), ); $clean = array(); foreach ( $list as $code => $row ) { $clean[ $code ] = array( 'name' => $row[0], 'dial' => $row[1], 'min' => $row[2], 'max' => $row[3], 'trunk' => $row[4], ); } /** * The country list for the phone field. * * It exists so that somebody can add their own country or correct the * digit count without touching the plugin. * * @param array $clean Countries by letter code. */ self::$list = apply_filters( 'ranq_forms_phone_countries', $clean ); return self::$list; } /** * One country, or the looser record when we do not know it. * * @param string $code The country letter code. * * @return array */ public static function get( $code ) { $code = self::normalize_code( $code ); $all = self::all(); return isset( $all[ $code ] ) ? $all[ $code ] : self::fallback(); } /** * Whether we know the country at all. * * @param string $code The country letter code. * * @return bool */ public static function exists( $code ) { $all = self::all(); return isset( $all[ self::normalize_code( $code ) ] ); } /** * The record for a country that is not on the list. * * Deliberately loose: 4 to 15 digits. Fifteen is the upper limit the * international rule E.164 allows at all, so nothing longer can even exist. * Better to accept a number we cannot check than to turn away a real person. * * @return array */ public static function fallback() { return array( 'name' => '', 'dial' => '', 'min' => 4, 'max' => 15, 'trunk' => '', ); } /** * The letter code reduced to two capital letters. * * @param string $code The raw code. * * @return string Empty when the code is not two letters. */ public static function normalize_code( $code ) { $code = strtoupper( preg_replace( '/[^A-Za-z]/', '', (string) $code ) ); return 2 === strlen( $code ) ? $code : ''; } /** * The number reduced to digits alone, without the dialling code and without * the trunk prefix. * * The script in the browser does exactly the same, in `assets/js/form.js`. If * one changes, so must the other, or the browser and the server stop saying * the same thing. * * @param string $value What the person typed in. * @param array $country The country record. * * @return string Digits alone. */ public static function national_digits( $value, $country ) { $digits = preg_replace( '/\D+/', '', (string) $value ); if ( '' === $digits ) { return ''; } // Someone who types "+381 60..." in themselves must not end up with two codes. $dial = isset( $country['dial'] ) ? (string) $country['dial'] : ''; if ( '' !== $dial && 0 === strpos( $digits, $dial ) ) { $rest = substr( $digits, strlen( $dial ) ); // It is stripped only if what is left can still be a number. Otherwise // the American number "1234567890" would lose its leading one. if ( strlen( $rest ) >= (int) $country['min'] ) { $digits = $rest; } } // The zero or the eight that is dialled only inside the country. $trunk = isset( $country['trunk'] ) ? (string) $country['trunk'] : ''; if ( '' !== $trunk && 0 === strpos( $digits, $trunk ) ) { $rest = substr( $digits, strlen( $trunk ) ); // The zero is NEVER part of the number itself in countries that have it // as a trunk prefix, so it is always stripped. If it were stripped only // when the rest had enough digits, a French number one digit short would // pass as valid, because that zero would fill the missing place. // // In Russia the eight is part of the number, "812" is Saint Petersburg, // so it is stripped only if what is left can still be a number. if ( '0' === substr( $trunk, 0, 1 ) || strlen( $rest ) >= (int) $country['min'] ) { $digits = $rest; } } return $digits; } }