Displaying posts filed under

プログラミング

VimでCoffeeScriptの開発環境を構築する

VimでCoffeeScriptを快適に書くための環境作り。

  • Mac OS X Lion
  • Vim 7.3(Lionに標準で入っているもの)
  • CoffeeScript 1.2.0

VimはMac標準のターミナル.app上で使用する。Lionからは256色表示に対応したので実用性が高くなった。

ppathogenのインストール

まずvimのプラグインを管理しやすくするためのプラグインであるpathogenを導入する。手順は配布ページに書いてある通り。

mkdir -p ~/.vim/autoload ~/.vim/bundle
curl 'www.vim.org/scripts/download_script.php?src_id=16224' \
  > ~/.vim/autoload/pathogen.vim

.vimrcに次の1行を追加してインストール完了。

call pathogen#infect()

vim-coffee-scriptプラグインのインストール

インストール自体は.vim/bundle以下にgit cloneするだけで完了。

cd ~/.vim/bundle
git clone https://github.com/kchmck/vim-coffee-script.git

タブの設定をCoffeeScriptの推奨値にあわせて変更。次の1行を ~/.vim/after/ftplugin/coffee.vim に追加する。

setlocal shiftwidth=2 expandtab

さらに最も使用頻度が高い:CoffeeCompileコマンド(編集中のファイルをJavaScriptにコンパイルして、結果を別ウィンドウに表示するコマンド)にはショートカットキーを割り当てる。私はC-cに割り当てることにした。以下の2行を ~/.vim/after/ftplugin/coffee.vim に追加する。

" CoffeeCompileコマンドにvertオプションを付けることでウィンドウが縦に分割され、
" splitrightオプションによってコンパイル結果が右側に表示されるようになる
nnoremap <silent> <C-C> :CoffeeCompile vert <CR><C-w>h
setlocal splitright

vim-indent-guidesプラグインのインストール

CoffeeScriptはインデントで構造を表すので、インデント量の違いを見やすくするためにvim-indent-guidesプラグインを導入する。これもインストール自体はgit cloneで完了。

cd ~/.vim/bundle
git clone https://github.com/nathanaelkane/vim-indent-guides.git

gVimで使用する場合はインデントの色も自動で設定されるようだが、私はターミナルで使用したいので手動で色を指定した。色見本はこちらのページを参考にした。

let g:indent_guides_enable_on_vim_startup = 1
let g:indent_guides_auto_colors = 0
 
" indent guides
augroup indentguides
    autocmd!
    autocmd VimEnter,Colorscheme * :hi IndentGuidesEven ctermbg=236
    autocmd VimEnter,Colorscheme * :hi IndentGuidesOdd ctermbg=235
augroup END

最終的な表示は次のようになる。

CoffeeCompileを実行してコンパイル結果を表示したところ

参考

doScrollによるDOMContentLoadedエミュレーションの落とし穴

今更こんなことが問題になるケースは稀だと思うが、『パーフェクトJavaScript』の中でも紹介されていたので注意として書いておく。

IE8以前でDOMContentLoadedイベントをエミュレートする方法として、doScrollを使ったハックは広く知られている。例として『パーフェクトJavaScript』230ページのリスト8.9より引用。

function IEContentLoaded (callback) {
    (function () {
        try {
            document.documentElement.doScroll('left');
        } catch (error) {
            setTimeout(arguments.callee, 0);
            return;
        }
        callback();
    })();
}

しかし実際に試してみればわかるが、これだとwindow.onloadより実行が遅くなる場合がある。

テスト1(画像なし) テスト2(画像あり)

具体的には

  • 画像など外部から読み込まれるリソースが少ない場合
  • リロードした時にキャッシュがきいている場合

こういったケースではwindow.onloadとdoScrollハックの実行順序は逆転する場合がある。もちろん逆転しない場合もある。doScrollハックはあくまでハックであって正式なイベントシステムの一部ではないのだから、一貫した動作をしなくても当然だと言える。

