2008年12月21日日曜日

Cocoa(iPhone)で、日本語を含むURLを開く方法

  • 基本的にはCore FoundationのC関数を利用する
    CFURLCreateStringByAddingPercentEscapes()
  • ただし、一部問題のあるケースがある
    URL中に&を含む場合などは正しく作れないので別の方法が必要

プログラマをやっていると、だんだんと日本語が嫌いになってきます。
いや、嫌いというのはおかしいのですが、とにかく英語以外の言語はトラブルが多いです。
コンピューターというのはつくづく英語を処理するためだけに作られているのだと思います。
(だからこそ、プログラムの「国際化」で飯を食える人がいる訳ですけど!)

すみません、前置きが愚痴っぽくなってしまいました。
iPhoneプログラミングにおいてももちろん、日本語を使う際に罠があります。
たとえばNSURLRequestなどがそうです。
日本語の含まれるURLを開こうとすると、うまく解釈してくれないんです。
(といいますか日本語が含まれるURLはURLエンコードしなくちゃいけないんです。・・・当たり前ですね)

例えばこんな感じです:
http://s3.amazonaws.com/twitter_production/profile_images/65140989/るーみゃ_normal.jpeg


自分の画像だけ出ないぞ!

そこでURLエンコーディングを行う関数を探してみたところ、ありました。
Cocoaで日本語のGETを飛ばそう!

早速真似してやってみました。
CFURLCreateStringByAddingPercentEscapes()関数を使うのがポイントらしいです。
            NSString *originalUrl = [userDict objectForKey:@"profile_image_url"];
           NSString *encodedUrl = (NSString *) CFURLCreateStringByAddingPercentEscapes
           (NULL, (CFStringRef) originalUrl, NULL, NULL, kCFStringEncodingUTF8);
           person.profile_image_url = encodedUrl;

たったのこれだけでうまくいきました!



ところが、有頂天になってTwitter上で騒いでたら、
@psychsさんから突っ込みが。
JavaScriptでいうencodeURI相当なんで、
日本を適当にエスケープしたいときにはいいんだけど、
たとえばパラメタに「&」を入れたい場合とかは、
ちゃんとencodeURIComponent 相当のやつを使って組み立てなきゃだめだよ

なんと具体的なアドバイス!(ありがとうございます!)ちょっと調べてみました。
javascript: escape(), encodeURI(), encodeURIComponent() 比較

細かいところはわかりませんが、とにかく一部の文字がエンコーディングされないみたいなのです。
深刻なのは?とか=とかでしょうか。気をつけないとトラブルに巻き込まれそうですね。

ちなみにiPhoneでencodeURIComponent相当のエンコーディングを行うためにはどうすればよいのかですが、
リファレンスを引いてみても特にそれらしきものは見当たりませんでした。
ひょっとしたら自分でやるしかないかもしれません。

■2008/12/29 22:00追記
Twitter上で、@norio_nomuraさんにencodeURIComponent相当のエンコードを行う方法を教えていただきました!ありがとうございました!
http://twitter.com/norio_nomura/status/1083641557
CFURLCreateStringByAddingPercentEscapes(NULL, string, NULL, CFSTR (";,/?:@&=+$#"), kCFStringEncodingUTF8);

最大のポイントは、第4引数のCFSTRです。ここで指定した文字列はエンコードされずにそのまま残るようです。
第4引数をNULLのまま使うと標準のencodeURL相当になり、
この例のように指定すると、encodeURLComponent相当のエンコードが可能になります!

iPhoneアプリをローカライズ(国際化対応)してみた

  • まずはNSLocalizedString()を利用してアプリを作る
  • 次にgenstringsコマンドを利用してLocalizable.stringファイルを自動生成する
  • Xcodeに取り込む
  • Xcodeからローカライズの設定を行う
  • 最後にかっこよく翻訳を行う
  • info.plistファイルの中身をローカライズしたい(たとえばアプリ名など)ときは、
  • InfoPlist.stringsファイルを手で作って、ローカライズ設定をして、翻訳すると良い
  • Localizable.stringは基本UTF-16に統一しておくこと
  • InfoPlist.stringsはUTF-16でないと動かない。Localizable.stringsはUTF-8でいいらしい(未確認)

