NextBSD · research · 2026-09-24

Choosing a language at boot

Whether the loader's two-second countdown should become a language list, what the loader can actually draw, and what Apple in fact does — which turns out not to be this.

Options research, not a commitment. Companion to nextbsd/nextbsd#505 (silent normal boot). Every Apple claim below was read out of released source or verified on a Darwin 27 machine; the two places I am inferring are marked.

Status
Not a plan. Initial options research only. Nothing here has been decided, agreed or committed to, and no ticket depends on it. The purpose is to lay out what is actually possible at each boot stage and what Apple does, so that a decision can be made later with the constraints visible. Where this page says “recommended” it means the option that currently looks strongest to me — read it as a starting point to argue with, not a conclusion.
Correction to the premise, stated up front. The idea we sketched was a grey splash with a language list replacing the loader countdown. Two findings undermine it, and I would rather lead with them than bury them: 29 of the 44 language names cannot be drawn by the loader at all (§3), and a list in the loader is console text, which is the one thing #505 exists to remove (§4). Apple's own answer avoids both (§2).

1 · The four places a language could be chosen

These are not interchangeable. Each has a different ceiling, and the ceiling is what decides this.

UEFI firmware
before the loader runs
Graphics
full GOP framebuffer, vendor's own font
Unicode
vendor-dependent; usually Latin only
Language state
PlatformLang / PlatformLangCodes EFI vars
NextBSD can draw here
No — it is the vendor's UI, not ours
Console text
none of ours
loader, Lua menu
loader_lua.efi, the 2 s countdown
Graphics
text cells only; color.lua has 8 colours (BLACK=0…WHITE=7)
Unicode
No — vt bitmap font, Latin only
Language state
can set kenv for the kernel
NextBSD can draw here
Yes, and it is where the countdown already is
Console text
a menu is console text — breaks #505
loader, gfx_fb
same binary, the framebuffer layer
Graphics
Yes — gfx_fb_putimage(png_t*…) via pnglite, gfx_fb_drawrect()
Unicode
only what the loaded vt_font covers — see below
Language state
same kenv
NextBSD can draw here
Yes — this is the real graphical option
Console text
a splash is not text, but a list still is
first boot, userland
after the kernel, before/at loginwindow
Graphics
anything — full framebuffer, TrueType, CoreFoundation
Unicode
complete
Language state
AppleLanguages, AppleLocale, NVRAM, CDIS.custom
NextBSD can draw here
Yes
Console text
none — it replaces the console

2 · What Apple actually does

Apple never asks for a language in the bootloader. It keeps a hint in firmware and asks once, graphically, in userland. The whole chain is observable, and most of it is reachable from open source:

  1. NVRAM holds the durable hint. prev-lang:kbd, verified on this Mac as en:0 — <language>:<keyboard-layout-id>. Language Chooser errors with prev-lang:kbd is not two parts. if it is malformed. A sibling skip-language-setup boolean suppresses the question. Open source: system_cmds/nvram/nvram.c:113 reaches it with IORegistryEntryFromPath(…, "IODeviceTree:/options") and IORegistryEntrySetCFProperty() — plain IOKit, which NextBSD has.
  2. Marker files decide whether to ask. All three verified present here: /var/db/.AppleSetupDone (ask only if absent), /var/db/.AppleSetupResume, and /var/db/.RunLanguageChooserToo — which is not a stamp but an XML plist, { kGuessedCountries = ["US"]; kVoiceOver = false; }.
  3. loginwindow makes the call, in -[Login1 setLanguagePrefIfNeeded]. Its log format string names every input it weighs: should_run_language_chooser enter, appleSetupDonePresent:%d, … runLanguageChooserTooPresent:%d, skipLanguageChooserNVRAMValue:%d, prevlangNVRAMSetup:%d.
  4. Language Chooser asks — graphically, in userland, full screen, with real endonyms. It is closed source, and it is not part of the boot loader; it runs after the kernel, as a userland app.
  5. It writes four keys plus two side channels. AppleLanguages, AppleLocale, AppleTextDirection, Country into .GlobalPreferences.plist — written to both /Library/Preferences and root's own copy, because for a root-context process root's copy wins the CFPreferences search order. Plus /var/log/CDIS.custom (LANGUAGE=en, still present on Darwin 27) and the NVRAM variable from step 1.
So the most Apple-like design is explicitly not a loader menu. It is: a firmware variable read silently at boot, and one graphical question in userland the first time the machine starts. If "most Apple-like" is the goal, that settles it — and it happens to be the option that keeps the console silent.

3 · Why the loader cannot show a language list properly

This is the hard constraint, and it is not a matter of effort.

The loader has eight colours