この実行順序の逆転を防ぐために、世の中のライブラリではdocument.onreadystatechangeイベントを併用するのが習わしとなっている。例えばdoScrollハックを世に広めたDiego Perini氏の実装を見れば、trying to always fire before onload というコメントとともにreadyStateをチェックするコードが入っているのが分かる。

document.onreadystatechange = function() {
    if (document.readyState == 'complete') {
        //コールバック関数を実行し、以後はdoScrollハックが動かないようにしておく
    }
};

テスト1(画像なし) テスト2(画像あり)

現実問題としては、わざわざ自前でdoScrollハックを書くような状況では、この順序の逆転が問題になることはまずないと思われる。しかし以下のような教訓を読み取ることはできるだろう:

  • 広く知られていようがハックはハック。使うときは慎重になろう
  • 可能な限り信頼できる既存の実装を使おう
  • 自分の手で実験しよう

UINavigationControllerによる「戻る」「進む」を検出する

UIViewControllerのviewWillAppear:、viewDidDisappear:等のメソッドはviewの表示状態が変化した時に呼ばれるが、なぜ変化したのかという理由までは分からない。つまり以下のようなイベントが簡単には区別できない:

  • UINavigationControllerの中で画面遷移が起こった(戻った・進んだ)
  • modalダイアログが開かれた・閉じられた
  • UITabBarControllerでタブが切り替えられた

今作っているアプリでは特にUINavigationControllerの中で「進んだ」「戻った」といったイベントを検出したかったので、それらをサポートするための抽象クラスを作ってみた(UIViewControllerのサブクラス)。

NavigationSupportController.h
NavigationSupportController.m
(iOS 4.3と5.0で動作確認)

このNavigationSupportControllerのサブクラスでは、viewWillAppear:、viewDidDisappear:等の代わりに次のメソッドをオーバーライドする。

  • viewWillAppear:(BOOL)animated direction:(NavigationDirection)direction
  • viewDidAppear:(BOOL)animated direction:(NavigationDirection)direction
  • viewWillDisappear:(BOOL)animated direction:(NavigationDirection)direction
  • viewDidDisappear:(BOOL)animated direction:(NavigationDirection)direction

NavigationDirectionは列挙型で、次の3種類の値をとる。

NavigationBackUINavigationControllerによる「戻る(=pop)」で画面遷移が起こった
NavigationForwardUINavigationControllerによる「進む(=push)」で画面遷移が起こった
NavigationNoneUINavigationControllerによらない表示状態の変化が起こった

こんな感じで判定する。

- (void)viewWillAppear:(BOOL)animated direction:(NavigationDirection)direction
{
    if (direction == NavigationBack) {
        //「戻る(= pop)」の場合
    } else if (direction == NavigationForward) {
        //「進む( = push)」の場合
    } else if (direction == NavigationNone) {
        //NavigationControllerによらない画面遷移の場合
    }
 
    //共通の処理はここに書く
    //つまり viewWillAppear:(BOOL)animated の方はオーバーライドすべきではない
}

検出ロジックの都合上、コントローラのインスタンスはpushするときに作成され、popされるときに破棄されることを前提としている。普通はこれで問題ないはず。

もっと確実・簡単な方法があれば教えてください。

mod_dosdetector-forkをApache 2.0で動かす

今更ながら、mod_dosdetector-forkをApache 2.0で動かすための方法をまとめておく。少しだけソースコードを編集する必要がある。

必要なもの:

一応、自動で編集するスクリプトも作ってみたが、先に手動で編集する場合の手順をざっと説明しておく。

手順1.

mod_dosdetector.c をエディタで開き、/* code for apache 2.0 */ というコメントが書いてある部分に次の1行を書き加える。

#include "apache20.h"

手順2.

Apache 2.2のソースを展開して srclib/apr/shmem/unix/shm.c をエディタで開き、関数apr_shm_removeの定義部分をコピーして、手順1で編集した部分の直後にペーストする。最終的にこんな感じになる。

/* code for apache 2.0 */
#include "apache20.h"
APR_DECLARE(apr_status_t) apr_shm_remove(const char *filename,
                                         apr_pool_t *pool)
{
//..略..
}

以上で必要な編集作業は終わり。ここまでの手順を自動化するスクリプトがapache20.plという名前で入っているので、これを実行してもいい。

