├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── examples ├── local │ └── index.html └── no-npm │ └── index.html ├── package.json ├── src ├── emoji.ts └── index.ts ├── LICENSE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "never" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.8 4 | 5 | - Fix NPM publish mistake which didn't include the TwemojiCountryFlags.woff2 font file 6 | - No code changes 7 | 8 | ## 0.1.7 9 | 10 | - Change package.json such that, hopefully, it works better in everybody's build setup (notably the TypeScript types). Thanks @DanielleHuisman for PR #13. 11 | - Switch to unbuild to enable the above. 12 | - Add this changelog. 13 | 14 | ## 0.1.6 15 | 16 | Sorry I forgot, please see the commit history. 17 | -------------------------------------------------------------------------------- /examples/local/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | country-flag-emoji-polyfill 4 | 8 | 13 | 14 |

country-flag-emoji-polyfill local-dev test page

15 |

16 | Run a dev server on the repository root, then open this page in your browser at http://localhost:PORT/examples/local 17 | 18 |

19 | If it works, this is a flag on all browsers, including Chrome-based browsers on Windows: 20 | 21 |

22 | 🇫🇮 23 | 24 | -------------------------------------------------------------------------------- /examples/no-npm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | country-flag-emoji-polyfill 4 | 8 | 13 | 14 |

country-flag-emoji-polyfill example

15 |

16 | This examples shows how to use country-flag-emoji-polyfill without NPM, 17 | by using the power of the excellent Skypack CDN. 18 | 19 |

20 | If it works, this is a flag on all browsers, including Chrome-based browsers on Windows: 21 | 22 |

