Read /etc/locale.conf when configuring the distro locale (#40940)

* Read /etc/locale.conf when configuring the distro locale ConfigUpdateLanguage only read /etc/default/locale to populate the $LANG environment variable. That path is used by Debian/Ubuntu, but systemd-based distributions (Fedora, Arch, openSUSE, ...) store the locale in /etc/locale.conf instead, so $LANG was never set on those distros. Check both locations and use the first one that exists. Both files share the same "LANG=" line format, so the existing parsing is reused unchanged. Fixes #13207 * Add unit test for distro locale configuration Cover the ConfigUpdateLanguage code path that populates $LANG from the distro locale file. The test creates the locale files in the test distro and verifies that $LANG is set correctly for each supported layout: - only /etc/default/locale present (Debian/Ubuntu) - only /etc/locale.conf present (Fedora, Arch, openSUSE, ...) - both present, confirming /etc/default/locale takes precedence

i4M1k0SU committed Jul 10, 2026 at 10:30 UTC 3dbf5c1ad9b54938f3381214b0935d8a5e0e9aa7
2 files changed +64 -7
src/linux/init/config.cpp
+24 -7
@@ -53,6 +53,7 @@ Abstract:
53 #define HOSTS_FILE_PATH ETC_FOLDER "hosts"
54 #define LANG_ENV "LANG"
55 #define LOCALE_FILE_PATH ETC_DEFAULT_FOLDER "locale"
56 +#define LOCALE_CONF_FILE_PATH ETC_FOLDER "locale.conf"
57 #define PATH_ENV "PATH"
58 #define RESOLV_CONF_DIRECTORY_MODE 0755
59 #define RESOLV_CONF_FILE_MODE 0644
@@ -2514,9 +2515,14 @@ void ConfigUpdateLanguage(EnvironmentBlock& Environment)
2515
2516 Routine Description:
2517
2517 - This routine queries the contents of the /etc/default/locale text file and
2518 + This routine queries the contents of the locale configuration file and
2519 if present updates the $LANG environment variable in the environment block.
2520
2521 + Different distributions store this file in different locations:
2522 + - /etc/default/locale
2523 + - /etc/locale.conf
2524 + Both share the same "LANG=" line format, so the first file that exists is used.
2525 +
2526 Arguments:
2527
2528 Environment - Supplies the environment block pointer to update.
@@ -2530,22 +2536,33 @@ Return Value:
2536 try
2537 {
2538 //
2533 - // Attempt to open the /etc/default/locale file. If the file does not exist
2534 - // then the $LANG environment variable will not be updated.
2539 + // Attempt to open the locale configuration file, trying each known path in turn.
2540 + // If none of the files exist then the $LANG environment variable will not be updated.
2541 //
2536 - // N.B. This file is being opened by root. The only user-visible content
2542 + // N.B. These files are being opened by root. The only user-visible content
2543 // will be the contents of the last line of the file that contains
2544 // "LANG=".
2545 //
2546
2541 - wil::unique_file LocaleFile{fopen(LOCALE_FILE_PATH, "r")};
2542 - if (!LocaleFile)
2547 + constexpr const char* LocaleFilePaths[] = {LOCALE_FILE_PATH, LOCALE_CONF_FILE_PATH};
2548 +
2549 + wil::unique_file LocaleFile;
2550 + for (const auto* Path : LocaleFilePaths)
2551 {
2552 + LocaleFile.reset(fopen(Path, "r"));
2553 + if (LocaleFile)
2554 + {
2555 + break;
2556 + }
2557 +
2558 if (errno != ENOENT)
2559 {
2546 - LOG_ERROR("fopen({}) failed {}", LOCALE_FILE_PATH, errno);
2560 + LOG_ERROR("fopen({}) failed {}", Path, errno);
2561 }
2562 + }
2563
2564 + if (!LocaleFile)
2565 + {
2566 return;
2567 }
2568
test/windows/UnitTests.cpp
+40
@@ -516,6 +516,46 @@ class UnitTests
516 }
517 }
518
519 + WSL2_TEST_METHOD(ConfigUpdateLanguage)
520 + {
521 + // Validates that init populates $LANG from the distro locale configuration file.
522 + // ConfigUpdateLanguage reads /etc/default/locale first, then /etc/locale.conf, and uses
523 + // the first file that exists. See ConfigUpdateLanguage in src/linux/init/config.cpp.
524 +
525 + DistroFileChange defaultLocale(L"/etc/default/locale", LxsstuLaunchWsl(L"test -f /etc/default/locale") == 0);
526 + DistroFileChange localeConf(L"/etc/locale.conf", LxsstuLaunchWsl(L"test -f /etc/locale.conf") == 0);
527 +
528 + const auto readLang = []() { return LxsstuLaunchWslAndCaptureOutput(L"echo $LANG").first; };
529 +
530 + // Only /etc/default/locale is present (Debian/Ubuntu).
531 + {
532 + defaultLocale.Delete();
533 + localeConf.Delete();
534 + defaultLocale.SetContent(L"LANG=de_DE.UTF-8\n");
535 + TerminateDistribution();
536 + VERIFY_ARE_EQUAL(readLang(), L"de_DE.UTF-8\n");
537 + }
538 +
539 + // Only /etc/locale.conf is present (Fedora, Arch, openSUSE, ...).
540 + {
541 + defaultLocale.Delete();
542 + localeConf.Delete();
543 + localeConf.SetContent(L"LANG=fr_FR.UTF-8\n");
544 + TerminateDistribution();
545 + VERIFY_ARE_EQUAL(readLang(), L"fr_FR.UTF-8\n");
546 + }
547 +
548 + // Both files are present: /etc/default/locale takes precedence because it is read first.
549 + {
550 + defaultLocale.Delete();
551 + localeConf.Delete();
552 + defaultLocale.SetContent(L"LANG=ja_JP.UTF-8\n");
553 + localeConf.SetContent(L"LANG=en_US.UTF-8\n");
554 + TerminateDistribution();
555 + VERIFY_ARE_EQUAL(readLang(), L"ja_JP.UTF-8\n");
556 + }
557 + }
558 +
559 TEST_METHOD(Dup)
560 {
561 VERIFY_NO_THROW(LxsstuRunTest(L"/data/test/wsl_unit_tests dup", L"Dup"));