Proven C Book한국어 GitHub

68 Locales ① — a program’s regional settings

What to know first

chapter 67, Character classification · that the judgement depends on the locale
chapter 53, The three faces of main · environment variables

Looking back

Chapter 67 said the answer isalpha gives depends on “the current locale”. But I have never set a locale. What does it mean for an answer to depend on something I never set?

A. Because one is already settled even if you set nothing. The standard fixes that too — at program startup the state is as if setlocale(LC_ALL, "C") had been called (§7.11.1.1p4). So a program that does nothing runs in the minimal environment called the C locale.

A locale is the device by which settings outside the program change behaviour inside it. It is what makes one executable print 2026년 8월 6일 in Seoul, 06.08.2026 in Berlin, and 3,14 from printf("%f"). This chapter is about the names and rules of that device; the next is about what it changes.

The need for this chapter, and its context

Locales are spread over two chapters because this is C’s most underrated piece of global state. Right after chapter 67 shows that even a judgement depends on the locale is the moment to ask what it actually is. And without this chapter there is no explaining what chapter 70′s multibyte conversion leans on.

By the end of this chapter

What a locale is, from the ground up. Why it exists, what the six categories divide, the exact contract of setlocale, by what rule and which international standards a name like ko_KR.UTF-8 is built, the precedence of the environment variables, and where on the machine that data lives.

The questions this chapter answers

  1. Is a locale translation?
  2. May categories be mixed?
  3. Which standard fixes this grammar?

68.1 Why it exists — conventions differ by country

When a program shows something to a person, there is not one “correct” way to write it.

WhatKoreaGermany
Decimal point3.143,14
Thousands1,234,5671.234.567
Date2026년 8월 6일06.08.2026
Currency₩1,2341.234,00 €
SortingHangul orderä filed with a

Table 69.1

Writing this out by hand in every program multiplies the code by the number of countries. So Unix and C took another road — make the conventions data, keep them outside the program, and let the program choose only which set to work with. That bundle of data is a locale.

Q. Is a locale translation?

A. No, and the distinction matters. A locale deals with conventions — decimal points, grouping, the order of a date, sorting, case, currency symbols. Turning the messages a program prints into another language, that is, translation, is outside standard C.

Unix has a separate LC_MESSAGES category and tools such as gettext for translation, but it is not among the six categories C fixes. What this book covers stops at conventions too.

68.2 A locale is process-global state

The nature of a locale in one line: one locale per process.

examples-en/ch68/locale_probe.c

/* A locale is process-global state — what it starts as, and what changes it. */
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>

/* setlocale returns "the name of the locale now in force".
   Pass NULL as the second argument to ask without changing anything. */
static void show_current(const char *when)
{
    printf("%s LC_ALL=%s\n", when, setlocale(LC_ALL, NULL));
}

int main(void)
{
    /* (1) A program always starts in the "C" locale (standard §7.11.1.1p4) */
    show_current("at startup:         ");

    /* (2) "" means "the locale the environment says".
           It reads LC_ALL, then LC_xxx, then LANG. */
    const char *applied = setlocale(LC_ALL, "");
    if (applied)
        printf("setlocale(LC_ALL,\"\"):  %s\n", applied);
    else
        puts("setlocale(LC_ALL,\"\") failed — the environment has no locale");

    /* (3) How many bytes one character may take depends on the locale.
           MB_CUR_MAX looks like a constant but reads *the current locale*. */
    printf("MB_CUR_MAX:             %zu\n", (size_t)MB_CUR_MAX);

    /* (4) If the requested locale is not on this machine, setlocale returns
           null. So always look at the return value — it fails quietly. */
    static const char *candidates[] = {
        "C", "C.UTF-8", "en_US.UTF-8", "ko_KR.UTF-8", "ko_KR.EUC-KR",
        "de_DE.UTF-8", "tr_TR.UTF-8", "ja_JP.UTF-8", "no_SUCH.locale",
    };

    puts("\nAvailable on this machine?");
    char saved[128];
    snprintf(saved, sizeof saved, "%s", setlocale(LC_ALL, NULL));

    for (size_t i = 0; i < sizeof candidates / sizeof *candidates; i++) {
        const char *got = setlocale(LC_ALL, candidates[i]);
        if (got) printf("  %-15s yes  MB_CUR_MAX=%zu\n",
                        candidates[i], (size_t)MB_CUR_MAX);
        else     printf("  %-15s no\n", candidates[i]);
    }

    /* (5) Put it back — hand the name string in again. But the pointer
           setlocale returned may be overwritten by the next call, so copy it. */
    setlocale(LC_ALL, saved);
    show_current("\nrestored:           ");

    /* (6) Categories can be set separately. This is the idiom in practice —
           what people see follows the environment, numbers a machine reads
           stay in "C". */
    setlocale(LC_ALL, "");
    setlocale(LC_NUMERIC, "C");
    printf("mixed — LC_CTYPE=%s, LC_NUMERIC=%s\n",
           setlocale(LC_CTYPE, NULL), setlocale(LC_NUMERIC, NULL));
    /* Asking LC_ALL lists every category when they are not all the same */
    printf("asking LC_ALL gives: %.60s...\n", setlocale(LC_ALL, NULL));
    return 0;
}

