Repository navigation
Expand file tree
/
Copy pathnextbsd-boot-language-research.html
More file actions
375 lines (344 loc) · 33 KB
/
Copy pathnextbsd-boot-language-research.html
File metadata and controls
375 lines (344 loc) · 33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
<!doctype html>
<meta charset="utf-8">
<title>Language at boot — options research</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=JetBrains+Mono:wght@400;700&family=Noto+Sans+JP:wght@400&display=swap">
<style>
:root {
--bg:#0a0b0d; --panel:#101216; --panel2:#15181d; --line:#2c3138;
--fg:#eef1f5; --fg2:#c3cad2; --dim:#8b949e; --dim2:#5b636c;
--cyan:#5ff; --green:#5f5; --red:#f66; --amber:#fb4;
}
* { box-sizing:border-box }
body { margin:0; background:var(--bg); color:var(--fg2);
font-family:'JetBrains Mono',ui-monospace,monospace; font-size:13px; line-height:1.65;
-webkit-font-smoothing:antialiased }
.wrap { max-width:1180px; margin:0 auto; padding:54px 32px 120px }
h1 { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:40px; line-height:1.1;
letter-spacing:-.02em; color:var(--fg); margin:0 0 10px }
h2 { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:23px; color:var(--fg);
margin:52px 0 6px; letter-spacing:-.01em }
h3 { font-family:'Space Grotesk',sans-serif; font-weight:500; font-size:15px; color:var(--fg);
margin:26px 0 8px }
.lede { font-size:14px; color:var(--dim); margin:0 0 4px }
.sub { color:var(--dim); font-size:12px; margin:0 0 30px }
p { margin:10px 0 }
code { color:var(--fg2); background:#191d23; padding:1px 5px; border-radius:3px; font-size:.92em }
a { color:var(--cyan) }
b.yes,.yes { color:var(--green) } b.no,.no { color:var(--red) }
.rule { height:1px; background:var(--line); margin:40px 0 }
.kicker { font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--dim2); margin:0 0 6px }
.panel { background:var(--panel); border:1px solid var(--line); border-radius:9px; padding:20px 24px; margin:18px 0 }
.verdict { background:var(--panel2); border:1px solid #39404a; border-left:3px solid var(--cyan); border-radius:9px; padding:20px 24px; margin:22px 0 }
.status { background:#101a16; border:1px solid #2b4a3a; border-left:3px solid var(--green);
border-radius:9px; padding:18px 22px; margin:22px 0 }
.statusk { font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--green); margin-bottom:6px }
.warn { background:#1a1512; border:1px solid #4a3a22; border-left:3px solid var(--amber); border-radius:9px; padding:18px 22px; margin:22px 0 }
table { border-collapse:collapse; width:100%; margin:14px 0; font-size:12px }
th { text-align:left; font-weight:700; color:var(--dim); border-bottom:1px solid var(--line);
padding:7px 10px; font-size:11px; letter-spacing:.08em; text-transform:uppercase }
td { padding:6px 10px; border-bottom:1px solid #1b1f25; vertical-align:top }
tr:hover td { background:#12151a }
.mono { font-variant-ligatures:none }
.tag { color:var(--cyan) }
.endo { font-family:'Noto Sans JP','JetBrains Mono',sans-serif; color:var(--fg); font-size:13px }
.eng { color:var(--dim) }
.gone { color:var(--red) }
.mk { white-space:nowrap; font-size:11px }
.stages { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:18px 0 }
.stage { background:var(--panel); border:1px solid var(--line); border-radius:9px; padding:0 0 12px; overflow:hidden }
.sh { background:#181c22; padding:13px 15px; border-bottom:1px solid var(--line) }
.sn { color:var(--fg); font-weight:700; font-size:12.5px }
.ss { color:var(--dim2); font-size:10.5px; margin-top:2px }
.cap { padding:9px 15px; border-bottom:1px solid #1b1f25 }
.cap:last-child { border-bottom:0 }
.capk { color:var(--dim2); font-size:10px; letter-spacing:.09em; text-transform:uppercase; margin-bottom:2px }
.capv { color:var(--fg2); font-size:11.5px; line-height:1.5 }
.term { background:#000; border:1px solid #23272e; border-radius:7px; padding:20px 24px;
font-size:13px; line-height:1.5; white-space:pre; overflow-x:auto; margin:14px 0 }
.grey { background:#aaa; color:#000 }
.opt { display:grid; grid-template-columns:150px 1fr; gap:0 18px; padding:14px 0; border-bottom:1px solid #1b1f25 }
.opt:last-child { border-bottom:0 }
.optn { color:var(--fg); font-weight:700; font-size:12.5px }
.tick { color:var(--dim2); font-size:11px; margin-top:3px }
ul { margin:8px 0; padding-left:20px } li { margin:5px 0 }
.chain { counter-reset:c; margin:14px 0; padding:0; list-style:none }
.chain li { counter-increment:c; position:relative; padding:9px 0 9px 42px; border-bottom:1px solid #1b1f25 }
.chain li:last-child { border-bottom:0 }
.chain li::before { content:counter(c); position:absolute; left:0; top:9px; width:24px; height:24px;
border:1px solid #39404a; border-radius:50%; color:var(--cyan); font-size:11px;
display:flex; align-items:center; justify-content:center }
@media (max-width:900px) { .stages { grid-template-columns:1fr 1fr } .opt { grid-template-columns:1fr } }
</style>
<div class="wrap">
<p class="kicker">NextBSD · research · 2026-09-24</p>
<h1>Choosing a language at boot</h1>
<p class="lede">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.</p>
<p class="sub">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.</p>
<div class="status">
<div class="statusk">Status</div>
<b>Not a plan. Initial options research only.</b> 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 <i>the option that currently looks strongest to me</i> — read it as a
starting point to argue with, not a conclusion.
</div>
<div class="warn">
<b>Correction to the premise, stated up front.</b> 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:
<b>29 of the 44 language names cannot be drawn by the loader at all</b> (§3), and <b>a list in the loader is
console text, which is the one thing #505 exists to remove</b> (§4). Apple's own answer avoids both (§2).
</div>
<h2>1 · The four places a language could be chosen</h2>
<p>These are not interchangeable. Each has a different ceiling, and the ceiling is what decides this.</p>
<div class="stages"><div class="stage"><div class="sh"><div class="sn">UEFI firmware</div><div class="ss">before the loader runs</div></div><div class="cap"><div class="capk">Graphics</div><div class="capv">full GOP framebuffer, vendor's own font</div></div>
<div class="cap"><div class="capk">Unicode</div><div class="capv">vendor-dependent; usually Latin only</div></div>
<div class="cap"><div class="capk">Language state</div><div class="capv"><code>PlatformLang</code> / <code>PlatformLangCodes</code> EFI vars</div></div>
<div class="cap"><div class="capk">NextBSD can draw here</div><div class="capv"><b class='no'>No</b> — it is the vendor's UI, not ours</div></div>
<div class="cap"><div class="capk">Console text</div><div class="capv">none of ours</div></div></div>
<div class="stage"><div class="sh"><div class="sn">loader, Lua menu</div><div class="ss"><code>loader_lua.efi</code>, the 2 s countdown</div></div><div class="cap"><div class="capk">Graphics</div><div class="capv">text cells only; <code>color.lua</code> has <b>8</b> colours (BLACK=0…WHITE=7)</div></div>
<div class="cap"><div class="capk">Unicode</div><div class="capv"><b class='no'>No</b> — vt bitmap font, Latin only</div></div>
<div class="cap"><div class="capk">Language state</div><div class="capv">can set <code>kenv</code> for the kernel</div></div>
<div class="cap"><div class="capk">NextBSD can draw here</div><div class="capv">Yes, and it is where the countdown already is</div></div>
<div class="cap"><div class="capk">Console text</div><div class="capv"><b class='no'>a menu is console text</b> — breaks #505</div></div></div>
<div class="stage"><div class="sh"><div class="sn">loader, gfx_fb</div><div class="ss">same binary, the framebuffer layer</div></div><div class="cap"><div class="capk">Graphics</div><div class="capv"><b class='yes'>Yes</b> — <code>gfx_fb_putimage(png_t*…)</code> via pnglite, <code>gfx_fb_drawrect()</code></div></div>
<div class="cap"><div class="capk">Unicode</div><div class="capv">only what the loaded <code>vt_font</code> covers — see below</div></div>
<div class="cap"><div class="capk">Language state</div><div class="capv">same <code>kenv</code></div></div>
<div class="cap"><div class="capk">NextBSD can draw here</div><div class="capv">Yes — this is the real graphical option</div></div>
<div class="cap"><div class="capk">Console text</div><div class="capv">a splash is not text, but a <i>list</i> still is</div></div></div>
<div class="stage"><div class="sh"><div class="sn">first boot, userland</div><div class="ss">after the kernel, before/at loginwindow</div></div><div class="cap"><div class="capk">Graphics</div><div class="capv">anything — full framebuffer, TrueType, CoreFoundation</div></div>
<div class="cap"><div class="capk">Unicode</div><div class="capv"><b class='yes'>complete</b></div></div>
<div class="cap"><div class="capk">Language state</div><div class="capv"><code>AppleLanguages</code>, <code>AppleLocale</code>, NVRAM, <code>CDIS.custom</code></div></div>
<div class="cap"><div class="capk">NextBSD can draw here</div><div class="capv">Yes</div></div>
<div class="cap"><div class="capk">Console text</div><div class="capv">none — it replaces the console</div></div></div></div>
<h2>2 · What Apple actually does</h2>
<p><b>Apple never asks for a language in the bootloader.</b> 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:</p>
<ol class="chain">
<li><b>NVRAM holds the durable hint.</b> <code>prev-lang:kbd</code>, verified on this Mac as
<code>en:0</code> — <i><language>:<keyboard-layout-id></i>. Language Chooser errors with
<code>prev-lang:kbd is not two parts.</code> if it is malformed. A sibling <code>skip-language-setup</code>
boolean suppresses the question. <b class="yes">Open source:</b> <code>system_cmds/nvram/nvram.c:113</code> reaches it with
<code>IORegistryEntryFromPath(…, "IODeviceTree:/options")</code> and <code>IORegistryEntrySetCFProperty()</code>
— plain IOKit, which NextBSD has.</li>
<li><b>Marker files decide whether to ask.</b> All three verified present here:
<code>/var/db/.AppleSetupDone</code> (ask only if absent), <code>/var/db/.AppleSetupResume</code>, and
<code>/var/db/.RunLanguageChooserToo</code> — which is not a stamp but an XML plist,
<code>{ kGuessedCountries = ["US"]; kVoiceOver = false; }</code>.</li>
<li><b><code>loginwindow</code> makes the call</b>, in
<code>-[Login1 setLanguagePrefIfNeeded]</code>. Its log format string names every input it weighs:
<code>should_run_language_chooser enter, appleSetupDonePresent:%d, … runLanguageChooserTooPresent:%d,
skipLanguageChooserNVRAMValue:%d, prevlangNVRAMSetup:%d</code>.</li>
<li><b>Language Chooser asks — graphically, in userland</b>, full screen, with real endonyms. It is closed source,
and it is <i>not</i> part of the boot loader; it runs after the kernel, as a userland app.</li>
<li><b>It writes four keys plus two side channels.</b> <code>AppleLanguages</code>, <code>AppleLocale</code>,
<code>AppleTextDirection</code>, <code>Country</code> into <code>.GlobalPreferences.plist</code> —
written to <i>both</i> <code>/Library/Preferences</code> and root's own copy, because for a root-context process
root's copy wins the CFPreferences search order. Plus <code>/var/log/CDIS.custom</code>
(<code>LANGUAGE=en</code>, still present on Darwin 27) and the NVRAM variable from step 1.</li>
</ol>
<div class="verdict">
<b>So the most Apple-like design is explicitly not a loader menu.</b> 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.
</div>
<h2>3 · Why the loader cannot show a language list properly</h2>
<p>This is the hard constraint, and it is not a matter of effort.</p>
<h3>The loader has eight colours</h3>
<p><code>color.lua</code> defines <code>BLACK=0</code> through <code>WHITE=7</code>. The grey we designed the splash
around does not exist there; the nearest is ANSI 7, <code>#aaaaaa</code>. That is survivable — inverted, it
looks like this, which is honestly not bad:</p>
<div class="term"><span class="grey"> </span>
<span class="grey"> NextBSD </span>
<span class="grey"> </span>
<span class="grey"> Select your language </span>
<span class="grey"> </span>
<span class="grey"> English </span>
<span class="grey"> > Deutsch </span>
<span class="grey"> Francais </span>
<span class="grey"> Espanol </span>
<span class="grey"> Portugues </span>
<span class="grey"> ????? ?????? ????? ??????? </span>
<span class="grey"> </span>
<span class="grey"> Up/Down to choose, Enter to continue </span>
<span class="grey"> </span></div>
<p>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.</p>
<h3>The loader's fonts have no glyphs for most of the list</h3>
<p>FreeBSD ships exactly these vt fonts — <code>gallant.hex</code>, <code>terminus-b32.hex</code>,
<code>tom-thumb.hex</code>, <code>vgarom-8x8/8x14/8x16/16x32.hex</code> and the two <code>vgarom-thin</code>
variants. All are Latin-plus-box-drawing. <b>None has a CJK, Arabic, Hebrew, Devanagari, Thai or Vietnamese
glyph.</b> Counting Apple's own list against that:</p>
<div class="panel" style="display:flex;gap:40px;flex-wrap:wrap">
<div><div class="capk">entries in Apple's list</div><div style="font-size:26px;color:var(--fg)">44</div></div>
<div><div class="capk">renderable in a vt font</div><div style="font-size:26px;color:var(--green)">15</div></div>
<div><div class="capk">need glyphs we don't have</div><div style="font-size:26px;color:var(--red)">29</div></div>
<div><div class="capk">no POSIX locale installed</div><div style="font-size:26px;color:var(--amber)">4</div></div>
</div>
<p>This is exactly why <code>languagesetup(1)</code> has an <code>-English</code> 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 <i>a language picker that cannot show you your own language's name is not doing its
job.</i></p>
<h3>But the loader can do graphics — that part is real</h3>
<p>Worth being precise, because "beyond Lua" does have an answer. <code>stand/common/gfx_fb.c</code> is a genuine
framebuffer layer, already in the tree:</p>
<ul>
<li><code>gfx_fb_putimage(png_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)</code> — <b class="yes">it decodes
and blits PNG</b>, via the vendored pnglite. This is how the boot logo is drawn.</li>
<li><code>gfx_fb_drawrect()</code>, plus a full <code>teken</code> terminal on top.</li>
<li><code>struct vt_font tg_font</code>, <code>setup_font()</code>, <code>font_lookup()</code> — it <b>loads vt
bitmap fonts at run time</b>, and <code>font_list_t fonts</code> means more than one can be present.</li>
</ul>
<p>So a grey splash with the cube as a PNG is straightforwardly possible today. <b>Text in 29 of the 44 languages is
not</b>, 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.</p>
<h2>4 · This collides with #505</h2>
<p>#505 is "nothing on the console between the loader and the login prompt", and you were emphatic: <i>not one line
of text</i> unless single-user or verbose. A language menu is not a splash — it is an interactive text screen, it
requires <code>io.getchar()</code>, 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.</p>
<p>The loader <i>can</i> still participate silently, and should: <code>PlatformLang</code> is already parsed in
<code>stand/efi/libefi/env.c:597</code>, inside <code>efi_print_global()</code> — today it is display-only, reachable
from <code>efi-show</code>. Promoting it from "printable" to "handed to the kernel as a <code>kenv</code> hint" is a
small, silent change.</p>
<h2>5 · The options, side by side</h2>
<div class="panel">
<div class="opt"><div><div class="optn">A · Lua list in the loader</div><div class="tick">what we sketched</div></div>
<div>Cheapest to build; <code>menu.lua</code> already has the machinery. But 8 colours, no endonyms for 29 of 44,
and it breaks #505. <b class="no">Not recommended.</b></div></div>
<div class="opt"><div><div class="optn">B · gfx_fb splash + list</div><div class="tick">PNG via pnglite</div></div>
<div>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. <b class="no">Right technology,
wrong stage.</b></div></div>
<div class="opt"><div><div class="optn">C · GRUB-style Unicode</div><div class="tick">for reference only</div></div>
<div>GRUB2 genuinely solves the glyph problem — <code>unicode.pf2</code> plus <code>gfxterm</code> renders CJK in a
boot menu. It is also GPL, a whole second bootloader, and nothing like Apple. <b class="no">Rejected</b>, but it is
the honest answer to "has anyone done this graphically".</div></div>
<div class="opt"><div><div class="optn">D · rEFInd</div><div class="tick">for reference only</div></div>
<div>Proves EFI-stage graphics with PNG icons and themes are practical. It is a boot <i>manager</i>, though — it
picks an OS, not a language. <b class="no">Not applicable.</b></div></div>
<div class="opt"><div><div class="optn">E · EFI hint + first-boot chooser</div><div class="tick">Apple's shape</div></div>
<div>Loader reads <code>PlatformLang</code> silently and passes a <code>kenv</code> hint; a durable choice lives in an
EFI variable (our <code>prev-lang:kbd</code> equivalent); the question is asked once in userland, full screen, with
real endonyms, gated by a <code>.AppleSetupDone</code>-shaped marker. <b class="yes">Strongest of the five.</b></div></div>
</div>
<div class="verdict">
<b>Currently strongest — E.</b> 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.
</div>
<h2>6 · If E were taken, roughly what it involves</h2>
<ol class="chain">
<li><b>Loader (silent).</b> Read <code>PlatformLang</code> where <code>env.c:597</code> already parses it, and set a
<code>kenv</code> hint. No UI, no countdown change, no console output.</li>
<li><b>An EFI variable as the durable hint</b>, in Apple's <code><lang>:<kbd></code> shape. Reachable from
userland; survives reinstall, which a file in <code>/var/db</code> does not.</li>
<li><b>A marker file</b> to gate the question — the <code>.AppleSetupDone</code> pattern, so it is asked once.</li>
<li><b>The chooser itself, in userland.</b> Full screen, endonyms, arrow keys. This is where the grey-and-spinner
splash design belongs, and where it can actually be honoured.</li>
<li><b>Writing the choice.</b> Four keys — <code>AppleLanguages</code>, <code>AppleLocale</code>,
<code>AppleTextDirection</code>, <code>Country</code> — to both <code>/Library/Preferences</code> and root's copy,
via <code>CFPreferencesSetValue</code> + <code>CFPreferencesSynchronize</code> on the exact domain triple
(<code>AnyApplication</code>, <code>AnyUser</code>, <code>CurrentHost</code>; note it is <i>CurrentHost</i> that means
<code>/Library/Preferences</code>). Plus <code>/var/log/CDIS.custom</code>, which Darwin still ships.</li>
<li><b>The <code>LANG</code> question, deliberately.</b> See §7 — this is a decision, not a detail.</li>
</ol>
<h2>7 · One thing to decide with open eyes</h2>
<p>Darwin does <b>not</b> keep <code>AppleLanguages</code> and <code>LANG</code> in sync, and I want that on the record
before we build something that does. Verified on this machine: with <code>AppleLocale = en_US</code> and
<code>LANG</code> unset, <code>setlocale(LC_ALL,"")</code> returns <code>C</code> and the codeset is US-ASCII.</p>
<ul>
<li><code>login(1)</code> sets only <code>TERM</code>, <code>SHELL</code>, <code>HOME</code>, <code>LOGNAME</code>,
<code>USER</code>, <code>PATH</code> — no locale.</li>
<li><b>Darwin has no <code>login.conf</code> at all.</b> In <code>shell_cmds</code>, <code>su.c:90</code> and
<code>env.c:36</code> wrap <code><login_cap.h></code> in <code>#ifndef __APPLE__</code>, and Libc ships no
<code>login_cap</code>. FreeBSD's <code>lang</code>/<code>charset</code> capabilities have no Darwin counterpart —
so the obvious BSD mechanism is not available to copy.</li>
<li>launchd, configd, PAM and OpenDirectory contain <b>zero</b> references to these keys.</li>
<li>The only bridge Apple ever shipped system-wide was historical: <code>/etc/rc</code> sourcing
<code>/var/log/CDIS.custom</code> and exporting <code>LANGUAGE</code>, for localized startup messages.</li>
<li>The only <i>open-source</i> conversion is CUPS — <code>_cupsAppleLocale()</code> in
<code>cups/cups/language.c:248</code>, Apache-2 and vendorable, including the hand-maintained exception table
(<code>{"en","en_US"}</code>, <code>{"nb","no"}</code>, <code>{"zh-Hans","zh_CN"}</code>…) that CF will not give you.
Tellingly <code>cupsd</code> exports <i>both</i> <code>LANG</code> and a separate <code>APPLE_LANGUAGE</code> rather
than choosing — Apple concluded <code>LANG</code> could not carry an Apple language id faithfully.</li>
</ul>
<p>Modern macOS's only system-wide <code>LANG</code> is a hardcoded fallback in the open-source
<code>zsh/zprofile</code>: <code>if [ -z "$LANG" ]; then export LANG=C.UTF-8; fi</code>. It never consults
<code>AppleLanguages</code>.</p>
<div class="panel"><b>So:</b> making the chooser also set <code>LANG</code> is a <b>deliberate divergence from
Darwin</b>. I think it is the right one — a BSD userland where <code>ls</code> 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.</div>
<h2>8 · Apple's 44 entries, against what we can actually render and use</h2>
<p><i>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.</i></p>
<table>
<thead><tr><th>tag</th><th>name, as the user reads it</th><th>English</th><th>POSIX locale</th><th>loader</th></tr></thead>
<tbody>
<tr><td class="mono tag">en</td><td class="endo">English</td><td class="eng">English</td><td class="mono">en_AU.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">en-GB</td><td class="endo">English (UK)</td><td class="eng">English</td><td class="mono">en_GB.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">zh-Hans</td><td class="endo">简体中文</td><td class="eng">Chinese, Simplified</td><td class="mono">zh_CN.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">zh-Hant</td><td class="endo">繁體中文</td><td class="eng">Chinese, Traditional</td><td class="mono">zh_CN.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">zh-HK</td><td class="endo">繁體中文(香港)</td><td class="eng">Chinese, Hong Kong</td><td class="mono">zh_HK.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">fr</td><td class="endo">Français</td><td class="eng">French</td><td class="mono">fr_BE.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">fr-CA</td><td class="endo">Français canadien</td><td class="eng">French, Canadian</td><td class="mono">fr_CA.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">de</td><td class="endo">Deutsch</td><td class="eng">German</td><td class="mono">de_AT.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">ja</td><td class="endo">日本語</td><td class="eng">Japanese</td><td class="mono">ja_JP.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">es</td><td class="endo">Español</td><td class="eng">Spanish</td><td class="mono">es_AR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">es-419</td><td class="endo">Español (Latinoamérica)</td><td class="eng">Spanish, Latin American</td><td class="mono">es_AR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">it</td><td class="endo">Italiano</td><td class="eng">Italian</td><td class="mono">it_CH.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">ko</td><td class="endo">한국어</td><td class="eng">Korean</td><td class="mono">ko_KR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">pt-BR</td><td class="endo">Português (Brasil)</td><td class="eng">Portuguese, Brazilian</td><td class="mono">pt_BR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">pt-PT</td><td class="endo">Português (Portugal)</td><td class="eng">Portuguese</td><td class="mono">pt_PT.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">ru</td><td class="endo">Русский</td><td class="eng">Russian</td><td class="mono">ru_RU.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">nl</td><td class="endo">Nederlands</td><td class="eng">Dutch</td><td class="mono">nl_BE.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">sv</td><td class="endo">Svenska</td><td class="eng">Swedish</td><td class="mono">sv_FI.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">nb</td><td class="endo">Norsk bokmål</td><td class="eng">Norwegian</td><td class="mono">nb_NO.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">da</td><td class="endo">Dansk</td><td class="eng">Danish</td><td class="mono">da_DK.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">fi</td><td class="endo">Suomi</td><td class="eng">Finnish</td><td class="mono">fi_FI.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">pl</td><td class="endo">Polski</td><td class="eng">Polish</td><td class="mono">pl_PL.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">tr</td><td class="endo">Türkçe</td><td class="eng">Turkish</td><td class="mono">tr_TR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">uk</td><td class="endo">Українська</td><td class="eng">Ukrainian</td><td class="mono">uk_UA.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">cs</td><td class="endo">Čeština</td><td class="eng">Czech</td><td class="mono">cs_CZ.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">sk</td><td class="endo">Slovenčina</td><td class="eng">Slovak</td><td class="mono">sk_SK.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">hu</td><td class="endo">Magyar</td><td class="eng">Hungarian</td><td class="mono">hu_HU.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">ca</td><td class="endo">Català</td><td class="eng">Catalan</td><td class="mono">ca_AD.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">hr</td><td class="endo">Hrvatski</td><td class="eng">Croatian</td><td class="mono">hr_HR.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">el</td><td class="endo">Ελληνικά</td><td class="eng">Greek</td><td class="mono">el_GR.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">he</td><td class="endo">עברית</td><td class="eng">Hebrew</td><td class="mono">he_IL.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">ro</td><td class="endo">Română</td><td class="eng">Romanian</td><td class="mono">ro_RO.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">th</td><td class="endo">ไทย</td><td class="eng">Thai</td><td class="mono gone">—</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">vi</td><td class="endo">Tiếng Việt</td><td class="eng">Vietnamese</td><td class="mono gone">—</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">id</td><td class="endo">Indonesia</td><td class="eng">Indonesian</td><td class="mono gone">—</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">ms</td><td class="endo">Melayu</td><td class="eng">Malay</td><td class="mono gone">—</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">ar</td><td class="endo">العربية</td><td class="eng">Arabic</td><td class="mono">ar_AE.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">hi</td><td class="endo">हिन्दी</td><td class="eng">Hindi</td><td class="mono">hi_IN.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">sl</td><td class="endo">Slovenščina</td><td class="eng">Slovenian</td><td class="mono">sl_SI.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">ru-RU</td><td class="endo">Русский</td><td class="eng">Russian</td><td class="mono">ru_RU.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">en-AU</td><td class="endo">English (Australia)</td><td class="eng">English</td><td class="mono">en_AU.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">en-IN</td><td class="endo">English (India)</td><td class="eng">English</td><td class="mono">en_IN.UTF-8</td><td class="mk"><span class="yes">ASCII</span></td></tr>
<tr><td class="mono tag">es-MX</td><td class="endo">Español (México)</td><td class="eng">Spanish</td><td class="mono">es_MX.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
<tr><td class="mono tag">de-AT</td><td class="endo">Deutsch (Österreich)</td><td class="eng">German</td><td class="mono">de_AT.UTF-8</td><td class="mk"><span class="no">needs glyphs</span></td></tr>
</tbody></table>
<p>The four with no locale at all — Indonesian, Malay, Thai, Vietnamese — would be offered a UI language with no
working <code>LC_CTYPE</code> to match, which is its own small decision. <code>es-419</code> (Latin American Spanish)
has no POSIX equivalent by construction; it is a region-less region.</p>
<div class="rule"></div>
<h2>9 · Still open, and what would need deciding</h2>
<ul>
<li>Whether the chooser is ours or Gershwin's <code>loginwindow</code> grows it — it is the natural owner, and it
already has the framebuffer and the fonts.</li>
<li>Whether an installed system that was configured by <code>nextbsd-installer</code> should ever ask, or inherit
silently.</li>
<li>Keyboard layout. Apple's variable carries it in the same string (<code>en:0</code>) and I have deliberately not
designed for it.</li>
<li>Whether the EFI variable or a file is authoritative when they disagree.</li>
<li><span class="no">Unverified:</span> <code>languagesetup</code>'s exact write set. The logic is in the closed
<code>InternationalSupport.framework</code>, which lives in the dyld shared cache, so the four-key list is inferred
from the resulting plist plus binary strings — not from a syscall trace.</li>
<li><span class="no">Unverified:</span> that a vt CJK font is infeasible rather than merely unattractive. Nobody has
tried; the size estimate is mine.</li>
</ul>
</div>