23 | 🇫🇮 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "country-flag-emoji-polyfill", 3 | "version": "0.1.8", 4 | "description": "Add country flag emojis to Chromium-based browsers on Windows", 5 | "repository": "https://github.com/talkjs/country-flag-emoji-polyfill", 6 | "author": "Egbert Teeselink", 7 | "license": "MIT", 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "main": "./dist/index.cjs", 16 | "types": "./dist/index.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "build": "unbuild --minify", 22 | "dev": "nodemon -w src -e ts -x \"npm run build\"", 23 | "make-font": "cd build && ./make-font.sh" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^3.0.2", 27 | "unbuild": "^2.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/emoji.ts: -------------------------------------------------------------------------------- 1 | // emoji detection code inspired by if-emoji and emoji-picker-element, with modifications. 2 | const FONT_FAMILY = 3 | '"Twemoji Mozilla","Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",' + 4 | '"Noto Color Emoji","EmojiOne Color","Android Emoji",sans-serif'; 5 | 6 | function makeCtx() { 7 | const canvas = document.createElement("canvas"); 8 | canvas.width = canvas.height = 1; 9 | const ctx = canvas.getContext("2d", { willReadFrequently: true })!; 10 | ctx.textBaseline = "top"; 11 | ctx.font = `100px ${FONT_FAMILY}`; 12 | ctx.scale(0.01, 0.01); 13 | return ctx; 14 | } 15 | 16 | function getColor(ctx: CanvasRenderingContext2D, text: string, color: string) { 17 | // we're rendering to a 1px canvas so it'll be a character (or, hopefully, a 18 | // color emoji) scaled down to a single vague brownish pixel 19 | ctx.clearRect(0, 0, 100, 100); 20 | ctx.fillStyle = color; 21 | ctx.fillText(text, 0, 0); 22 | 23 | const bytes = ctx.getImageData(0, 0, 1, 1).data; 24 | return bytes.join(","); 25 | } 26 | 27 | /** 28 | * Detects whether the emoji in `text` is rendered as a color emoji by this 29 | * browser. 30 | * 31 | * Note: this is not complete for detecting support for any emoji. Notably, it 32 | * does not detect whether emojis that consist of two glyphs with a 33 | * zero-width-joiner are rendered as a single emoji or as two, because this is 34 | * not needed to detect country flag support. 35 | */ 36 | export function supportsEmoji(text: string) { 37 | // Render `text` to a single pixel in white and in black, and then compare 38 | // them to each other and ensure they're the same color, and neither one is 39 | // black. This shows that the emoji was rendered in color, and the font color 40 | // was disregarded. 41 | const ctx = makeCtx(); 42 | const white = getColor(ctx, text, "#fff"); 43 | const black = getColor(ctx, text, "#000"); 44 | 45 | // This is RGBA, so for 0,0,0, we are checking that the first RGB is not all zeroes. 46 | // Most of the time when unsupported this is 0,0,0,0, but on Chrome on Mac it is 47 | // 0,0,0,61 - there is a transparency here. 48 | return black === white && !black.startsWith("0,0,0,"); 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { supportsEmoji } from "./emoji"; 2 | 3 | /** 4 | * Injects a style element into the HEAD with a web font with country flags, 5 | * iff the browser does support emojis but not country flags. 6 | * 7 | * @param fontName - Override the default font name ("Twemoji Country Flags") 8 | * @param fontUrl - Override the font URL (defaults to a jsdeliver-hosted) 9 | * 10 | * @returns true if the web font was loaded (ie the browser does not support country flags) 11 | */ 12 | export function polyfillCountryFlagEmojis( 13 | fontName = "Twemoji Country Flags", 14 | fontUrl = "https://cdn.jsdelivr.net/npm/country-flag-emoji-polyfill@0.1/dist/TwemojiCountryFlags.woff2" 15 | ) { 16 | if ( 17 | typeof window !== "undefined" && 18 | supportsEmoji("😊") && 19 | !supportsEmoji("🇨🇭") 20 | ) { 21 | const style = document.createElement("style"); 22 | 23 | // I generated the `unicode-range` below using 24 | // https://wakamaifondue.com/beta/, which is awesome and it helps make sure 25 | // this font is never tried for any character that it does not support. 26 | // 27 | // See build/make-font.sh for more background why these are the relevant 28 | // unicode ranges. 29 | // 30 | // Also, we're setting `font-display` to "swap" because without it, all text 31 | // will be invisible during the time between this style tag being injected 32 | // and the font having been loaded. This happens because developers will 33 | // typically set `Twemoji Country Flags` as the first font in their 34 | // `font-family` lists, and the browser tries to prevent a "flash of 35 | // unstyled text" and therefore hide all text instead of rendering it with 36 | // potentially the wrong font. This matters when you're waiting for Open 37 | // Sans to load and you dont want your blog to briefly render in Times New 38 | // Roman first, but it's actively harmful for apps, where text might briefly 39 | // disappear just to load some country flag fallbacks that might not even be 40 | // on the page. 41 | // 42 | // Apparently (when I tested this) browsers aren't smart enough to only do 43 | // hide characters that match the not-yet-loaded font's unicode-range. 44 | // Setting it to "swap" unfortunately makes the browser render eg `□` or 45 | // `ɴʟ` for country flags until the font is in. But this is way better than 46 | // hiding all UI text everywhere :D 47 | style.textContent = `@font-face { 48 | font-family: "${fontName}"; 49 | unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E0063, U+E0065, U+E0067, 50 | U+E006C, U+E006E, U+E0073-E0074, U+E0077, U+E007F; 51 | src: url('${fontUrl}') format('woff2'); 52 | font-display: swap; 53 | }`; 54 | document.head.appendChild(style); 55 | 56 | return true; 57 | } 58 | return false; 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License for the Code (MIT) 2 | 3 | Copyright (c) 2022 TalkJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ## License for the Visual Design 24 | 25 | The Emoji art in the "Twemoji Country Flags" font comes from [Twemoji](https://twitter.github.io/twemoji), 26 | and is used and redistributed under the CC-BY-4.0 [license terms](https://github.com/twitter/twemoji#license) 27 | offered by the Twemoji project. 28 | 29 | ### Creative Commons Attribution 4.0 International (CC BY 4.0) 30 | https://creativecommons.org/licenses/by/4.0/legalcode 31 | or for the human readable summary: https://creativecommons.org/licenses/by/4.0/ 32 | 33 | 34 | #### You are free to: 35 | **Share** — copy and redistribute the material in any medium or format 36 | 37 | **Adapt** — remix, transform, and build upon the material for any purpose, even commercially. 38 | 39 | The licensor cannot revoke these freedoms as long as you follow the license terms. 40 | 41 | 42 | #### Under the following terms: 43 | **Attribution** — You must give appropriate credit, provide a link to the license, 44 | and indicate if changes were made. 45 | You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 46 | 47 | **No additional restrictions** — You may not apply legal terms or **technological measures** 48 | that legally restrict others from doing anything the license permits. 49 | 50 | #### Notices: 51 | You do not have to comply with the license for elements of the material in the public domain 52 | or where your use is permitted by an applicable exception or limitation. No warranties are given. 53 | The license may not give you all of the permissions necessary for your intended use. 54 | For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Country Flag Emoji Polyfill 2 | 3 | ![screenshot of broken emojis](https://user-images.githubusercontent.com/703546/159265695-1ed79f91-2398-4e02-a38d-7aa67426d945.png) 4 | 5 | Recent Windows versions (finally) support emojis natively, but they still do not support pretty country flags. By extension, all Chromium-based browsers can't display country flag emojis natively. 6 | 7 | In short, if "🇨🇭" looks like "ᴄʜ" and not like a flag, then this polyfill is for you. 8 | 9 | It's 0.7kB gzipped, with zero dependencies. The font with country flags is 77kB and only downloaded when needed. 10 | 11 | --- 12 | 13 | This module is sponsored by [TalkJS](https://talkjs.com), a Chat API with pre-built UI for web & mobile apps. 14 | 15 | [![talkjs logo](https://user-images.githubusercontent.com/703546/159268048-19871f36-90f2-409f-ad9f-af711abc8302.png)](https://talkjs.com) 16 | 17 | ## Usage 18 | 19 | ### 1. With NPM: 20 | 21 | ```sh 22 | npm install country-flag-emoji-polyfill 23 | ``` 24 | 25 | ```js 26 | import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; 27 | 28 | // ... 29 | 30 | polyfillCountryFlagEmojis(); 31 | ``` 32 | 33 | #### Or, just with a script tag: 34 | 35 | Thanks to the excellent [Skypack CDN](https://www.skypack.dev), you can also use this polyfill without NPM: 36 | 37 | ```html 38 | 42 | ``` 43 | 44 | This code only works on browsers that support ES Modules, but Chromium has done so for quite a while so it should work appropriately. 45 | 46 | [See here for a full working example](https://talkjs.github.io/country-flag-emoji-polyfill/examples/no-npm/index.html) ([source](./examples/no-npm/index.html)) 47 | 48 | ### 2. Update your CSS 49 | 50 | This will load a webfont called `"Twemoji Country Flags"` on relevant browsers. Next, prefix your `font-family` CSS with this font **everywhere where you want country flags to work**. Eg if your CSS currently has this: 51 | 52 | ``` 53 | body { 54 | font-family: "Helvetica", "Comic Sans", serif; 55 | } 56 | ``` 57 | 58 | then you want to change it to 59 | 60 | ``` 61 | body { 62 | font-family: "Twemoji Country Flags", "Helvetica", "Comic Sans", serif; 63 | } 64 | ``` 65 | 66 | This is safe because the font is loaded such that the browser will only use it for country flag emojis and not for any other characters (using [`unicode-range`](https://github.com/talkjs/country-flag-emoji-polyfill/blob/master/src/index.ts#L45)). Therefore, the browser will simply use the next font in the list for every character except country flags. 67 | 68 | Browsers that have native support for country flags will not load the font at all, and therefore will simply ignore it in the `font-family` list. 69 | 70 | ## API 71 | 72 | ```ts 73 | function polyfillCountryFlags(fontName?: string, fontUrl?: string): boolean; 74 | ``` 75 | 76 | Injects a web font with country flags if deemed necessary. 77 | 78 | Parameters: 79 | 80 | - `fontName` - (optional) Override the default font name ("Twemoji Country Flags") 81 | - `fontUrl` - (optional) Override the font URL (defaults to a jsdeliver-hosted) 82 | 83 | If the browser supports color emojis but not country flags, this function injects a `style` element into the HEAD with a web font with country flags, and returns `true`. Otherwise, it does nothing and returns `false`. 84 | 85 | ## Background 86 | 87 | Firefox on Windows adds country flag emoji support falling back on their [Twemoji Mozilla](https://github.com/mozilla/twemoji-colr) font, which itself is simply all [Twemoji](https://twemoji.twitter.com/) emojis concatenated into a single huge color font. 88 | 89 | Chromium, however, apparently [does not plan to support country flags on Windows](https://bugs.chromium.org/p/chromium/issues/detail?id=1209677#c5), except if Windows itself adds it. This means that Chromium-based browsers such as Chrome, Edge and Brave won't likely support it soon either. That's a huge chunk of browser users, who will complain that other people's nice flag emojis look to them like "ᴄʜ" and not like a picture. 90 | 91 | Until either Microsoft or Google recognize how ridiculous this is, you're stuck with this polyfill. 92 | 93 | ### How it works 94 | 95 | This polyfill merely combines other people's hard work. 96 | 97 | The key building block of this polyfill is a font, "Twemoji Country Flags", a subset of "Twemoji Mozilla" made using the excellent [`pyftsubset`](https://fonttools.readthedocs.io/en/latest/subset/index.html) tool from [fonttools](https://github.com/fonttools/fonttools). 98 | 99 | This is important, because Twemoji Mozilla is 1.5MB, which is a pretty huge hit on your app perfomance. The subset is only 78kb, which is much better. 100 | 101 | It then injects some CSS to load this font as a webfont, but only if the browser supports regular emojis and not country flags. 102 | 103 | As far as I can tell, all browsers that have this problem support WOFF2 fonts, so I made no effort to do the usual multi-font-format `@font-face` syntax (with eg ttf and woff fonts also). 104 | 105 | ### How to build 106 | 107 | This might need updates if [Twemoji Mozilla](https://github.com/mozilla/twemoji-colr) gets a new version - especially if new country flags are added. 108 | 109 | - clone the repo 110 | - make sure you're on a system with bash, fonttools and curl (On my WSL/Ubuntu, a single `apt install fonttools` was enough) 111 | - run `npm run make-font` 112 | - find the new font in `dist/TwemojiCountryFlags.woff2` 113 | --------------------------------------------------------------------------------