# 展開したApache 2.2のソースディレクトリを指定する
perl apache20.pl path/to/httpd-2.2.x

あとは通常通りのインストール手順でOK。

#apxsが/usr/sbinに入っている場合
#コンパイル
make PATH=/usr/sbin:$PATH
 
#インストール
make PATH=/usr/sbin:$PATH install

動作確認は次の環境で行った。

  • ソースコードのコピー元: Apache 2.2.21
  • 動作: Apache 2.0.64 on CentOS 5
  • 動作: Apache 2.0.52 on CentOS 4

今後どの程度メンテナンスを行っていくかは不明なので、そこは予めご了承いただきたい。あと2.2側のコード変更によっては自動化スクリプトは動かなくなる可能性があるので、そのときは適宜手動で対応のこと。

Dart言語のIsolateについて調べてみた

Googleが先日発表したプログラミング言語Dart。その特長の一つとして挙げられるIsolateが面白そうだったので、ちょっと特性を調べてみた。結論から言うと、なかなか気難しい機能だな、という印象。

  • 何か意味のあるサンプルを作りたかったわけではなく、単に動作の性質が知りたかっただけです
  • 実装は読んでないので多分に憶測混じりです
  • dartlang.org上のDartboardでテストしています

DartのIsolateはErlangのプロセスに似ていると評価されることがあるが(確かに似たところもあるが)、使用感は全く別物と思った方が良いと思う。最も大きな違いは「receiveがブロックしない」つまり「メッセージを受信するまで待つ」ことができないこと。

class IsolateA extends Isolate {
  main() {
    port.receive((msg, replyTo) {
      //受信したメッセージの処理
    });
    //ここに書いたコードはメッセージの受信を待たず実行される
    print("hoge");
  }
}

これが何を意味するかと言うと、メッセージを受信した後で実行したい処理は全てクロージャの中に入れて、イベント駆動で動作させる必要があるということ。複数のIsolateを扱ったり、処理の前後関係が発生したりするとかなり面倒なことになる。Dart組み込みのPromiseを駆使することが必須になるだろう(最後に例を載せています)。

それと別のIsolateにデータを送るには厳密にメッセージ送信しか手段がない。コンストラクタやプロパティ経由で値を送ってもIsolateインスタンスの中からは参照できない。

class IsolateB extends Isolate {
  int x = 0;
 
  main() {
    port.receive((msg, replyTo) {
      print(x + msg);
    });
  }
}
 
main() {
  IsolateB iso = new IsolateB();
  //ここで値を設定しても消える
  iso.x = 5;
 
  //表示されるのは 105 ではなく 100
  iso.spawn().then((port) {
    port.send(100); 
  });
}

では初期化が必要な場合はどうするのかというと、初期化専用のメッセージ構造を決めておいて、その中に値を込める。例えば配列や辞書を使って。

class IsolateC extends Isolate {
  int x = 0;
 
  main() {
    port.receive((msg, replyTo) {
      if(msg[0] == "init") {
        x = msg[1];
      } else {
        print(x + msg[1]);
      }
    });
  }
}
 
main() {
  new IsolateC().spawn().then((port) {
    port.send(["init", 5]);
    port.send(["print", 100]);
  });
}

こういったやり方はErlangでもお馴染みだけど、Erlangと違ってパターンマッチはないしapplyもないし文字列からメソッドを得ることもできないし、そもそも静的型付け思想と全然相容れないしで、はっきりいって、ひどい。このIsolateという機能がどうにもDart言語に最適化されていない、練り込みが足りない感じがするのはこの辺りだ。

そしてメッセージ送信を用いても、制約なくどんなオブジェクトでも送れるわけではない。リファレンスに説明がない&実装を読んでないので詳しい条件は不明だが、独自に定義したクラスのインスタンスは送れない。組み込みのRegExpやPromise等も駄目(Dartboardにはエラーすら出ない)。intやStringといった基本的な値の他には、ListとMapくらいしか送れないと考えた方が良いかもしれない。

class Xyz {
  String toString() => "xyz";
}
 
