'static + Life

運良く生きています

macOS のロケールについて調べてみる

この記事ははてなエンジニア Advent Calendar 2025 7 日目の記事です。

背景

半年ぐらい前に、id:onk さんの以下のエントリが話題になってました。

onk.hatenablog.jp

ここで以下の言及があります:

ここまで見て「面白い〜」と社の Slack で共有したところ、id:KashEight から「man setlocale が参考になりそう?」という反応を貰った。

私です。ということで、ここらへん反応しながら面倒くさくてずっと表に出していなかったので出していきます*1

そもそもロケールとは

まず、ロケールそのものの話をしましょう。

我々が想像するロケールがどのようなものがあるでしょうか。 元のエントリでは caldate がおかしい、つまりは日時がおかしいことが述べられています。 日時のほかには場所や言語、文字順序あるいはそれらをまとめたセット等が列挙できるでしょう。 実際、ja_JP.UTF-8 を例としたパスからもなんやらそういう要素があるのは推測できるかと思います。

となると、このロケールという情報をソフトウェア開発者側で一から実装するのは大変そうですね。 OS 側でよしなに用意してくれるのがまだ現実的な気がします。

ロケールPOSIX

POSIX では既にロケールについて仕様を定めております。

実際に POSIX.1-2024 (IEEE Std 1003.1-2024, The Open Group Standard Base Specifications, Issue 8) ではロケールについて以下のように書かれています:

A locale is the definition of the subset of a user's environment that depends on language and cultural conventions. It is made up from one or more categories. Each category is identified by its name and controls specific aspects of the behavior of components of the system.

ref: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap07.html

そして POSIX では上記のカテゴリに対応する以下 6 つの環境変数を定めています*2:

  • LC_CTYPE
  • LC_COLLATE
  • LC_MONETARY
  • LC_NUMERIC
  • LC_TIME
  • LC_MESSAGES

上記の環境変数ロケールを設定したり、libc にある setlocale などの関数を通してソフトウェア側もロケールを設定したりできるようになります。 現在の環境変数で使用されているロケールをソフトウェア側で適用したい場合とかに setlocale(LC_ALL, "") を使用するコードとかはその代表的な例でしょう。

libc の実装例

glibc

glibc では /locale 以下に実装、/localedata 以下にロケールに関するデータがあります。

ロケールデータは既に LC_* の形になっているわけではなく、一度 make によるビルドを通す必要があります。

なお、glibc には独自拡張として LC_ADDRESS のような追加の環境変数があります*3

musl libc

musl libc では /src/locale 以下に実装があります。

https://git.musl-libc.org/cgit/musl/tree/src/locale

ロケールデータは調べた感じ用意されている感じではなさそうです。 また、musl libc を使用している Linux ディストリビューションとして有名であろう Alpine LinuxWiki を見ると以下の記述が一発目に出てくるので、ロケールサポートはそんな期待するものではないかもです。

Musl does not implement most of the locale features that glibc implements.

ref: https://wiki.alpinelinux.org/wiki/Locale

macOS におけるロケール

さて、本題の macOSロケールについて探ってみます。 まず、ロケール関係のコマンドや C API 周りのマニュアルをいくつか見てみましょう*4

$ man 1 localedef
(省略)
HISTORY
      localedef first appeared in FreeBSD 11.

      It was written by Garrett D'Amore <garrett@nexenta.com> for illumos.  John Marino <draco@marino.st> provided the alternations necessary to compile cleanly on DragonFly.  Baptiste Daroussin <bapt@FreeBSD.org> ported it to FreeBSD and converted it to tree(3).
$ man 1 locale
(省略)
HISTORY
      locale appeared in Mac OS X 10.4
$ man 3 setlocale
(省略)
HISTORY
      The setlocale() function first appeared in 4.4BSD.

macOSBSD 系統というなのを実感しますが、FreeBSDMac OS X、4.4BSD と色々別のものが出てきてます。 全部独自実装しているわけではなく、他の実装を使ってそうな雰囲気を感じます。

libc を見る

というわけで、macOS の libc 実装を見てみます。 というのも、以下のように Appleソースコードを公開しているので確認することが可能です*5

opensource.apple.com

このうち、libc のソースコードは以下で見れます:

https://github.com/apple-oss-distributions/Libc

OS のバージョンごとによって libc のバージョンが異なりますが、今回は Sequoia 15 で一番最新である Libc-1698.140.3 をベースに見てみます*6

libc のうちロケールに関する実装は以下となります:

https://github.com/apple-oss-distributions/Libc/tree/Libc-1698.140.3/locale

見る感じ、FreeBSD というディレクトリが確認できますね。ちょっと確認してみると、setlocale(3) の実装もここに含まれていそうです。

なお、FreeBSD での実装は以下となります:

https://github.com/freebsd/freebsd-src/blob/579bb6c2cd77138854d0eb81acdd8494ed329410/lib/libc/locale/setlocale.c

比べて見てみると、macOS の実装は差分*7はあれどもけっこう構造は似ている感じがあります。

ではロケールデータ (今回は問題となった LC_TIME) を探してみます…が、ここにはなさそうです。 FreeBSD では、/share/timedef 以下に LC_TIME の元データと生成用の Makefile があり、 make コマンドだけで作成できるようになっていそうです:

