識別子の大文字・小文字変換はロケールに依存させない(トルコ語の I 問題)
トルコ語・アゼルバイジャン語では I の小文字が点のない ı、i の大文字が点付きの İ になる。ロケール依存の変換を使うと "I18N" が "ı18n" になり、キーやコマンド名の比較が壊れる。
どこで問題になるか
HTTP ヘッダー名、設定キー、enum 名、ファイル拡張子、コマンド名など「人間の言語ではない文字列」を大文字小文字無視で比べるとき。サーバーやユーザー環境のロケールがトルコ語だと、"TITLE".lower() == "title" のつもりの比較が一致しなくなる。
言語ごとの書き方
| 言語 | ロケール非依存(識別子向け) | ロケール依存(表示用の文章向け) |
|---|---|---|
| JavaScript | s.toLowerCase()(実行環境のロケールに左右されない) | s.toLocaleLowerCase('tr') → "I18N" が "ı18n" |
| Java / Kotlin | s.toLowerCase(Locale.ROOT) | 引数なしの s.toLowerCase() は既定ロケールを使うので要注意 |
| C# | s.ToLowerInvariant()、比較は StringComparison.OrdinalIgnoreCase | s.ToLower()(現在のカルチャ) |
| Go | strings.ToLower(s)、比較は strings.EqualFold(a, b) | strings.ToLowerSpecial(unicode.TurkishCase, s) |
| Python | s.lower()、比較は s.casefold() | 標準ではロケール依存の変換はしない |
押さえておくこと
- 識別子は ロケール非依存の変換か、大文字小文字を無視する比較関数(
EqualFold、OrdinalIgnoreCase)を使う。表示用の文章だけロケールを指定する。 - ロケール非依存でも
"İ".toLowerCase()はi+結合用の点(U+0307)の2文字になる。変換後の長さが変わることがあるので、長さやインデックスを前提にしない。 - Java で引数なしの
toLowerCase()/toUpperCase()/String.formatを使っている箇所は、静的解析(Error Prone など)で検出できる。
出典
- @mattn_jp · 2026-10-13 (i18n にまつわる落とし穴を並べた投稿への引用)