tohokuaikiのチラシの裏

技術的ネタとか。

Confluenceのプラグイン開発を承ります。ご連絡はこちらのホームページからお願いいたします。

PHPのsetlocaleまとめ

前2つのエントリでハマりまくったPHPのsetlocaleのまとめ。*1

Apache CLIの共通の認識

  • $locale -a を行って表示されないロケールは(つまりOSで有効化されてないロケール)setlocale()しても無効である。
  • ただし、言語パック(debianだとtask-japaneseパッケージなど)を入れる必要は無い。
  • LC_MESSAGESディレクトリを置くフォルダ名は、文字コードが必要ない。付けてはいけない。(×:ja_JP.UTF8 〇:ja_JP)
  • setlocale()第二引数で設定するロケールは、$locale -a の中にあるもので一致してなければならない。locale -a の中に、"ja_JP.utf8"があった場合"ja_JP"はN.G
  • setlocale()の第二引数内の「utf8」は、文字表記の揺れを認める。UTF8/UTF-8/utf8/utf-8/UtF-8などは全て許容される。

CLIの場合のsetlocale

  • CLIでのsetlocale()の第二引数は環境変数$LANGUAGEで上書きされる。$LANGUAGEが無い場合は、$LANGで上書きされる
  • この2つの環境変数が無い場合に第二引数の環境変数が評価される。

*1:そもそもロケールがen_USしか入ってないdebianだからっていうのでハマったのであるが

PHPのsetlocaleをCLIでチェックばかりしてたらハマった話。

まとめ

setlocale()の挙動について調べている際に、ApacheからではなくCLIから調べていた。

このCLIの場合の調査は下記の通り。

  • CLIからPHPを実行してsetlocaleする際は、OSのロケールに依存する。
  • OSの/etc/locale.genで有効にしてないロケールは、setlocale()で変更しても無視される。
  • というか、そもそも環境変数LANGUAGEによりsetlocale()が上書きされるのでsetlocale()にあまり意味は無い。

Debianロケールを追加する

英語・日本語の.moを用意し、setlocale()する最初のスクリプトを書いたところ、CentOSでは問題なくそれぞれの言語カタログが当たったが、debianでは上手くいかない。英語の方を見に行ってしまう。

で、gettextが欲しければ素直に追加しろということで、root権限で追加した。

/etc/locale.gen でja_JPを追加。

en_US.UTF-8 UTF-8
ja_JP.UTF-8 UTF-8

最終行に追加。

locale-genコマンドでシステムに反映

# locale-gen
Generating locales (this might take a while)...
  en_US.UTF-8... done
  ja_JP.UTF-8... done
Generation complete.

すると、$locale -a で

$ locale -a
C
C.UTF-8
en_US.utf8
ja_JP.utf8
POSIX

となった。

しかし、先ほどの空くプロとでgettext()で反映させようとしても出てこない。

ちなみに、en_US.utf8というのは、en_US.UTF8でもen_US.UTF-8でもen_US.utf-8でも良いみたい。

パッケージセットから追加する。

task-japaneseというパッケージらしい。しかし、これを普通にapt-get installしたところでlocaleが変わるのか・・・?

# apt-cache search task-japanese
task-japanese - Japanese environment
task-japanese-desktop - Japanese desktop
task-japanese-gnome-desktop - Japanese GNOME desktop
task-japanese-kde-desktop - Japanese KDE desktop

まだインストールしてないのを確認

# dpkg -l task-japanese
dpkg-query: no packages found matching task-japanese

Debianの公式Wikiを調べて ChangeLanguage - Debian Wiki

# dpkg-reconfigure locales

を行う。

f:id:tohokuaiki:20170314170055p:plain

ここでチェックが入っているのは、/etc/locale.genで有効化されている言語。

ja_JP.UTF-8にチェックを入れて、進む。

デフォルトのLocaleはLocale - Debian Wikiによると「None」でイイらしい。

Get root and type dpkg-reconfigure locales and select the locale(s) you want to generate. At the end, you'll be asked which one should be the default. If you have users who access the system through ssh, it is recommended that you choose None as your default locale.

さて・・・・。

# dpkg -l task-japanese
dpkg-query: no packages found matching task-japanese

あれ?パッケージがインストールされてない。

仕方ないので、手動でインストール

# apt-get install -y task-japanese

しかし、まだgettext()はja_JPの方を見に行かない。

んー、デフォルトのLanguageに引っ張られてるのか?

# env |grep LANG
LANG=en_US.UTF-8
LANGUAGE=en_US:en

デフォルトLanguageを日本語にする。