class IsolateD extends Isolate {
  main() {
    port.receive((msg, replyTo) {
      print(msg);
    });
  }
}
 
main() {
  new IsolateD().spawn().then((port) {
    //エラーすら出ずに無視される
    port.send(new Xyz());
  });
}

それはそれとして、新たに起動したIsolateに対してメッセージを送るサンプルは多いが、別のIsolateから送られたメッセージを”メインIsolate”で受け取る方法があまり紹介されていないので触れておくと、ReceivePortを普通にnewして、toSendPortで対応するSendPortを生成、sendの第二引数で送信元Isolateに通知すれば良い。

class EchoIsolate extends Isolate {
  SendPort mainPort;
 
  main() {
    //第2引数でSendPortが送られてくる
    port.receive((msg, replyTo) {
      if(msg == "init") {
        mainPort = replyTo;
      } else {
        mainPort.send("echo: " + msg);
      }
    });
  }
}
 
main() {
  ReceivePort p = new ReceivePort();
 
  new EchoIsolate().spawn().then((port) {
    port.send("init", p.toSendPort());
    port.send("foo");
    port.send("bar");
  });
 
  //echo: foo, echo bar
  p.receive((msg, replyTo) => print(msg));
}

特定のIsolateと単発でやり取りする場合はsendの代わりにcallを使うとこの過程を自動化してくれる。

class EchoIsolate extends Isolate {
  main() {
    port.receive((msg, replyTo) {
      replyTo.send("echo: " + msg);
    });
  }
}
 
main() {
  new EchoIsolate().spawn().then((port) {
    port.call("foo").receive((msg, replyTo) => print(msg));
  });
}

また特例として、メッセージの中にReceivePortを入れると自動でSendPortに変換されるので、次のようにも書ける。

class EchoIsolate extends Isolate {
  SendPort mainPort;
 
  main() {
    port.receive((msg, replyTo) {
      if(msg is SendPort) {
        mainPort = msg;
      } else {
        mainPort.send("echo: " + msg);
      }
    });
  }
}
 
main() {
  ReceivePort p = new ReceivePort();
 
  new EchoIsolate().spawn().then((port) {
    port.send(p);
    port.send("foo");
    port.send("bar");
  });
 
  //echo: foo, echo bar
  p.receive((msg, replyTo) => print(msg));
}

最後にFizzBuzzを題材として、2つのIsolateを起動し、”メインIsolate”を含む3つをリング状に連結してメッセージを回す例を載せておく。main => Fizz => Buzz => main とメッセージが回って、mainでprintする。それぞれの起動と初期化が非同期になるので、盛大にPromiseを使うことになった。

class FizzBuzzIsolate extends Isolate {
  static final MSG_INIT = "init";
 
  Promise<SendPort> _nextPromise;
  String _word;
  int _divisor;
 
  _senderFunc(i, s) => (SendPort nextPort) {
    if(i % _divisor == 0) s += _word;
    nextPort.send([i, s]);
  };
 
  static init(SendPort port, SendPort nextPort, String word, int divisor) =>
    port.send([MSG_INIT, [word, divisor]], nextPort);
 
  main() {
    _nextPromise = new Promise<SendPort>();
 
    port.receive((msg, replyTo) {
      if(msg == null) {
        port.close();
      } else if(msg[0] === MSG_INIT) {
        _word = msg[1][0];
        _divisor = msg[1][1];
        _nextPromise.complete(replyTo);
      } else {
        _nextPromise.then(_senderFunc(msg[0], msg[1]));
      }
    });
  }
}
 
main() {
  ReceivePort mainPort = new ReceivePort();
  Promise<SendPort>
    fizz = new FizzBuzzIsolate().spawn(),
    buzz = new FizzBuzzIsolate().spawn(),
    p = new Promise();
  p.waitFor([fizz, buzz], 2);
  p.then((val) {
    FizzBuzzIsolate.init(fizz.value, buzz.value, "Fizz", 3);
    FizzBuzzIsolate.init(buzz.value, mainPort.toSendPort(), "Buzz", 5);
  });
 
  _sender(i) => (port) => port.send([i, ""]);
  for(int i=1; i<=100; ++i) fizz.then(_sender(i));
 
  mainPort.receive((message, replyTo) {
    print(message[1] == "" ? message[0] : message[1]);
  });
}

