Flags are emoji, and Windows draws them as two letters #88

Closed
opened 2026-09-07 14:19:12 +00:00 by tiagoagueda · 0 comments
Owner

Observation

currently flags (either in phone number and languages) are rendered as emojis.
unfortunately Microsoft doesn't do emoji flags so, flags must be rendered in svg, find an
alternative

What is wrong

A flag emoji is not a character. 🇵🇹 is two regional indicator code points, P and T,
and a font is invited — never required — to draw the pair as a flag. Segoe UI Emoji has
never accepted the invitation and Microsoft has said it does not intend to. Windows
therefore draws exactly what the code points say: PT.

Both places this was used carry a comment admitting it:

  • core/languages.py — "Windows shows the two letters instead — FR rather than a
    French flag — which is a legible fallback and not a broken image, and is worth knowing
    before it is reported as a bug."
  • core/phones.py — "The same Windows caveat as the language picker applies, and the same
    answer: two letters is a legible fallback."

It has now been reported as a bug, by the maintainer, on the platform he develops on. The
prediction was right and the judgement behind it was wrong: two letters where everyone else
sees a flag does not read as a fallback, it reads as something broken. Postulo runs on the
machine somebody already has, and a great many of those machines are Windows.

Where it shows

Where Flags Control
Settings → Language and time 24 now, 53 once #70's African languages land radio rows
Any telephone field 1 shown, 241 selectable native <select>

The constraint that shapes the fix

An <option> element can contain text and nothing else. No <img>, no <svg>, in any
browser; it is not a styling problem to work around. So the two places need two answers.

The language picker is already rows rather than a dropdown — it was rebuilt that way in
#64, because an <option> cannot carry lang on its content either. A row can hold an
image, so it simply gets one.

The phone picker stays a native <select>. It is the best-supported control there is:
the system picker on a phone, a familiar target for every screen reader, and typing a letter
jumps to a country. Replacing it with a custom listbox to make room for an image would trade
something that works everywhere for something that has to be re-implemented and re-tested
against every assistive technology. Instead the flag moves beside the closed select, where
it is arguably more use than it was inside the list: visible without opening anything.

Each <option> carries its flag's URL in a data-flag attribute — an attribute is not
content, so this is allowed — and the change handler swaps the image. With scripts off the
server has already rendered the flag for the country the field loaded with, which is correct;
it just stops following the select until the form is saved. That is the project's usual
shape: the control works without JavaScript, and JavaScript makes it better.

The artwork

flag-icons 7.5.0, MIT, vendored the way Lucide and
zxcvbn already are: npm run sync:flags copies the listed files out of node_modules into
static/flags/, the result is committed, and Node is never needed to run Postulo. The
content security policy already says img-src 'self', so nothing has to be loosened.

Rectangular 4:3, the flag as officially drawn. The maintainer was offered
circle-flags — also MIT, ten times smaller —
and turned it down: "i don't like circle flags". Recorded here so that the size below is
understood as a decision rather than an oversight.

What it costs, measured rather than guessed