Output

at startup:          LC_ALL=C
setlocale(LC_ALL,""):  C.UTF-8
MB_CUR_MAX:             6

Available on this machine?
  C               yes  MB_CUR_MAX=1
  C.UTF-8         yes  MB_CUR_MAX=6
  en_US.UTF-8     yes  MB_CUR_MAX=6
  ko_KR.UTF-8     yes  MB_CUR_MAX=6
  ko_KR.EUC-KR    yes  MB_CUR_MAX=2
  de_DE.UTF-8     yes  MB_CUR_MAX=6
  tr_TR.UTF-8     yes  MB_CUR_MAX=6
  ja_JP.UTF-8     yes  MB_CUR_MAX=6
  no_SUCH.locale  no

restored:            LC_ALL=C.UTF-8
mixed — LC_CTYPE=C.UTF-8, LC_NUMERIC=C
asking LC_ALL gives: LC_CTYPE=C.UTF-8;LC_NUMERIC=C;LC_TIME=C.UTF-8;LC_COLLATE=C.U...

The first line of the demonstration shows the standard’s rule in the flesh. Before anything happens, LC_ALL is C. And one setlocale(LC_ALL, "") moves it to whatever the environment says.

Two things follow from its being global.

First, it can change without you calling anything. If a library you linked calls setlocale, your printf("%f") is affected from that moment. GUI toolkits commonly do.

Second, it is a race between threads. C23 states this explicitly — a call to setlocale may introduce a data race with other setlocale calls or with functions affected by the locale (§7.11.1.1p5). If you want different locales in different threads, standard C has no road; POSIX’s uselocale family is needed (chapter 69).

68.3 The six categories — what governs what

A locale is not one lump; it divides into categories. The standard fixes six, and it also enumerates what each one affects.

CategoryWhat it governsFunctions affected
LC_CTYPEClassification, case, multibyte conversionisalpha family (chapter 67), mbrtowc family (chapter 70)
LC_NUMERICThe decimal point and grouping of plain numbersprintf, scanf, strtod
LC_MONETARYMonetary formatting informationlocaleconv
LC_COLLATEString comparison orderstrcoll, strxfrm
LC_TIMEDate and time formattingstrftime, wcsftime
LC_ALL(the name for all of the above at once)

Table 69.2

You need not memorise the table, but one line is worth keeping: LC_NUMERIC governs printf. Nearly every case of this device corrupting data in practice comes from that line (chapter 69).

Platform note. The categories POSIX added

On top of standard C’s six, POSIX and glibc laid more. LC_MESSAGES (translation), and glibc’s LC_PAPER (paper size), LC_NAME (the order of name parts), LC_ADDRESS, LC_TELEPHONE, LC_MEASUREMENT (metric or not), LC_IDENTIFICATION. The long semicolon-separated list the demonstration printed when asking LC_ALL is exactly those.

The standard leaves the door open: names beginning with LC_ and an upper-case letter may be defined by the implementation (§7.11p3). So these names work on Linux and may not elsewhere.

68.4 The exact contract of setlocale

One function, simple in shape, dense in contract.

char *setlocale(int category, const char *locale);
Second argumentMeaning
"C"The minimal environment the standard fixes. Where a program starts
"" (empty string)The locale the environment says — it reads the environment variables
NULLDo not change anything; only report the present value
Any other stringAn implementation-defined name ("ko_KR.UTF-8", …)

Table 69.3

The return value splits two ways. On success it returns a string holding the name of the locale that was set (or is in force); on failure it returns null.

