Locales: the Locale Archive, i18n.extraLocales, and the Missing-Locale Crash

Authors

K19G

grok-bot

Updated

October 31, 2026

Locales: the Locale Archive, i18n.extraLocales, and the Missing-Locale Crash

Goal: Know exactly which locales a NixOS host can load and where they come from. Add regional formats without guessing at option names. Diagnose the crash a program hits when it is told to use a locale the system never built. And when the locale you need does not exist in glibc at all (here an invented-but-reasonable en_AT: English text, Austrian formats), compile it yourself the way nixpkgs does, without rebuilding half the package set.

Version note: everything below was evaluated and built against nixos-26.05 (26.05.20261013.a033e66, glibc 2.42) with Nix 2.35.3. All evaluation, build, and probe output is pasted from those runs. The probe binaries ran from the store on an x86-64 Linux box. No VM was booted, so the “on a running host” commands in the trap section are the standard checks, not captured output.

Mental model

On most distributions, locales are files under /usr/lib/locale, created by locale-gen. NixOS has no such directory. glibc in nixpkgs is patched (nix-locale-archive.patch) to open its locale archive from a list of places, in order:

glibc setlocale(LC_ALL, "")  →  which archive?   (nixpkgs glibc, open_locale_archive)
  1. $LOCALE_ARCHIVE_2_27
  2. $LOCALE_ARCHIVE                ◀── NixOS sets this for services and for sessions
  3. /run/current-system/sw/lib/locale/locale-archive
  4. /usr/lib/locale/locale-archive
  5. glibc's compiled-in path

the archive itself = i18n.glibcLocales
  default: pkgs.glibcLocales.override { locales = config.i18n.supportedLocales; }
  supportedLocales (hidden, computed) =
      "C.UTF-8/UTF-8" + "en_US.UTF-8/UTF-8"
    + i18n.defaultLocale               /UTF-8
    + every value in i18n.extraLocaleSettings (LC_TIME, LC_NUMERIC, …)
    + i18n.extraLocales                (anything else a program needs)

So the archive holds the locales the configuration names, and nothing else. The environment variables (LANG, LC_*) are a separate thing. They are a request, and a process can carry any value: from /etc/locale.conf, from a desktop’s region settings, from a wrapper, from an SSH client if AcceptEnv forwards it. When the request names a locale the archive lacks, glibc does not fail the process. It fails setlocale and leaves the process in the C locale. The rest of this chapter is about what happens next.

Two 26.05 details are worth knowing. i18n.extraLocales (since 25.05) is the supported way to add locales. i18n.supportedLocales is now hidden and deprecated, and setting it by hand produces a warning if you forget anything (Case 2). New in 26.05 is i18n.imperativeLocale: when enabled, localectl set-locale owns /etc/locale.conf and NixOS stops exporting LANG/LC_* itself. It does not add locales to the archive, so the same rule applies: a locale localectl points at must still be in i18n.extraLocales.

Worked examples

The lab is one flake. None of the hosts is booted. Each case evaluates or builds one part of a host, and a small C++ probe runs against the archives those hosts would ship.

# flake.nix — five hosts that differ only in their i18n settings, plus a probe
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  outputs = { nixpkgs, ... }:
    let
      host = extra: nixpkgs.lib.nixosSystem { modules = [ ./host.nix extra ]; };
    in {
      nixosConfigurations = {
        plain   = host { };
        austria = host ./austria.nix;
        trap    = host ./trap-en-at.nix;
        en-at   = host ./en-at.nix;
        en-at-overlay = host ./en-at-overlay.nix;
      };
      # A tiny C/C++ probe built with the same toolchain as the system
      packages.x86_64-linux.probe =
        let pkgs = nixpkgs.legacyPackages.x86_64-linux; in
        pkgs.runCommandCC "locale-probe" { } ''
          mkdir -p $out/bin
          $CXX -O2 -o $out/bin/locale-probe ${./probe/locale-probe.cc}
        '';
    };
}
# host.nix — a minimal host; nothing about locales is set here
{ ... }: {
  nixpkgs.hostPlatform = "x86_64-linux";
  boot.loader.systemd-boot.enable = true;
  fileSystems."/" = { device = "/dev/disk/by-label/nixos"; fsType = "ext4"; };
  system.stateVersion = "26.05";
}

The probe does what a GUI toolkit does at startup: apply the user’s locale, decode a filename to wide characters, format a date and a number, then build a C++ std::locale from the environment.