color.lua defines BLACK=0 through WHITE=7. The grey we designed the splash around does not exist there; the nearest is ANSI 7, #aaaaaa. That is survivable — inverted, it looks like this, which is honestly not bad:

NextBSD Select your language English > Deutsch Francais Espanol Portugues ????? ?????? ????? ??????? Up/Down to choose, Enter to continue

Note lines 6–10 have already lost their diacritics, and line 11 is what Japanese, Korean, Greek and Arabic become. That is the real problem, and it is the font, not the palette.

The loader's fonts have no glyphs for most of the list

FreeBSD ships exactly these vt fonts — gallant.hex, terminus-b32.hex, tom-thumb.hex, vgarom-8x8/8x14/8x16/16x32.hex and the two vgarom-thin variants. All are Latin-plus-box-drawing. None has a CJK, Arabic, Hebrew, Devanagari, Thai or Vietnamese glyph. Counting Apple's own list against that:

entries in Apple's list
44
renderable in a vt font
15
need glyphs we don't have
29
no POSIX locale installed
4

This is exactly why languagesetup(1) has an -English mode that prints ISO 639 codes instead of names — Apple hit the same wall in a text context and solved it by giving up on endonyms. A loader list would have to do the same, and a language picker that cannot show you your own language's name is not doing its job.

But the loader can do graphics — that part is real

Worth being precise, because "beyond Lua" does have an answer. stand/common/gfx_fb.c is a genuine framebuffer layer, already in the tree:

So a grey splash with the cube as a PNG is straightforwardly possible today. Text in 29 of the 44 languages is not, unless someone builds and ships a vt font with CJK coverage — a bitmap CJK font is on the order of megabytes in the loader's own format, loaded before the kernel, for one screen seen once.

4 · This collides with #505

#505 is "nothing on the console between the loader and the login prompt", and you were emphatic: not one line of text unless single-user or verbose. A language menu is not a splash — it is an interactive text screen, it requires io.getchar(), and it necessarily waits for a human. Adding it would make the boot louder and slower in exchange for a question asked once per machine lifetime.

The loader can still participate silently, and should: PlatformLang is already parsed in stand/efi/libefi/env.c:597, inside efi_print_global() — today it is display-only, reachable from efi-show. Promoting it from "printable" to "handed to the kernel as a kenv hint" is a small, silent change.

5 · The options, side by side

A · Lua list in the loader
what we sketched
Cheapest to build; menu.lua already has the machinery. But 8 colours, no endonyms for 29 of 44, and it breaks #505. Not recommended.
B · gfx_fb splash + list
PNG via pnglite
The graphical option that exists in-tree. Handsome splash, real cube. Still cannot draw the names without a custom multi-megabyte vt font, and still an interactive screen before the kernel. Right technology, wrong stage.
C · GRUB-style Unicode
for reference only
GRUB2 genuinely solves the glyph problem — unicode.pf2 plus gfxterm renders CJK in a boot menu. It is also GPL, a whole second bootloader, and nothing like Apple. Rejected, but it is the honest answer to "has anyone done this graphically".
D · rEFInd
for reference only
Proves EFI-stage graphics with PNG icons and themes are practical. It is a boot manager, though — it picks an OS, not a language. Not applicable.
E · EFI hint + first-boot chooser
Apple's shape
Loader reads PlatformLang silently and passes a kenv hint; a durable choice lives in an EFI variable (our prev-lang:kbd equivalent); the question is asked once in userland, full screen, with real endonyms, gated by a .AppleSetupDone-shaped marker. Strongest of the five.
Currently strongest — E. Offered as a starting position, not a decision. It is the only option that is simultaneously the most Apple-like, compatible with #505, and able to show a Japanese speaker the word 日本語. The loader's role shrinks to one silent variable read, which is the opposite of the countdown-replacement we started from, and better for it.

6 · If E were taken, roughly what it involves

  1. Loader (silent). Read PlatformLang where env.c:597 already parses it, and set a kenv hint. No UI, no countdown change, no console output.
  2. An EFI variable as the durable hint, in Apple's <lang>:<kbd> shape. Reachable from userland; survives reinstall, which a file in /var/db does not.
  3. A marker file to gate the question — the .AppleSetupDone pattern, so it is asked once.
  4. The chooser itself, in userland. Full screen, endonyms, arrow keys. This is where the grey-and-spinner splash design belongs, and where it can actually be honoured.
  5. Writing the choice. Four keys — AppleLanguages, AppleLocale, AppleTextDirection, Country — to both /Library/Preferences and root's copy, via CFPreferencesSetValue + CFPreferencesSynchronize on the exact domain triple (AnyApplication, AnyUser, CurrentHost; note it is CurrentHost that means /Library/Preferences). Plus /var/log/CDIS.custom, which Darwin still ships.
  6. The LANG question, deliberately. See §7 — this is a decision, not a detail.