https://github.com/freebsd/freebsd-src/tree/579bb6c2cd77138854d0eb81acdd8494ed329410/share/timedef/Makefile

構造は glibc とは違いますが、作成手順は似ていますね。

ちなみに macOS にてデフォルトで実行できる make は GNU make*8 なので、該当の Makefile を実行してもビルドが失敗します。 もしビルドしたいなら BSD make*9 を入れてビルドしましょう*10

余談ですが、macOS の libc 全体を見てみると、NetBSD だったり OpenBSD なりの名前があるディレクトリがあって面白いです。

コマンドを見る

libc と離れて少しコマンドの方を見てみます。

ロケールそのものを調べたり作成したりする localelocaledef は adv_cmds、元のエントリであった cal (ncal) は misc_cmds、同じく date は shell_cmds にそれぞれあります。

マニュアルでも確認できますが locale 以外はどれも FreeBSD の実装がもとになっていそうです。 特に localedef に関しては、illumos → FreeBSDmacOS の変遷を辿っていそうなのは興味深いですね*11

結論

もっと掘ってもいいんですが、ロケールそのものの話とは離れそうなのと時間もかかりそうなので一旦ここで区切って結論を出してみます。

  • macOS ではロケール周りの libc 実装は FreeBSD の実装がもとになっていそう。
  • 一方で、ロケールデータは (自分が確認した範囲) ではここにあるというのはなさそう。
  • コマンド周りでも FreeBSD 実装のものは多そう。

ここでの結論は以上な感じでしょうか。ちょっとそこまでまとまっている感じはしませんが…。

余談

元々は CJK 文字における曖昧幅*12に関して調べていて、今回書いている話はそのときの副産物だったりします。

libc での wcwidth(3) の実装を調べるために libc を調べたり、自分で LC_CTYPE を作ったり*13してたときにここらへんのロケールについても調べていました。 たまたま元のエントリにあった問題をみた瞬間、自分が調べてたことじゃん!ということで反応したというのが経緯だったりします。

なお、今回出てくる locale コマンド使って今回のロケールについて詳しく調べてみると韓国語ロケールがぶっ壊れていることが改めて確認できます*14:

$ LC_TIME="ja_JP.UTF-8" locale -ck LC_TIME
LC_TIME
ab_day="日";"月";"火";"水";"木";"金";"土"
abday="日";"月";"火";"水";"木";"金";"土"
day="日曜日";"月曜日";"火曜日";"水曜日";"木曜日";"金曜日";"土曜日"
abmon=" 1";" 2";" 3";" 4";" 5";" 6";" 7";" 8";" 9";"10";"11";"12"
mon="1月";"2月";"3月";"4月";"5月";"6月";"7月";"8月";"9月";"10月";"11月";"12月"
am_pm="午前";"午後"
t_fmt_ampm="%I:%M:%S %p"
era=""
era_d_fmt=""
era_t_fmt=""
era_d_t_fmt=""
alt_digits=""
d_t_fmt="%a %b/%e %T %Y"
d_fmt="%Y/%m/%d"
t_fmt="%H時%M分%S秒"
$ LC_TIME="ko_KR.UTF-8" locale -ck LC_TIME
LC_TIME
ab_day="일";"월";"화";"수";"목";"금";"토"
abday="일";"월";"화";"수";"목";"금";"토"
day="일요일";"월요일";"화요일";"수요일";"목요일";"금요일";"토요일"
abmon=" 1";" 2";" 3";" 4";" 5";" 6";" 7";" 8";" 9";"10";"11";"12"
mon="1월";"2월";"3월";"4월";"5월";"6월";"7월";"8월";"9월";"10월";"11월";"12월"
am_pm="#오전";"AM"
t_fmt_ampm="12월"
era=""
era_d_fmt=""
era_t_fmt=""
era_d_t_fmt=""
alt_digits=""
d_t_fmt="%a %b/%e %H:%M:%S %Y"
d_fmt="%Y/%m/%d"
t_fmt="%H시 %M분 %S초"

*1:一応 8 月ぐらいに社内の勉強会で出したりはしました

*2:ここに書かれていない LANG や LC_ALL はこれら全体を一括で設定する役割があります、二つの違いは優先度だけで、LANG は優先度が一番低く、LC_* が設定されているとそれが優先されます、LC_ALL はその逆

*3:locale(7) - Linux manual page

*4:macOS Sequoia 15.7.1 で実行

*5:基本的には Apple Public Source License Version 2.0 での公開になりますが、bash など一部のものは別のライセンスになっています

*6:これはページだと macOS 15.6 で、今の環境で実行している macOS 15.7.1 の一つ前となるんですが、なぜか macOS 15.7 がないのでこのバージョンで行なっています

*7:macOS 側にある xlocale 周りの追加処理など

*8:実際に man 1 make したら GNU make の説明が出てきます

*9:homebrew を使うなら brew install bsdmake で導入可能

*10:といっても自分の環境でビルドしても失敗しましたが…

*11:これに関しては FreeBSDREADME にも書かれていることが確認できます

*12:いわゆる East Asian Ambiguous Width 問題

*13:https://github.com/KashEight/locale-eaw-bsd

*14:am_pm と t_fmt_ampm が明らかにおかしい