iPhoneの機種を判別するUIDevice Extension

iPhone 5の発表間近といわれる今日この頃。

アプリ内でiPhone(iOSデバイス)の機種を判別するには sysctlbyname というC言語の関数を使うのだが、結果は “iPhone3,1″ のような文字列で返ってくるため、そのままでは扱いづらい。これをわかりやすい形に変換してくれるUIDevice Extensionというオープンソースのライブラリが存在する。

erica / uidevice-extension – GitHub BSDライセンス
(注: forked from ars/uidevice-extension とあるが、arsとericaはともにErica Sadunさんのアカウントなので、これが本家リポジトリと考えて良いようだ)

UIDevice Extensionには多くの機能があるが、機種判別を行うだけなら UIDevice-Hardware.{h,m} のみ自分のプロジェクトにコピーすれば良い。いずれもUIDeviceクラスに対するカテゴリとして構成されているので、使用したい箇所で .h をインクルードすれば、UIDeviceに専用のメソッドが追加される。

機種名を文字列として取得するには platformString メソッドを使う。このメソッドが適しているのはユーザがどの機種を使っているか集計したい場合等だろう。なお2011年9月25日現在、未発表のiPhone 5まで対応している(この情報が確実に正しいとまでは言えないはずだが)。

//@"iPhone 3GS" や @"iPhone 4" といった文字列が返る
NSString *iosDevice = [[UIDevice currentDevice] platformString];
 
//おまけ: iOSのバージョンとアプリのバージョンを取得する
NSString *osVersion = [[UIDevice currentDevice] systemVersion];
NSString *appVersion = [[[NSBundle mainBundle] infoDictionary]
                                        objectForKey:(NSString*)kCFBundleVersionKey];

機種に応じてロジックを切り替える必要がある場合には platformType メソッドが適している。戻り値はヘッダファイル内で定義された列挙型 UIDevicePlatform である。

if([[UIDevice currentDevice] platformType] == UIDevice4iPhone) {
    //iPhone4 の場合の処理
}

ただ大雑把に機種を振り分ける場合にはコードが煩雑になりがちなので、jorgenptさんのforkで定義されているような包括的なマクロを利用するのが良いかもしれない。いずれにせよこの種の判定でロジックを切り替える場合、前方互換性に注意する必要がある。

iPhoneアプリの使用状況をGoogle Analytics for iOSで解析する

Google AnalyticsにはiOS版があり、アプリ内の画面操作に仮想的なURLを割り当てることで、Webサイトと同じように”アクセス解析”ができる。

インストール

公式サイトからSDKをダウンロードして展開、Libraryディレクトリの中に入っている GANTracker.h と libGoogleAnalytics.a を自分のプロジェクトにコピーする(もう一つの libGoogleAnalytics_NoThumb.a はThumb命令を無効にしたプロジェクトで使用するもの。標準のプロジェクトでは必要ない)。

次にXcodeでプロジェクトファイルを開き「TARGETS」でアプリを選択、「Build Phases」「Link Binary With Libraries」を開いて、「+」ボタンを押してCFNetworkフレームワークとlibsqlite3.0.dylibを追加する。ちなみに libGoogleAnalytics.a はプロジェクトにコピーした段階で追加されているはず。

使用方法

公式サイトのドキュメントを読めばわかる通り、トラッキングの概念や使い方はWeb用のGoogle Analyticsとほぼ同じなので、迷うことはないだろう。ただトラッキング用のメソッドを実行してもすぐにはGoogleサーバーにデータが送られず、一定の時間(初期化時に dispatchPeriod として指定する)ごとにまとめて送られるという動作が異なる。

あと GANTracker.h のコメントにはサイトに書いていない有用な情報が含まれているので、そちらもチェックした方が良い。下記の便利マクロに使っている知識もそこから得た。

便利マクロ

http://d.hatena.ne.jp/ninjinkun/20110115/1295074900