# update-locale LANG=ja_JP.UTF-8 LANGUAGE=ja_JP:ja
$ more /etc/default/locale
#  File generated by update-locale
LANG=ja_JP.UTF-8
LANGUAGE=ja_JP:ja

で、・・・・あ、gettext()で日本語が出た。確かにgettext()がデフォルト設定のロケールに引っ張られている。・・・ということは今度は日本語に切り替えようとしても切り替わらない・・・。困ったな。

色々調べてたら、

$export LANGUAGE="en_US"
$php checmMO.php

というように、環境変数LANGUAGEの値に影響がされていた。

しかも、これはコンソールからCLIPHPを動作させた時であって、Apacheから見た時はこの設定に影響されず、ちゃんと切り替わっていた。

PHPでgettextする際の注意事項というか、setlocaleの罠

PHPでgettextを使って国際化しようとかいうとだいたいこういう記事がヒットする。

<?php
// 日本-日本語にロケールをセットして
setlocale(LC_ALL, 'ja_JP'); 
// 翻訳カタログが入ったmessage.moファイルのエンコードをUTF-8と宣言して
bind_textdomain_codeset('message', 'UTF-8');
// message.moのディレクトリを指定して
bindtextdomain('message', dirname(__FILE__));
// gettext()がコールされた際に、message.moからカタログを検索するようにして
textdomain('message');
// 翻訳言語を出す
echo gettext('hoge');

罠1:message.moのディレクトリは指定したディレクトリ+ロケール名+LC_MESSAGESである。

bindtextdomain()で、/path/to/language/catalog/directoryを設定したら、実際に.moファイルを置くのは

/path/to/language/catalog/directory/ja_JP/LC_MESSAGES

である。

まぁ、これはちょっとよくハマリがちな罠。

罠2:Linux系のOSだとシステムに入ってないロケールには切り替えられない

これにハマった。setlocale効かない。

RHEL/CentOS系だとロケールは何でもガンガンと入れてくれるらしい。

$ locale -a
C
POSIX
aa_DJ
aa_DJ.iso88591
aa_DJ.utf8
...
en_SG.utf8
en_US
en_US.iso88591
en_US.iso885915
en_US.utf8
...
ja_JP
ja_JP.eucjp
ja_JP.ujis
ja_JP.utf8
japanese
japanese.euc
...
zu_ZA
zu_ZA.iso88591
zu_ZA.utf8

と500近く出てくるが、debianだと

$ locale -a
C
C.UTF-8
en_US.utf8
POSIX

これだけになる。

setlocale()が成功したかどうかをチェックしなければならない。

返り値をチェックして、設定した言語とstrcasecmp()===0になれば成功。

debianロケールを追加しないままだとこうなる。

$ php -r 'var_dump(setlocale(LC_ALL, "en_US.UTF-8"));'
string(11) "en_US.UTF-8"
$ php -r 'var_dump(setlocale(LC_ALL, "en_US"));'
bool(false)
$ php -r 'var_dump(setlocale(LC_ALL, "ja_JP.UTF-8"));'
bool(false)
$ php -r 'var_dump(setlocale(LC_ALL, "ja_JP"));'
bool(false)

Windowsの罠

Windowsは知らん。もっとややこしいらしい。

PHP: setlocale - Manual

注意:
Windows では、setlocale(LC_ALL, '') を使用するとシステムの地域と言語の設定の値を使用します (コントロールパネルで確認できます)。

自体に色々と罠が潜んでいるとマニュアルに書いてあった。

たとえば

$ php -r 'var_dump(setlocale(LC_ALL, ""));'
string(18) "Japanese_Japan.932"

などというものを返す。

$ php -r 'var_dump(setlocale(LC_ALL, "en_US"));'
bool(false)

en_USすらダメ。ほとんどsetlocaleを使うなという気もする。あるいは、完全にWindowsのみでコードと.moの設置場所を書くかしかない。

罠3 Apacheを再起動せよ

なんか、setlocaleが成功したり失敗したりする。。。。

検索したらPHPドキュメントのコメントが・・・。 PHP: setlocale - Manual

Omer Sabic

On Linux/Apache, when you install and try to use a new locale, the setlocale() function with the new locale will fail sometimes, but not always. To furthermore complicate, setlocale() will always complete with any of the previously installed locales. This would seem a really weird behaviour, which you can fix by restarting Apache, as Kari Sderholm aka Haprog mentioned, but I felt it needed to be properly pointed out.

Apacheを再起動せよと・・・・。

確かに治った。

でまぁ、そんな理由は…

ロケールに関する設定は、OSのロケール機能を使っているからということになる。PHP側でも実装するのダルイもんね。