7 · One thing to decide with open eyes

Darwin does not keep AppleLanguages and LANG in sync, and I want that on the record before we build something that does. Verified on this machine: with AppleLocale = en_US and LANG unset, setlocale(LC_ALL,"") returns C and the codeset is US-ASCII.

Modern macOS's only system-wide LANG is a hardcoded fallback in the open-source zsh/zprofile: if [ -z "$LANG" ]; then export LANG=C.UTF-8; fi. It never consults AppleLanguages.

So: making the chooser also set LANG is a deliberate divergence from Darwin. I think it is the right one — a BSD userland where ls cannot print a filename in your language is worse than being faithful — but it should be written down as a choice, not slipped in as an implementation detail.

8 · Apple's 44 entries, against what we can actually render and use

Locale column checked against the 293 locales (83 UTF-8) installed on this Darwin 27 machine; NextBSD ships FreeBSD's set, so re-check before relying on it. "needs glyphs" means no vt font we ship has the characters.

tagname, as the user reads itEnglishPOSIX localeloader
enEnglishEnglishen_AU.UTF-8ASCII
en-GBEnglish (UK)Englishen_GB.UTF-8ASCII
zh-Hans简体中文Chinese, Simplifiedzh_CN.UTF-8needs glyphs
zh-Hant繁體中文Chinese, Traditionalzh_CN.UTF-8needs glyphs
zh-HK繁體中文(香港)Chinese, Hong Kongzh_HK.UTF-8needs glyphs
frFrançaisFrenchfr_BE.UTF-8needs glyphs
fr-CAFrançais canadienFrench, Canadianfr_CA.UTF-8needs glyphs
deDeutschGermande_AT.UTF-8ASCII
ja日本語Japaneseja_JP.UTF-8needs glyphs
esEspañolSpanishes_AR.UTF-8needs glyphs
es-419Español (Latinoamérica)Spanish, Latin Americanes_AR.UTF-8needs glyphs
itItalianoItalianit_CH.UTF-8ASCII
ko한국어Koreanko_KR.UTF-8needs glyphs
pt-BRPortuguês (Brasil)Portuguese, Brazilianpt_BR.UTF-8needs glyphs
pt-PTPortuguês (Portugal)Portuguesept_PT.UTF-8needs glyphs
ruРусскийRussianru_RU.UTF-8needs glyphs
nlNederlandsDutchnl_BE.UTF-8ASCII
svSvenskaSwedishsv_FI.UTF-8ASCII
nbNorsk bokmålNorwegiannb_NO.UTF-8needs glyphs
daDanskDanishda_DK.UTF-8ASCII
fiSuomiFinnishfi_FI.UTF-8ASCII
plPolskiPolishpl_PL.UTF-8ASCII
trTürkçeTurkishtr_TR.UTF-8needs glyphs
ukУкраїнськаUkrainianuk_UA.UTF-8needs glyphs
csČeštinaCzechcs_CZ.UTF-8needs glyphs
skSlovenčinaSlovaksk_SK.UTF-8needs glyphs
huMagyarHungarianhu_HU.UTF-8ASCII
caCatalàCatalanca_AD.UTF-8needs glyphs
hrHrvatskiCroatianhr_HR.UTF-8ASCII
elΕλληνικάGreekel_GR.UTF-8needs glyphs
heעבריתHebrewhe_IL.UTF-8needs glyphs
roRomânăRomanianro_RO.UTF-8needs glyphs
thไทยThai—needs glyphs
viTiếng ViệtVietnamese—needs glyphs
idIndonesiaIndonesian—ASCII
msMelayuMalay—ASCII
arالعربيةArabicar_AE.UTF-8needs glyphs
hiहिन्दीHindihi_IN.UTF-8needs glyphs
slSlovenščinaSloveniansl_SI.UTF-8needs glyphs
ru-RUРусскийRussianru_RU.UTF-8needs glyphs
en-AUEnglish (Australia)Englishen_AU.UTF-8ASCII
en-INEnglish (India)Englishen_IN.UTF-8ASCII
es-MXEspañol (México)Spanishes_MX.UTF-8needs glyphs
de-ATDeutsch (Österreich)Germande_AT.UTF-8needs glyphs

The four with no locale at all — Indonesian, Malay, Thai, Vietnamese — would be offered a UI language with no working LC_CTYPE to match, which is its own small decision. es-419 (Latin American Spanish) has no POSIX equivalent by construction; it is a region-less region.

9 · Still open, and what would need deciding