241 flags committed 1.5 MB
Language page, 24 flags 139 KB raw, 42 KB gzipped
Heaviest single flag 177 KB (Serbia's coat of arms), 20 px on screen
Telephone field one flag, ~6 KB

Static files are served by WhiteNoise from CompressedManifestStaticFilesStorage, so they go
out pre-compressed and under a content-hashed name that can be cached for ever. svgo was
tried on the worst offenders and recovered 2% — the set is already optimised and the weight
is real detail, not slack.

Also fixed on the way

languages.FLAGS held the emoji and phones.FROM_LANGUAGE held the ISO code, for the same
24 languages, and the two were verified to agree exactly — one fact written down twice, in
the codebase whose own phone field refuses to store a country column for precisely that
reason. FLAGS becomes FLAG_COUNTRIES and holds ISO codes; a test asserts the two maps
never contradict each other where both have an opinion.

Classification

Bug, interface, internationalisation. Cosmetic in consequence and unmissable in effect: it
is the first screen a new Windows user opens.

## Observation > currently flags (either in phone number and languages) are rendered as emojis. > unfortunately Microsoft doesn't do emoji flags so, flags must be rendered in svg, find an > alternative ## What is wrong A flag emoji is not a character. `🇵🇹` is two *regional indicator* code points, P and T, and a font is invited — never required — to draw the pair as a flag. Segoe UI Emoji has never accepted the invitation and Microsoft has said it does not intend to. Windows therefore draws exactly what the code points say: `PT`. Both places this was used carry a comment admitting it: - `core/languages.py` — *"**Windows shows the two letters instead** — `FR` rather than a French flag — which is a legible fallback and not a broken image, and is worth knowing before it is reported as a bug."* - `core/phones.py` — *"The same Windows caveat as the language picker applies, and the same answer: two letters is a legible fallback."* It has now been reported as a bug, by the maintainer, on the platform he develops on. The prediction was right and the judgement behind it was wrong: two letters where everyone else sees a flag does not read as a fallback, it reads as something broken. Postulo runs on the machine somebody already has, and a great many of those machines are Windows. ## Where it shows | Where | Flags | Control | | --- | --- | --- | | Settings → Language and time | 24 now, 53 once #70's African languages land | radio rows | | Any telephone field | 1 shown, 241 selectable | native `<select>` | ## The constraint that shapes the fix **An `<option>` element can contain text and nothing else.** No `<img>`, no `<svg>`, in any browser; it is not a styling problem to work around. So the two places need two answers. **The language picker** is already rows rather than a dropdown — it was rebuilt that way in #64, because an `<option>` cannot carry `lang` on its content either. A row can hold an image, so it simply gets one. **The phone picker** stays a native `<select>`. It is the best-supported control there is: the system picker on a phone, a familiar target for every screen reader, and typing a letter jumps to a country. Replacing it with a custom listbox to make room for an image would trade something that works everywhere for something that has to be re-implemented and re-tested against every assistive technology. Instead the flag moves *beside* the closed select, where it is arguably more use than it was inside the list: visible without opening anything. Each `<option>` carries its flag's URL in a `data-flag` attribute — an attribute is not content, so this is allowed — and the change handler swaps the image. With scripts off the server has already rendered the flag for the country the field loaded with, which is correct; it just stops following the select until the form is saved. That is the project's usual shape: the control works without JavaScript, and JavaScript makes it better. ## The artwork [flag-icons](https://github.com/lipis/flag-icons) 7.5.0, MIT, vendored the way Lucide and zxcvbn already are: `npm run sync:flags` copies the listed files out of `node_modules` into `static/flags/`, the result is committed, and Node is never needed to *run* Postulo. The content security policy already says `img-src 'self'`, so nothing has to be loosened. Rectangular 4:3, the flag as officially drawn. The maintainer was offered [circle-flags](https://github.com/HatScripts/circle-flags) — also MIT, ten times smaller — and turned it down: *"i don't like circle flags"*. Recorded here so that the size below is understood as a decision rather than an oversight. ## What it costs, measured rather than guessed | | | | --- | --- | | 241 flags committed | 1.5 MB | | Language page, 24 flags | 139 KB raw, **42 KB gzipped** | | Heaviest single flag | 177 KB (Serbia's coat of arms), 20 px on screen | | Telephone field | one flag, ~6 KB | Static files are served by WhiteNoise from `CompressedManifestStaticFilesStorage`, so they go out pre-compressed and under a content-hashed name that can be cached for ever. svgo was tried on the worst offenders and recovered 2% — the set is already optimised and the weight is real detail, not slack. ## Also fixed on the way `languages.FLAGS` held the emoji and `phones.FROM_LANGUAGE` held the ISO code, for the same 24 languages, and the two were verified to agree exactly — one fact written down twice, in the codebase whose own phone field refuses to store a country column for precisely that reason. `FLAGS` becomes `FLAG_COUNTRIES` and holds ISO codes; a test asserts the two maps never contradict each other where both have an opinion. ## Classification Bug, interface, internationalisation. Cosmetic in consequence and unmissable in effect: it is the first screen a new Windows user opens.
tiagoagueda added this to the 0.3.0 milestone 2026-09-07 14:19:12 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#88
No description provided.