ただ、ロケールに影響を受けるPHP関数は多々あるようで、それはそれで問題だとは思うけどとりあえずgettextが使えればいいので今のところはスルーする。時間表記とか気になるのだけれども、それはその時に下記のエントリを読んで考える。 d.hatena.ne.jp

ポータビリティの高いgettextを使うための対応方法

setlocaleしないで自前でロケールの切り替えを行う。一番簡単なのは、ドメイン名にロケール名称を入れてしまうこと。

また、PHPのgettextモジュールが入ってない場合もあるかもしれないが、その場合も考慮したPHPのgettextライブラリがある。

名称・リンク リリース(初版 - 最新) ライセンス 備考
php-gettext 2003/10/23 - 2015/11/11 GPL WordPressもこれを使っている
PEARのFile_Gettext 2004/03/30 - 2012/03/04 BSD 4年半の沈黙を破り、現時点のリリースはびびる
oscarotero/Gettext 2013/12/01 - 2017/03/05 MIT composerの名称がgettext/gettextという意欲作。Bladeからpotを作る機能もある。PHP5.4以上

oscarotero/Gettextを使いたいなーと思うのだけど、意外と依存性が高いのとPHP5.3は使えないみたいなので若干敷居が高い。

phpMyAdminも独自でgettextを持っていて、CVE - CVE-2015-8980なんてあげられてたりする。

PEAR File_Gettextを使ったサンプル

<?php
require_once 'File/Gettext.php';

function __($str)
{
    static $mo;
    
    if (is_null($mo)){
        $mo = File_Gettext::factory('MO');
        $mo->load('locale/ja_JP/LC_MESSAGES/message.mo');
    }
    
    $catalog = $mo->toArray();
    if (isset($catalog['strings'][$str])){
        return $catalog['strings'][$str];
    }
    return $str;
}


echo __('email');

こんな感じかな。gettextの機能を全く使わないでいるので、複数形とか来た時にイマイチな感じがする。

ただし、PHPのgettextモジュールが組み込まれてなくても使えるのでポータビリティとしてはかなり高い。

PHPのgettextは使うんだけど、OSにロケールが無い場合にドメインでなんとかしたい。

今回の自分のケース。

gettextは現在のロケールドメインによってカタログを見に行くので、どのディレクトリでも対応するなら、Cロケールを使ってその中に各言語ごとのファイルを用意しておけばいいんじゃん?っておもったのだけど、

gettext(3C) (SunOS リファレンスマニュアル (3) : 基本ライブラリ関数)

現在のロケールが C ロケールの場合、gettext()、gettext()、dcgettext() は、渡されたメッセージ文字列をそのまま返します。

ということで、ダメみたい。

jQuery-UI のautocompleteで候補が出てくるメッセージがうっとうしいので出さなくしたい。

なんかjQuery-UIのautocomplete使うとこんなメッセージ出るんですよね。

f:id:tohokuaiki:20170301192540p:plain

ヘルパメッセージらしいのだけど、面倒なので出したくない。

autocompleteの際のオプションで、

                messages: {
                  noResults: '',
                  results: function() {}
                },
                focus: function (event, ui) {
                    $(".ui-helper-hidden-accessible").hide();
                    event.preventDefault();
                }

ってやってやると出ない。

stackoverflow.com

stackoverflow.com

追記

なんか、挙動がおかしくなる。

やっぱり

.ui-helper-hidden-accessible{
    display: none;
}

が良いみたい。

MySQLのForeignキー指定時の自動命名則

今更だけど、MySQLのForeignキーの自動命名則。

たとえば、こんなテーブルを作った時。

CREATE TABLE `author_author_type` ( 
  `id` int (10) unsigned NOT NULL AUTO_INCREMENT,
  `author_id` int (10) unsigned NOT NULL,
  `author_type_id` int (10) unsigned NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`),
  FOREIGN KEY (`author_type_id`) REFERENCES `author_types` (`id`)
) ENGINE = InnoDB; 

一旦作って、show create tableすると

CREATE TABLE `author_author_type` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `author_id` int(10) unsigned NOT NULL,
  `author_type_id` int(10) unsigned NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  KEY `author_id` (`author_id`),
  KEY `author_type_id` (`author_type_id`),
  CONSTRAINT `author_author_type_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`),
  CONSTRAINT `author_author_type_ibfk_2` FOREIGN KEY (`author_type_id`) REFERENCES `author_types` (`id`)
) ENGINE=InnoDB;

というようになる。

自動で作成されるもの

INDEXが張られる。

自前のフィールド名でINDEX名が付けられる。

CONSTRAINTが付けられる

FOREIGNキーを作った順番にibfk_1,ibfk_2…というように自動的に制約名が付けられる。

この制約名とかINDEX名がダサイという場合は、きちんと自分で指定しろってことですね。