ここで紹介されているマクロが便利なので改造して使わせていただいている。自分で追加・変更した内容は以下の通り。

初期化マクロ

 GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE)

GAアカウントと送信間隔とdelegateの設定を行う、初期化用のマクロ。定義は下のようになる。

#define __GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE) \
[[GANTracker sharedTracker] startTrackerWithAccountID:ACCOUNT \
                            dispatchPeriod:PERIOD \
                            delegate:DELEGATE];
 
#ifdef DEBUG
#define GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE) { \
  __GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE); \
  [GANTracker sharedTracker].debug = YES; \
  [GANTracker sharedTracker].dryRun = YES; }
#else
#define GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE) __GA_INIT_TRACKER(ACCOUNT, PERIOD, DELEGATE);
#endif

DEBUGマクロが定義されていた場合限り debug プロパティと dryRun プロパティが有効になる。debug プロパティを有効にするとデータがサーバに送られるタイミングでコンソールにログが出る(生の__utm.gifに対するリクエストログ)。dryRun プロパティを有効にすると実際のサーバにはデータが送られない。

ページトラッキング

GA_TRACK_PAGE(PAGE)

元サイトのものとほぼ同じだが、trackPageview:withError: は先頭に / がない場合は自動で付加してくれるので、その部分の処理を省いている。この方が / 付きのパス(NSURLから得たパスなど)をそのまま使えるので便利。

#define GA_TRACK_PAGE(PAGE) { \
  NSError *error; \
  if (![[GANTracker sharedTracker] trackPageview:PAGE withError:&error]) { NSLog(@"%@",error.helpAnchor);  } };

UITableViewのスクロール位置をView再読み込み時に復帰する方法

メモリ警告によってViewが破棄された場合、UITableViewのスクロール位置はリセットされてしまう。再度Viewが読み込まれたときに元の位置に戻すには、破棄される直前のスクロール位置を覚えておく必要がある。だいたい次のようなコードになる:

ScrollTestViewController.h

#import <UIKit/UIKit.h>
 
/* UITableViewControllerではなくUIViewControllerを使っている理由は後述 */
@interface ScrollTestViewController :
UIViewController<UITableViewDataSource, UITableViewDelegate> {
 
    /* UITableViewはIBOutletでretainする想定 */
    UITableView *_tableView;
 
    /* Viewが破棄される直前のスクロール位置 */
    CGPoint lastScrollOffset;
}
@property (nonatomic, retain) IBOutlet UITableView *tableView;
 
@end

ScrollTestViewController.m

@implementation ScrollTestViewController
 
@synthesize tableView = _tableView;
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    /*
     reloadDataでデータを読み込んだ後に
     記録されているスクロール位置を復帰
     初期化直後は{0,0}で初期化されているので影響はない
    */
    [_tableView reloadData];
    [_tableView setContentOffset:lastScrollOffset];
}
 
- (void)viewDidUnload {
    [super viewDidUnload];
 
    /*
     スクロール位置を記録した後でUITableViewを破棄する
     viewDidUnloadが呼ばれた時点でメインのViewは既に破棄されているが、
     Controllerがretainしているサブビューはまだ生きているというのがポイント
    */
    lastScrollOffset = [_tableView contentOffset];
    self.tableView = nil;
}
 
- (void) dealloc {
    self.tableView = nil;
    [super dealloc];
}
 
/*
...
その他、UIViewControllerでUITableViewを使うためには
viewWillAppearで選択を解除する処理などが必要だが
本題には関係ないので省略
...
*/
 
@end

ここでUITableViewControllerを使ってしまうと、UITableViewが破棄されるタイミングや再読み込みされるタイミングを制御できなくなるので、記憶するのも元に戻すのも難しくなる。例えばUITableViewのreloadDataをオーバーライドする必要があったりするらしい。

参考: iPhone 画面切り替え時、あらかじめテーブルビューを目的の位置にスクロールさせとく技

Rails3のi18n機能および日本語化

必要な情報はRails Guidesの中の以下のページにまとまっている。

