テック技報最新号へ →
コード

識別子の大文字・小文字変換はロケールに依存させない(トルコ語の I 問題)

トルコ語・アゼルバイジャン語では I の小文字が点のない ı、i の大文字が点付きの İ になる。ロケール依存の変換を使うと "I18N" が "ı18n" になり、キーやコマンド名の比較が壊れる。

@mattn_jp · 投稿 2026-10-13
#i18n#unicode#javascript#java#go#落とし穴

どこで問題になるか

HTTP ヘッダー名、設定キー、enum 名、ファイル拡張子、コマンド名など「人間の言語ではない文字列」を大文字小文字無視で比べるとき。サーバーやユーザー環境のロケールがトルコ語だと、"TITLE".lower() == "title" のつもりの比較が一致しなくなる。

言語ごとの書き方

言語ロケール非依存(識別子向け)ロケール依存(表示用の文章向け)
JavaScripts.toLowerCase()(実行環境のロケールに左右されない)s.toLocaleLowerCase('tr') → "I18N" が "ı18n"
Java / Kotlins.toLowerCase(Locale.ROOT)引数なしの s.toLowerCase() は既定ロケールを使うので要注意
C#s.ToLowerInvariant()、比較は StringComparison.OrdinalIgnoreCases.ToLower()(現在のカルチャ)
Gostrings.ToLower(s)、比較は strings.EqualFold(a, b)strings.ToLowerSpecial(unicode.TurkishCase, s)
Pythons.lower()、比較は s.casefold()標準ではロケール依存の変換はしない

押さえておくこと

  • 識別子は ロケール非依存の変換か、大文字小文字を無視する比較関数(EqualFold、OrdinalIgnoreCase)を使う。表示用の文章だけロケールを指定する。
  • ロケール非依存でも "İ".toLowerCase() は i +結合用の点(U+0307)の2文字になる。変換後の長さが変わることがあるので、長さやインデックスを前提にしない。
  • Java で引数なしの toLowerCase() / toUpperCase() / String.format を使っている箇所は、静的解析(Error Prone など)で検出できる。

出典

X の元の投稿を開く