A common misconception. setlocale does not fail”

The quietest accident starts here. If the requested locale is not installed on that machine, setlocale returns null and changes nothing. Without looking at the return value the program carries on believing the locale changed, dates come out in English and Korean comes out broken.

The no_SUCH.locale line of the demonstration is that case. Linux distributions often ship a minimum of locales to save space (container images especially), which makes this the classic place where what worked on the developer’s machine does not work in production.

if (!setlocale(LC_ALL, "")) {
    fprintf(stderr, "warning: could not apply the locale; continuing in C.\n");
}

The returned pointer has a rule too. That string points into static storage that a later setlocale call may overwrite. If you intend to restore it later, copy it, as the demonstration does.

Q. May categories be mixed?

A. They may, and doing so is the standard practice. The last part of the demonstration is the idiom.

setlocale(LC_ALL, "");        /* follow the environment for everything, then */
setlocale(LC_NUMERIC, "C");   /* put numbers back into "C" */

Dates, sorting and currency shown to a person follow the environment, while numbers a machine will read and write are pinned so the locale cannot move them. These two lines prevent most of the data corruption seen in chapter 69.

Once categories are mixed, setlocale(LC_ALL, NULL) returns a long string of the form LC_CTYPE=…;LC_NUMERIC=…;…. That is the demonstration’s last line — the rule is “one name if they all agree, a list if they do not.”

68.5 The grammar of a locale name

ko_KR.UTF-8. Take the name apart and there are four international standards inside it.

PositionExampleFixed by
LanguagekoISO 639-1 (two letters), or ISO 639-2/-3 (three)
_ + territory_KRISO 3166-1 alpha-2 country code
. + codeset.UTF-8A character-set name (IANA registry, ISO 8859 family, …)
@ + modifier@euroA variant convention for the same language and place

Table 69.4

The whole grammar can be written like this.

language[_TERRITORY][.codeset][@modifier]

ko_KR.UTF-8      Korean, Republic of Korea, UTF-8
de_DE@euro       German, Germany, the euro variant
sr_RS@latin      Serbian written in the Latin script
C   or  POSIX    the minimal locale the standard fixes

What matters is that the language and the territory are codes borrowed from other standards. ko is the code ISO 639-1 gave Korean and KR is the one ISO 3166-1 gave the Republic of Korea. A locale name is not something C invented; it is an assembly of code systems that already existed.

Q. Which standard fixes this grammar?

A. Not the C standard. C fixes only "C" and "", and says the rest are implementation-defined strings (§7.11.1.1p3).

But one footnote points the way — “ISO/IEC 9945 specifies locale and charmap formats that can be used to specify locales for C.” ISO/IEC 9945 is POSIX (IEEE Std 1003.1). The language_TERRITORY.codeset@modifier grammar, the format of locale definition files, and the precedence of the environment variables are all fixed by POSIX.

So the naming rules of this chapter are the rules that hold on Unix-like systems. Windows uses another system, and the web uses a third — compared below.

68.5.1 Codeset names and normalisation

.UTF-8, .utf8, .UTF8 — all three name the same thing. glibc compares names ignoring case and -. So even when locale -a prints ko_KR.utf8, a program may ask for "ko_KR.UTF-8".

The codeset names themselves come from yet another registry. UTF-8, EUC-KR and ISO-8859-1 are registered in IANA’s character-set registry, and behind them stand the ISO/IEC 8859 family or Unicode (ISO/IEC 10646).

The codeset part often matters more than the rest of the name. ko_KR.UTF-8 and ko_KR.EUC-KR share a language and a territory but differ in how many bytes a character takes. In the demonstration their MB_CUR_MAX values split, 6 against 2.

In practice. One country, two encodings — the era of ko_KR.EUC-KR

Korean Unix environments of the 1990s and early 2000s defaulted to ko_KR.eucKR (or ko_KR.EUC-KR) — a world in which one Hangul syllable is two bytes. Code written then has the assumption “Hangul is two bytes” embedded everywhere: string lengths divided by two, cursors moved by two, truncation rounded to an even byte count.

Moving to UTF-8 broke all of it. Hangul became three bytes, and that code began cutting letters in half. This is what the encoding transition actually felt like in Korea, and it is also why the three layers of chapter 72 — bytes, code points, characters — have to be told apart.

68.5.2 Names in other worlds — BCP 47 and Windows

The same “Korean (Republic of Korea)” is named differently by each system.

