Tạo icon lá cờ (Country Flag Emoji) từ Country Code (Alpha-2 code)

Nguyên tắc hiển thị ký tự cờ các nước trên trình duyệt (country flag emoji) là sự lắp ghép của 02 emoji (UTF-8) của 2 ký tự ASCII trong country code.

Tại sao tôi làm điều này?

Có vẻ như Windows không hỗ trợ emoji lá cờ, thay vào đó thì lại là 2 ký tự country code.

Trên Windows thì chỉ có Mozilla Firefox hiển thị tốt. Trên hệ điều hành như Mac OS, các nền tảng di động như iOS, Android lại hiển thị đúng.

Tôi có một ứng dụng sử dụng GeoIP để chuyển đổi từ địa chỉ IP của bạn thành thông tin khu vực. Tất nhiên hiển thị tên đất nước cũng ổn, nhưng sẽ đẹp và trực quan hơn nếu có thêm một lá cờ của quốc gia tương ứng.

Cách tạo icon lá cờ (country flag emoji) từ country code

Index của ký tự emoji trong bảng UTF-8 của một ký tự ASCII viết hoa được tính bằng cách lấy index của ký tự đó trong bảng mã ASCII (ví dụ chữ ‘A’ trong bảng ASCII là 65) rồi cộng với 127397 (đây là một magic number).

Ví dụ với country code là “VN”, thì index của emoji tương ứng với “V” (86) là 127483 🇻 và “N” (78) là 127475 🇳. Khi hai ký tự này đứng cạnh nhau thì sẽ trình duyệt sẽ tự “biến” thành lá cờ 🇻🇳.

Nếu chỉ là hiển thị trên trình duyệt với định dạng tài liệu HTML, ta có thể sử dụng ngay cú pháp &#entity_number;, ví dụ 🇻 là 🇻 và 🇳 là 🇳, vậy biểu tượng lá cờ 🇻🇳 sẽ là 🇻🇳.

Tuy nhiên nếu muốn hiển thị dưới dạng text thì ta phải chuyển đổi mã index trên thành ký tự UTF-8. Mỗi ngôn ngữ lập trình lại có hàm riêng thực hiện phép chuyển đổi này.

Sau đây là phần ví dụ thực thi bằng một vài ngôn ngữ phổ biến.

PHP

function get_flag_emoji( $country_code ) {
    $cc = strtoupper( $country_code );
    $html_cc = '&#' . (127397 + ord($cc[0])) . ';&#' . (127397 + ord($cc[1])) . ';'; // for HTML display
    $utf8_cc = mb_convert_encoding( $html_cc, 'UTF-8', 'HTML-ENTITIES' ); // for text display

    return $utf8_cc;
}

echo get_flag_emoji( "VN" );

JavaScript

const getFlagEmoji = countryCode => String.fromCodePoint(
    ...countryCode.toUpperCase().split('')
      .map(char => 127397 + char.charCodeAt())
);

// Display all country flags
["AF", "AX", "AL", "DZ", "AS", "AD", "AO", "AI", "AQ", "AG",
 "AR", "AM", "AW", "AU", "AT", "AZ", "BS", "BH", "BD", "BB",
 "BY", "BE", "BZ", "BJ", "BM", "BT", "BO", "BQ", "BA", "BW",
 "BV", "BR", "IO", "BN", "BG", "BF", "BI", "CV", "KH", "CM",
 "CA", "KY", "CF", "TD", "CL", "CN", "CX", "CC", "CO", "KM",
 "CD", "CG", "CK", "CR", "CI", "HR", "CU", "CW", "CY", "CZ",
 "DK", "DJ", "DM", "DO", "EC", "EG", "SV", "GQ", "ER", "EE",
 "SZ", "ET", "FK", "FO", "FJ", "FI", "FR", "GF", "PF", "TF",
 "GA", "GM", "GE", "DE", "GH", "GI", "GR", "GL", "GD", "GP",
 "GU", "GT", "GG", "GN", "GW", "GY", "HT", "HM", "VA", "HN",
 "HK", "HU", "IS", "IN", "ID", "IR", "IQ", "IE", "IM", "IL",
 "IT", "JM", "JP", "JE", "JO", "KZ", "KE", "KI", "KP", "KR",
 "KW", "KG", "LA", "LV", "LB", "LS", "LR", "LY", "LI", "LT",
 "LU", "MO", "MK", "MG", "MW", "MY", "MV", "ML", "MT", "MH",
 "MQ", "MR", "MU", "YT", "MX", "FM", "MD", "MC", "MN", "ME",
 "MS", "MA", "MZ", "MM", "NA", "NR", "NP", "NL", "NC", "NZ",
 "NI", "NE", "NG", "NU", "NF", "MP", "NO", "OM", "PK", "PW",
 "PS", "PA", "PG", "PY", "PE", "PH", "PN", "PL", "PT", "PR",
 "QA", "RE", "RO", "RU", "RW", "BL", "SH", "KN", "LC", "MF",
 "PM", "VC", "WS", "SM", "ST", "SA", "SN", "RS", "SC", "SL",
 "SG", "SX", "SK", "SI", "SB", "SO", "ZA", "GS", "SS", "ES",
 "LK", "SD", "SR", "SJ", "SE", "CH", "SY", "TW", "TJ", "TZ",
 "TH", "TL", "TG", "TK", "TO", "TT", "TN", "TR", "TM", "TC",
 "TV", "UG", "UA", "AE", "GB", "UM", "US", "UY", "UZ", "VU",
 "VE", "VN", "VG", "VI", "WF", "EH", "YE", "ZM", "ZW"]
 .map(getFlagEmoji).join(" ");

🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇶 🇧🇦 🇧🇼 🇧🇻 🇧🇷 🇮🇴 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇨🇻 🇰🇭 🇨🇲 🇨🇦 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇩 🇨🇬 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇸🇿 🇪🇹 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇲 🇻🇦 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇰🇵 🇰🇷 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇲🇫 🇵🇲 🇻🇨 🇼🇸 🇸🇲 🇸🇹 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇸🇧 🇸🇴 🇿🇦 🇬🇸 🇸🇸 🇪🇸 🇱🇰 🇸🇩 🇸🇷 🇸🇯 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🇺🇲 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇪 🇻🇳 🇻🇬 🇻🇮 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