Rails Internationalization (I18n) API

  • I18n.translate メソッドを使って「抽象化されたキー」から、ロケールに基づく「自然言語」に変換する。いきなり英語で文章を考えなくて済む分、日本人には取っ付きやすいかもしれない。同様に、 I18n.localize メソッドで日付や時刻のフォーマットを行う。
    • それぞれ I18n.t, I18n.l という省略形が用意されている。さらに多くのコンテクストでは t, l というショートカットメソッドが使える。
  • Railsフレームワーク自体もこのメカニズムで国際化されているため、各コンポーネントが要求するキーに対して適切な翻訳を用意することで、フレームワーク全体が翻訳される。
  • キーは階層化することができる。またRailsが要求するキーも、コンポーネントごとに階層化されている(例: activerecord.errors.messages.record_invalid など)
    • 標準のメッセージファイルにおいては、YAML(.yml)またはRuby(.rb)のハッシュを使って、キーの階層をそのまま再現する。
    • 階層の上位部分をスコープ(scope)と呼ぶが、どこまでがスコープかという区別は明確ではない。
  • I18n.locale= で使用するロケールを設定する。
    • 使用するロケールの決定方法について。Web”サイト”においてはURL中にロケールを含めるべきである。一方で、Web”アプリケーション/サービス”においてはAcceptヘッダやDBに保存した個人設定を使うことも有効である。

フレームワーク全体の日本語化

とにかく手っ取り早く”日本語化”をしたいという場合は

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml

を config/locales/ に放り込んで、config/application.rb に

config.i18n.default_locale = :ja

と書いておけば、フレームワークが自動で出力する各種メッセージは日本語になる。ただ標準ではエラーメッセージのフィールド名の後に空白が入るので、errors.formatを次のように修正しておいた方が良いかもしれない(好みの問題だが)。

--- ja.yml.orig     2011-06-22 22:38:21.000000000 +0900
+++ ja.yml     2011-06-22 22:38:33.000000000 +0900
@@ -188,7 +188,7 @@
       submit: "保存する"
 
   errors:
-    format: "%{attribute} %{message}"
+    format: "%{attribute}%{message}"
 
     messages: &errors_messages
       inclusion: "は一覧にありません。"

以後、この ja.yml に自分のアプリ用のメッセージを追加していっても良いが、やはり専用の翻訳ファイルを追加した方が扱いやすいだろう。翻訳ファイルは config/locales/ の中に拡張子 .yml か .rb で作成する。ファイル名は何でもいいし、言語ごとに分けてもいいし分けなくてもいい。ただしサブディレクトリの中に入れた場合は追加の設定が必要になる(参照 3.4 Organization of Locale Files)。また新たにファイルを追加した場合はサーバを再起動しなければならない。

ActiveRecordのモデル名・フィールド名の日本語化

上の手順でvalidationのエラーメッセージは日本語化されるが、モデル名やフィールド名は自分で翻訳を用意する必要がある。詳細については下ページ参照。

Translations for Active Record Models

ActiveModelのモデル名・フィールド名の日本語化

任意のクラスにおいて ActiveModel::Translation モジュールをextendすることで、ActiveRecord同様の翻訳機能が有効になる。

class MyForm
  extend ActiveModel::Translation
end

Active Model Translation

ただしスコープが activerecord ではなく activemodel であることに注意(サブクラスで i18n_scope メソッドをオーバライドすれば変更できるが)。

activemodel:
  models:
    my_form: "モデル名"
  attributes:
    my_form
      attr_name: "属性名"

なおvalidationに関してはActiveRecordと共通である。

Rails3+Rspecでファイルアップロードのテストをする

標準のTest::Unitに含まれている fixture_file_upload はrspec-railsに含まれていないので、自分でspec_helper.rbに追加する。

def fixture_file_upload(path, type)
  Rack::Test::UploadedFile.new(fixture_path + path, type)
end

標準の fixture_file_upload 同様、Rack::Test::UploadedFile のインスタンスを返すだけ。

テストに使用するファイルは spec/fixtures/ 以下に置く。例えば spec/fixtures/files/dummy.txt を使用するには

fixture_file_upload("/files/dummy.txt", "text/plain")