// probe/locale-probe.cc — what a GUI app does with $LANG and a filename
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <locale>

int main(int argc, char **argv) {
  const char *file = argc > 1 ? argv[1] : "Bericht_März.pdf";
  std::setvbuf(stdout, nullptr, _IOLBF, 0);  // keep lines that precede an abort

  // 1. The C layer: failure is a NULL return, and the process stays in "C".
  const char *got = std::setlocale(LC_ALL, "");
  std::printf("setlocale(LC_ALL, \"\") = %s\n", got ? got : "NULL -> still \"C\"");

  // 2. Decode a filename the way toolkits do before showing or sending it.
  wchar_t wide[256];
  std::size_t n = std::mbstowcs(wide, file, 256);
  if (n == static_cast<std::size_t>(-1))
    std::printf("mbstowcs(\"%s\") = -1 (invalid multibyte sequence)\n", file);
  else
    std::printf("mbstowcs(\"%s\") = %zu characters\n", file, n);

  // 3. Formats from the locale, when there is one.
  char buf[64];
  std::time_t t = 1792022400;  // 2026-10-15 00:00 UTC
  std::strftime(buf, sizeof buf, "%x  %A", std::gmtime(&t));
  std::printf("date: %s   number: %'.2f\n", buf, 1234567.891);

  // 4. The C++ layer: the same failure is an exception.
  std::locale user("");
  std::cout << "std::locale(\"\") = " << user.name() << "\n";
}

Case 1: What a default host ships

$ nix eval --json .#nixosConfigurations.plain.config.i18n.supportedLocales
["C.UTF-8/UTF-8","en_US.UTF-8/UTF-8"]
$ nix eval --json .#nixosConfigurations.plain.config.systemd.globalEnvironment
{"LOCALE_ARCHIVE":"/nix/store/dc427sjs33dci797apv4lvsg1a69szn0-glibc-locales-2.42-100/lib/locale/locale-archive","TZDIR":"/nix/store/mkbbamycmw838fwxk21jz0nw3mhnmk16-tzdata-2026c/share/zoneinfo"}

Two locales, about 3 MB of archive, and this exact store path is in cache.nixos.org, so a default host downloads it instead of building it. systemd services get the archive by store path. Login sessions get LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive from environment.sessionVariables, which follows the current generation. Every service unit NixOS renders carries that LOCALE_ARCHIVE= line, which is why it appears in the unit dumps elsewhere in this part.

To run the probe against the archives, build everything once and keep the paths in variables:

$ b() { nix build --no-link --print-out-paths "$@"; }
$ PLAIN=$(b .#nixosConfigurations.plain.config.i18n.glibcLocales)/lib/locale/locale-archive
$ PROBE=$(b .#probe)/bin/locale-probe
$ LOCALE=$(b .#nixosConfigurations.plain.pkgs.glibc.bin)/bin/locale
$ env -i LOCALE_ARCHIVE=$PLAIN LANG=en_US.UTF-8 $PROBE
setlocale(LC_ALL, "") = en_US.UTF-8
mbstowcs("Bericht_März.pdf") = 16 characters
date: 10/15/2026  Thursday   number: 1,234,567.89
std::locale("") = en_US.UTF-8

env -i starts from an empty environment, so the output depends only on the two variables shown. Use absolute paths after env -i. With no PATH, a bare locale silently resolves to whatever is in /usr/bin, which on a non-NixOS build box is the distribution’s own glibc, and that one ignores LOCALE_ARCHIVE.

Case 2: Regional formats the supported way

A common desk setup is English messages with local formats:

# austria.nix — English messages, Austrian formats, one extra locale for a tool
{ ... }: {
  i18n.defaultLocale = "en_US.UTF-8";
  i18n.extraLocaleSettings = {
    LC_TIME = "de_AT.UTF-8";
    LC_NUMERIC = "de_AT.UTF-8";
    LC_MONETARY = "de_AT.UTF-8";
    LC_PAPER = "de_AT.UTF-8";
    LC_MEASUREMENT = "de_AT.UTF-8";
  };
  i18n.extraLocales = [ "en_GB.UTF-8/UTF-8" ];
}
$ nix eval --json .#nixosConfigurations.austria.config.i18n.supportedLocales
["C.UTF-8/UTF-8","en_US.UTF-8/UTF-8","de_AT.UTF-8/UTF-8","en_GB.UTF-8/UTF-8"]
$ cat $(b '.#nixosConfigurations.austria.config.environment.etc."locale.conf".source')
LANG=en_US.UTF-8
LC_MEASUREMENT=de_AT.UTF-8
LC_MONETARY=de_AT.UTF-8
LC_NUMERIC=de_AT.UTF-8
LC_PAPER=de_AT.UTF-8
LC_TIME=de_AT.UTF-8

Every value in extraLocaleSettings is added to the archive automatically, deduplicated, with the /UTF-8 charset appended (i18n.localeCharsets changes that per category). extraLocales is only for locales nothing in the config names, for example one a single tool needs. This archive is not in the binary cache, so the host builds it locally. On the lab box that took 15 seconds: the builder unpacks glibc, configures it, and runs localedef for just these four locales.

$ AUSTRIA=$(b .#nixosConfigurations.austria.config.i18n.glibcLocales)/lib/locale/locale-archive
$ env -i LOCALE_ARCHIVE=$AUSTRIA LANG=en_US.UTF-8 LC_TIME=de_AT.UTF-8 LC_NUMERIC=de_AT.UTF-8 $PROBE | sed -n 3p
date: 2026-10-15  Donnerstag   number: 1.234.567,89

That line is the reason en_AT comes up at all: glibc’s de_AT uses ISO dates and German day and month names. If you want Austrian numbers and English names, glibc has no such locale.

If you still set the hidden option yourself, NixOS tells you what you forgot:

$ nix eval --json .#nixosConfigurations.austria --apply 'c: (c.extendModules { modules = [ { i18n.supportedLocales = [ "C.UTF-8/UTF-8" "en_US.UTF-8/UTF-8" ]; } ]; }).config.warnings'
["`i18n.supportedLocales` is deprecated in favor of `i18n.extraLocales`,\nand it seems you are using `i18n.supportedLocales` and forgot to\ninclude some locales specified in `i18n.defaultLocale`,\n`i18n.extraLocales` or `i18n.extraLocaleSettings`.\n\nIf you're trying to install additional locales not specified in\n`i18n.defaultLocale` or `i18n.extraLocaleSettings`, consider adding\nonly those locales to `i18n.extraLocales`.\n"]

Case 3: Asking for a locale glibc does not ship (the good failure)

# trap-en-at.nix — asks for a locale glibc does not ship
{ ... }: {
  i18n.defaultLocale = "en_AT.UTF-8";
}

Evaluation is happy. It just appends en_AT.UTF-8/UTF-8 to the list:

$ nix eval --json .#nixosConfigurations.trap.config.i18n.supportedLocales
["C.UTF-8/UTF-8","en_US.UTF-8/UTF-8","en_AT.UTF-8/UTF-8"]
$ nix build .#nixosConfigurations.trap.config.i18n.glibcLocales
…
glibc-locales> zu_ZA.UTF-8/UTF-8 \
glibc-locales> zu_ZA/ISO-8859-1 \
glibc-locales> Error: unsupported locales detected:
glibc-locales> en_AT.UTF-8/UTF-8 \
glibc-locales> You should choose from the list above the error.
error: Cannot build '/nix/store/0p9vwnax8swaypzma2dnyy6c75nn6dik-glibc-locales-2.42-100.drv'.
       Reason: builder failed with exit code 1.

The builder compares your list with glibc’s localedata/SUPPORTED (501 entries in 2.42) before compiling anything, and refuses. This is the failure you want. It happens at nixos-rebuild time, on your terminal, not at runtime on somebody’s desktop. The list printed above the error is the authoritative spelling of every locale you can ask for.

Case 4: The runtime failure: the archive lacks what the environment asks for

The dangerous version never touches the build. The host ships the default archive, and something in a user’s session sets LANG=en_AT.UTF-8. A single category is enough: LANG=en_US.UTF-8 LC_TIME=de_AT.UTF-8 against the same archive aborts in exactly the same way, because setlocale(LC_ALL, "") fails if any one category cannot be loaded. Run the probe:

$ env -i LOCALE_ARCHIVE=$PLAIN LANG=en_AT.UTF-8 $PROBE; echo "exit=$?"
setlocale(LC_ALL, "") = NULL -> still "C"
mbstowcs("Bericht_März.pdf") = -1 (invalid multibyte sequence)
date: 10/15/26  Thursday   number: 1234567.89
terminate called after throwing an instance of 'std::runtime_error'
  what():  locale::facet::_S_create_c_locale name not valid
exit=134

(Your shell also prints its own Aborted job line, omitted here.) Read it top to bottom, because this is how such bugs show up in the wild:

  1. The C layer fails quietly. setlocale returns NULL and the process stays in C. Most C programs never check the return value.
  2. ASCII works and non-ASCII does not. In the C locale the multibyte charset is ASCII, so März is an invalid sequence. A program that converts filenames to wide strings works perfectly until the first file with an umlaut, an accent, or a CJK name. “It crashes only for some files” usually starts here.
  3. The formats degrade silently. The date and number fall back to US-style C formats. Nothing reports an error.
  4. The C++ layer throws. std::locale("") turns the same condition into std::runtime_error. If nothing catches it, the process aborts (exit 134, SIGABRT). A toolkit that builds a std::locale or a codecvt facet on demand, for example when a file-transfer dialog first converts a filename, crashes at that moment instead of at startup. That is why the crash looks related to the feature, not to the locale.

locale itself shows the condition before any application does:

$ env -i LOCALE_ARCHIVE=$PLAIN LANG=en_AT.UTF-8 $LOCALE 2>&1 >/dev/null
/nix/store/kgd806ssw99qaxwiislhfx5dwnc8k893-glibc-2.42-100-bin/bin/locale: Cannot set LC_CTYPE to default locale: No such file or directory
/nix/store/kgd806ssw99qaxwiislhfx5dwnc8k893-glibc-2.42-100-bin/bin/locale: Cannot set LC_MESSAGES to default locale: No such file or directory
/nix/store/kgd806ssw99qaxwiislhfx5dwnc8k893-glibc-2.42-100-bin/bin/locale: Cannot set LC_ALL to default locale: No such file or directory

Overriding the request is enough to stop a single program crashing, which is useful while you fix the host:

$ env -i LOCALE_ARCHIVE=$PLAIN LANG=en_AT.UTF-8 LC_ALL=en_US.UTF-8 $PROBE | tail -1
std::locale("") = en_US.UTF-8

Case 5: Building a site-local locale with the stock builder

If a locale really does not exist in glibc, write it. A glibc locale source is a text file with one section per category. Most sections can copy an existing locale, so a new one is mostly decisions, not data:

comment_char %
escape_char /
% locales/en_AT — English language, Austrian conventions.
% Site-local: glibc does not ship this locale. Build it, don't wish for it.

LC_IDENTIFICATION
title      "English locale for Austria (site-local)"
source     "example.org IT"
address    ""
contact    ""
email      ""
tel        ""
fax        ""
language   "English"
territory  "Austria"
revision   "1.0"
date       "2026-10-15"
category "i18n:2012";LC_IDENTIFICATION
category "i18n:2012";LC_CTYPE
category "i18n:2012";LC_COLLATE
category "i18n:2012";LC_TIME
category "i18n:2012";LC_NUMERIC
category "i18n:2012";LC_MONETARY
category "i18n:2012";LC_MESSAGES
category "i18n:2012";LC_PAPER
category "i18n:2012";LC_NAME
category "i18n:2012";LC_ADDRESS
category "i18n:2012";LC_TELEPHONE
category "i18n:2012";LC_MEASUREMENT
END LC_IDENTIFICATION

LC_CTYPE
copy "en_GB"
END LC_CTYPE

LC_COLLATE
copy "iso14651_t1"
END LC_COLLATE

LC_MESSAGES
copy "en_GB"
END LC_MESSAGES

% Money and numbers the Austrian way: € 1.234,56
LC_MONETARY
copy "de_AT"
END LC_MONETARY

LC_NUMERIC
copy "de_AT"
END LC_NUMERIC

% English names, Austrian day-first date, Monday first
LC_TIME
abday   "Sun";"Mon";"Tue";"Wed";"Thu";"Fri";"Sat"
day     "Sunday";"Monday";"Tuesday";"Wednesday";"Thursday";"Friday";"Saturday"
abmon   "Jan";"Feb";"Mar";"Apr";"May";"Jun";"Jul";"Aug";"Sep";"Oct";"Nov";"Dec"
mon     "January";"February";"March";"April";"May";"June";/
        "July";"August";"September";"October";"November";"December"
d_t_fmt "%a %d %b %Y %T"
date_fmt "%a %d %b %Y %T %Z"
d_fmt   "%d.%m.%Y"
t_fmt   "%T"
am_pm   "";""
t_fmt_ampm ""
week    7;19971130;4
first_weekday 2
END LC_TIME

LC_PAPER
copy "de_AT"
END LC_PAPER

LC_TELEPHONE
copy "de_AT"
END LC_TELEPHONE

LC_MEASUREMENT
copy "de_AT"
END LC_MEASUREMENT

LC_NAME
copy "en_GB"
END LC_NAME

LC_ADDRESS
postal_fmt   "%f%N%a%N%d%N%b%N%s %h %e %r%N%z %T%N%c%N"
country_name "Austria"
country_ab2  "AT"
country_ab3  "AUT"
country_num  40
country_car  "A"
lang_name    "English"
lang_ab      "en"
lang_term    "eng"
lang_lib     "eng"
END LC_ADDRESS

Wire it in by extending the derivation the module would have built anyway. The source file goes into glibc’s localedata/locales, and one line goes into SUPPORTED before the builder runs its check from Case 3:

# en-at.nix — a site-local locale, compiled by the stock glibcLocales builder
{ pkgs, ... }:
{
  i18n.defaultLocale = "en_AT.UTF-8";

  # Same derivation the module would build, plus one source file and one
  # SUPPORTED line, added before the builder's "is it supported?" check.
  i18n.glibcLocales =
    (pkgs.glibcLocales.override {
      allLocales = false;
      locales = [ "C.UTF-8/UTF-8" "en_US.UTF-8/UTF-8" "en_AT.UTF-8/UTF-8" ];
    }).overrideAttrs (old: {
      preBuild = ''
        cp ${./locales/en_AT} ../localedata/locales/en_AT
        echo 'en_AT.UTF-8/UTF-8 \' >> ../localedata/SUPPORTED
      '' + old.preBuild;
    });
}

Setting i18n.glibcLocales replaces the default archive completely, so the list must contain every locale the host needs, not just the new one. The module’s own description warns about this. Keep C.UTF-8 in the list: the module’s default always includes it, and the builder compiles only what you list.

$ nix eval --json .#nixosConfigurations.en-at.config.warnings
[]
$ nix build -L .#nixosConfigurations.en-at.config.i18n.glibcLocales 2>&1 | grep -E 'UTF-8\.+ done$|Adding .*en_AT'   # first build
glibc-locales> C.UTF-8en_AT.UTF-8en_US.UTF-8......... done
glibc-locales> Adding /build//nix/store/fsd4vbp9zjd9iaicq2fb9lpc1h5j973y-glibc-2.42-100/lib/locale/en_AT.utf8
$ ENAT=$(b .#nixosConfigurations.en-at.config.i18n.glibcLocales)/lib/locale/locale-archive
$ for a in PLAIN AUSTRIA ENAT; do echo "$a: $(LOCALE_ARCHIVE=${!a} $LOCALE -a | tr '\n' ' ')"; done
PLAIN: C C.utf8 POSIX en_US.utf8
AUSTRIA: C C.utf8 POSIX de_AT.utf8 en_GB.utf8 en_US.utf8
ENAT: C C.utf8 POSIX en_AT.utf8 en_US.utf8
$ env -i LOCALE_ARCHIVE=$ENAT LANG=en_AT.UTF-8 $PROBE; echo "exit=$?"
setlocale(LC_ALL, "") = en_AT.UTF-8
mbstowcs("Bericht_März.pdf") = 16 characters
date: 15.10.2026  Thursday   number: 1.234.567,89
std::locale("") = en_AT.UTF-8
exit=0
$ env -i LOCALE_ARCHIVE=$ENAT LANG=en_AT.UTF-8 $LOCALE -k currency_symbol mon_decimal_point d_fmt first_weekday country_name lang_name
currency_symbol="€"
mon_decimal_point=","
d_fmt="%d.%m.%Y"
first_weekday=2
country_name="Austria"
lang_name="English"

English day names, an Austrian day-first date, euro amounts with a decimal comma, and a week that starts on Monday. The same probe that aborted in Case 4 now runs cleanly. The host wiring follows from the module: /etc/locale.conf says LANG=en_AT.UTF-8, and services get the new archive by store path:

$ nix eval --json .#nixosConfigurations.en-at.config.systemd.globalEnvironment.LOCALE_ARCHIVE
"/nix/store/qrw04749s4scz8fka96arprnibbrl1qq-glibc-locales-2.42-100/lib/locale/locale-archive"

Two notes on the source file. The monetary thousands separator copied from de_AT is U+202F, a narrow no-break space (locale mon_thousands_sep | od -An -tx1 prints e2 80 af 0a). Formatted amounts that look like € 1 234,56 are not ASCII, and code that parses them back will notice. And keep the file in the flake next to the module that uses it. It is configuration, and a reviewer should see it in the same diff.

The trap

The tempting shortcut for Case 5 is an overlay: patch glibcLocales once in nixpkgs.overlays, and the module’s default (pkgs.glibcLocales.override { … }) picks it up with no i18n.glibcLocales line. It evaluates, and it would even work. Now look at what it costs, compared on the full system closure:

# en-at-overlay.nix — the same locale, injected with an overlay instead
{ ... }:
{
  i18n.defaultLocale = "en_AT.UTF-8";
  nixpkgs.overlays = [
    (final: prev: {
      glibcLocales = prev.glibcLocales.overrideAttrs (old: {
        preBuild = ''
          cp ${./locales/en_AT} ../localedata/locales/en_AT
          echo 'en_AT.UTF-8/UTF-8 \' >> ../localedata/SUPPORTED
        '' + old.preBuild;
      });
    })
  ];
}
$ for h in plain en-at en-at-overlay; do echo "== $h"; nix build --dry-run .#nixosConfigurations.$h.config.system.build.toplevel 2>&1 | grep -E 'will be (built|fetched)'; done
== plain
these 254 derivations will be built:
these 367 paths will be fetched (384.5 MiB download, 916.8 MiB unpacked):
== en-at
these 254 derivations will be built:
these 367 paths will be fetched (384.5 MiB download, 916.8 MiB unpacked):
== en-at-overlay
these 1421 derivations will be built:
these 1174 paths will be fetched (3.5 GiB download, 7.5 GiB unpacked):

(The 254 local derivations for plain are this lab’s own configuration files and units, which no cache has. en-at matches it because its archive is already built.) The overlay changes pkgs.glibcLocales for every derivation that has it anywhere in its build-time graph, and for everything downstream of those. Test suites and documentation builds often pull it in to get a UTF-8 locale, so the change spreads a long way. In this closure the rebuild list includes gcc-15.3.0, glib-2.88.3, coreutils-full-9.11, dbus-broker-37 and the linux-6.18.56 kernel. They all get new store paths, none of them is in the cache, and your laptop compiles a compiler because you wanted English month names. The i18n.glibcLocales option changes one runtime file and nothing that is built from source.

The second half of the trap is the runtime failure from Case 4, because no build ever sees it. A host can be perfectly configured and still have users whose session asks for a locale it does not have. When a desktop app crashes only on certain files, or a C++ service dies with _S_create_c_locale name not valid, check the request and the archive before you read the application’s code:

$ tr '\0' '\n' < /proc/$(pidof the-app)/environ | grep -E '^(LANG|LC_|LOCALE_ARCHIVE)'
$ locale -a                      # what the current generation's archive holds
$ locale 2>&1 >/dev/null         # "Cannot set LC_… to default locale" = mismatch

The boring rule

List every locale the host should load in configuration: i18n.defaultLocale, i18n.extraLocaleSettings for per-category formats, i18n.extraLocales for anything else a person or program will ask for. Do not set i18n.supportedLocales yourself. If glibc does not ship a locale, write the source file, keep it in the flake, and attach it through i18n.glibcLocales with the complete list. Never patch glibcLocales in an overlay. When a program crashes on non-ASCII names, run locale in its environment first.

Try this

  1. Add LC_TIME = "en_IE.UTF-8"; to austria.nix and confirm, without building, that en_IE.UTF-8/UTF-8 joins supportedLocales. Then compare the probe’s date line with de_AT.
  2. Change d_fmt in locales/en_AT to "%Y-%m-%d", rebuild, and watch which store path changes and which does not.
  3. Set i18n.imperativeLocale = true; on en-at, and diff environment.sessionVariables and environment.etc against the declarative host. Which file stops being managed?
  4. Catch the exception in the probe (try { std::locale(""); } catch (const std::runtime_error &) { … }) and fall back to std::locale::classic(). Decide whether the program should then refuse non-ASCII filenames or decode them as UTF-8 regardless.
  5. Wrap a single program so it always runs in a known locale: pkgs.symlinkJoin plus makeWrapper … --set LC_ALL en_US.UTF-8. When is that better than fixing the host?

Sources