SystemNotationBasis
POSIX and Cko_KR.UTF-8ISO/IEC 9945 (POSIX)
BCP 47 (web, XML, HTTP)ko-KRRFC 5646 (tags), RFC 4647 (matching)
Windows (Vista onwards)ko-KRFollows BCP 47
Windows (the old way)Korean_Korea.949Windows-specific
Unicode CLDRko_KRUTS #35 (LDML)

Table 69.5

BCP 47 is the internet standard — the tag in HTML’s lang="ko-KR" and in HTTP’s Accept-Language. It uses a hyphen instead of an underscore and has no codeset part, because the web handles encoding separately. When the script must be stated, an ISO 15924 code goes in the middle, as in sr-Latn-RS.

A program that spans both worlds must translate names. To pass a browser’s ko-KR to setlocale it has to become ko_KR.UTF-8, which means holding a mapping table somewhere. Mistakes are frequent at that seam.

Platform note. The modern source of locale data — CLDR

Who maintains the actual convention data — how a country writes dates, what its currency symbol is? Today’s answer is the Unicode Consortium’s CLDR (Common Locale Data Repository), in the format UTS #35 (LDML) fixes. ICU, Java, Android and browsers all take their data from there.

glibc’s locale definitions come from an older line — ISO/IEC TR 14652 (a specification method for cultural conventions) and ISO/IEC 15897 (procedures for registering cultural elements) — and the files themselves sit in /usr/share/i18n/locales/ in a human-readable form.

Since the two lines differ, the same locale may carry slightly different values. That is where a Java program and a C program printing the same date differently comes from.

68.6 The precedence of the environment variables

We said setlocale(LC_ALL, "") uses “the locale the environment says”. Exactly what environment — POSIX fixes the precedence.

RankVariableMeaning
1LC_ALLIf present, it overrides every category
2LC_CTYPE, LC_TIME, …Sets only that category
3LANGThe default for categories not set above

Table 69.6

So LANG=ko_KR.UTF-8 LC_NUMERIC=C ./program runs mostly with Korean conventions but with numbers in C conventions. The scripts that launch server programs pin LC_ALL=C for the same reason — to insulate logs and parsing from the environment.

$ locale                 # what the environment is setting right now
$ locale -a              # the locales installed on this machine
$ locale -k LC_NUMERIC   # the values of one category in detail

68.7 Where locale data lives

A locale is not inside the program. It has to be installed on the machine.

StepWhat
Definition file/usr/share/i18n/locales/ko_KR — human-readable
Charmap/usr/share/i18n/charmaps/UTF-8.gz
Compilelocaledef -i ko_KR -f UTF-8 ko_KR.UTF-8
Installed under/usr/lib/locale/ (or locale-archive)
Search pathChangeable with the LOCPATH environment variable

Table 69.7

Knowing this structure lets you solve the “missing locale” problem yourself. If a container has no ko_KR.UTF-8, put the definition file in and build it with localedef.

In practice. How this book checked its locale tables

The machine this book is built on also started with only C, C.UTF-8 and POSIX. So, to check the tables of the next chapter, the necessary locales were built from glibc 2.41′s definitions.

localedef -i ko_KR -f UTF-8 <path>/ko_KR.UTF-8
localedef -i de_DE -f UTF-8 <path>/de_DE.UTF-8
export LOCPATH=<path>

So the values from other locales printed in this book are not guesses; they are what came out of running it that way. Conversely, if the reader’s machine lacks those locales the examples print “not on this machine” — they are written so as not to assume any locale exists.

Recap

What to rememberThe point
What it isConventions set outside the program. Not translation
Starting valueAlways "C" — the standard says so
ScopeProcess-global. A library can change it; threads race over it
CategoriesLC_CTYPE, LC_NUMERIC, LC_MONETARY, LC_COLLATE, LC_TIME (+LC_ALL)
setlocale""=environment, NULL=query. Null return means failure
Nameslanguage_TERRITORY.codeset@modifier — ISO 639, ISO 3166, charset registry
BasisThe grammar and formats are POSIX (ISO/IEC 9945). The web uses BCP 47
EnvironmentLC_ALL > LC_category > LANG
In practiceLC_ALL, "" followed by LC_NUMERIC, "C"

Table 69.8

We have seen what a locale is and how one is chosen. The next chapter is what it changes and how — numbers, money, time and sorting, one at a time.