iPhoneアプリを作ったならば、
「一人でも多くの人に使ってもらいたい」
「1本でも多く売りたい」
というのが心情というものです。
日本ではヒットしないアプリが海外では大ブレイクということも考えられますし、
iPhoneアプリの市場は日本国内よりも海外のほうが圧倒的に大きいです。
従って、iPhoneでアプリを作るなら、国際化対応は必須であるといえましょう!

ということで、今日はそんな大事な大事な国際化のやり方を勉強してみました。
Cocoa Frameworkは元々非常に国際化がしやすい作りになっていますので、
iPhoneアプリでもその仕組みの恩恵を受けることが出来ます!




国際化の第1歩はアプリ作成からです。まずはアプリを作らないと始まりません。
このとき、普段は画面に表示する文字列を
self.navigationItem.title = @"Title";

こんな感じで指定していると思いますが、
これをNSLocalizedString()関数を利用して
self.navigationItem.title = NSLocalizedString(@"Title",
@"Title message for the main view");

こんな風に書き直します。
第1引数はメッセージキー(普通は英語の文字列をそのまま使えばいいと思います)、
第2引数はコメントです。コメントは面倒でしたらnilでも大丈夫ですが、あると翻訳時に大変便利です。


アプリ側の準備が出来たら、次は翻訳文字列ファイル(Localizable.strings)を生成します。
手で作っても良いのですが、genstringsという大変便利なコマンドが最初から用意されているので、
こちらを利用するのが良いと思います。
使い方はこんな感じです。
genstrings [オプション] ファイル名

たとえば
genstrings *.m

コレを実行すると、カレントディレクトリにあるすべての拡張子がmのファイルから
自動的にLocalizable.stringsをつくってカレントディレクトリに保存してくれます。
生成されたLocalizable.stringsはUTF-16でエンコードされています。
(BOMのありなし、およびエンディアンはリトルかビッグかについては不明ですが、
とにかくUTF-16としてMac, Xcode, およびiPhoneが正しく認識してくれるのは間違いないです)

これだけだとプロジェクト内のすべてのソースをまとめてファイルにしてくれないので、
実際の開発時にはfindコマンドを併用して、以下のように実行するのがおすすめです。
genstrings -a $(find . -name "*.m") 

このコマンド入力によって!
カレントディレクトリ以下のすべてのの拡張子がmのファイルを対象にしてLocalizable.stringsを生成してくれる
ソースコードが更新されていた場合、以前に作成したLocalizable.stringsの内容に新しく追加された文字列が追加される
(-aオプションがないとファイルは上書き保存されてしまいます)


Localizable.stringsが生成されたら、Xcodeに取り込みます。
単純にファイルをXcodeプロジェクトに対してドラッグ&ドロップすれば大丈夫です。
このとき、必ず文字コードを「UTF-16」にしてください。
取り込み時にUTF-16にするのを忘れていたら、あとからファイルのエンコードをUTF-16にする必要があります。


取り込んだら、ローカライズの設定を行います。


まずは対象のファイルを選択して右クリック、「情報を見る」ウィンドウを開きます。
「ファイルをローカライズ可能にする」をクリック。


再度、「情報を見る」ウィンドウを開き直し、
「ローカリゼーションの追加」をクリック。プルダウンからJapaneseを選択。
ドイツ語とかフランス語もプルダウンから簡単に選択できます。
その他の言語の場合は自分で調べる必要がありそうです。


成功するとこんな感じになります。

解除するときは「情報を見る」ウィンドウの一般タブから、
「すべてのローカライズを取り除く」をクリックするだけです。

以上、とても簡単です。
ただしこのローカライズの設定時に文字コードの指定がUTF-16ではなくなることがあるので、
もう一度設定したファイルの文字コードをチェックしておくと安心です。


これで準備は出来たので、あとはLocalizable.stringsを翻訳するだけです。
翻訳したらビルドしてiPhoneにインストールすれば完成!


Localizable.stringsでは翻訳できない箇所、たとえばアプリ名を翻訳するときなど、
Info.plistの内容を翻訳したいときは、InfoPlist.stringsと言うファイルをXcode上で作って、
ローカライズ設定を行い、翻訳してビルドすればうまくいくと思います。


Xibファイルについてもローカライズ設定が可能なようなのですが、
Xib自体を翻訳するより、ソースコード中からラベルやタイトル名を変更するようにして、
ソースコード自体をLocalizable.stringsで翻訳するようにしたほうが変更に強くて楽かと思います。




トラブルシューティング
以上の内容に従っても翻訳されない!というときは、
まずはLocalizable.stringsが本当にUTF-16になっているか確認してみましょう。
InfoPlist.stringsはUTF-16でないと動きません。
Localizable.stringsについてはUTF-8でも動くそうなのですが、私の場合駄目だったので、
やはりUTF-16をオススメしておきます。
ただし、いずれの場合でも、UTF-8とUTF-16が混在すると確実に動きません。

それでも駄目な場合は、Finderでプロジェクトのディレクトリを開き、
buildディレクトリの中身を丸ごと消してから再度ビルドするとうまくいくと思います。
(以前のビルド結果が残って居るとうまくいかないみたいです)


それでは素敵な翻訳ライフを!

2008年12月20日土曜日

NSURLConnection使用中にbad server certificationエラーが出たときの対処法

  • 1203, NSURLErrorDomain, bad server certificate
  • このエラーはSSL使用時に証明書の内容が不正なときに発生する
  • 要するに自己認証証明書(オレオレ証明書)警告
  • 標準APIにはこのオレオレ証明書警告をオフにする機能がない
  • NSURLRequestのallowsAnyHTTPSCertificateForHost:をオーバーライドすることで回避可能

皆さんも、自宅に自分用のサーバーをお持ちだったり、会社のサーバーに業務でアクセスしたりということがあると思います。
こういうちょっとしたサーバーでは、たいていの場合正式な認証局が発行したものではない、自己認証による証明書、
いわゆる「オレオレ証明書」によるSSH認証が行われています。

便利だし、正式な認証局に頼むとお金が必要になったりでついついやってしまいますよね。
ブラウザからアクセスすると警告が出ますが、無視してしまえばなんてことはありません。

ところが。
iPhoneのアプリからこうした「オレオレ証明書」を利用しているサーバーに対して、
NSURLConnectクラスを用いてアクセスしようとすると
1203, NSURLErrorDomain, bad server certificate

というエラー(NSErrorのインスタンス)が発生し、処理が中断されてしまいます。
Appleさんちょっと厳しいですって。
(たとえば、Pythonでliburlを利用してアクセスしたときはたとえオレオレ証明書でも一切怒られません)
しかも困ったことに、このエラーを回避する方法が標準APIに用意されていません。
対策はただひとつ、「オレオレ証明書なんて使うな、Verisignにお金払え」ということらしいです。

オレオレ証明書なんて許さないぞという決意は大変良く分かりますが、
自宅のサーバーならともかく、会社のサーバーでは自分が勝手に証明書取るわけにもいかず。
困りました。

そこで先人たちがNSURLRequestクラスのprivateなメソッドを利用する回避手段を編み出してくださいました。
http://lists.apple.com/archives/Macnetworkprog/2006/Nov/msg00020.html
http://www.phapper.com/Default.aspx?g=posts&m=8


この方法に従って、NSURLConnectionクラス(およびNSURLRequestクラス)を利用する箇所で、以下のようなクラスカテゴリ実装を行います。
@implementation NSURLRequest(NSHTTPURLRequest)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
 return YES; // Or whatever logic
}
@end

これで全てのオレオレ証明書の認証を回避することが出来ます。
もし特定のホストのみを回避したいのであれば、以下のように適当なロジックを組んでやればいいと思います。
@implementation NSURLRequest(NSHTTPURLRequest)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host
{
 return [@"ba-tyan.oreore.com" isEqual:host];
}
@end

iPhoneではAdHocによって100人までは自由にアプリを配布することが出来ますので、
App Storeを利用しない、自分専用・内輪向けのハックアプリなんかをこうして作っても面白いですね。
でも、せっかくiPhoneで開発するならApp Storeに挑戦しなければ損!もちろん、そのうち挑戦しますよ?