Titanium MobileでiPhone上の表示名を国際化する
Titanium Mobileで文字列を国際化する方法は、AppceleratorのDocumentation Guide『Internationalizing your Application』でわかります。
iPhone上の表示名を国際化したい場合は、string.xmlとは別に「app.xml」ファイルを作ります。app.xmlには、name属性が「appname」のstring要素を書きます。このファイルを言語毎に用意すれば、iPhoneの言語環境に応じてアプリ名が切り替わることをiOSシミュレータとiPhone 3GSで確認しました。
<?xml version="1.0" encoding="UTF-8"?> <resources> <string name="appname"></string> </resources>
余談
ja.lproj内にInfoPlist.stringsファイルを作成できないかと思い、「/Library/Application Support/Titanium/mobilesdk/osx/1.6.1/common/localecompiler.py」のソースを読んでいたら53行目のlocalization_file_name_iosを見つけ、さらにその上の行ではファイル名がapp.xmlだとInfoPlist.stringsを作ってくれることがわかりました。
def isApp(self,file): return (os.path.basename(file) == "app.xml") def localization_file_name_ios(self,file): if self.isApp(file): return "InfoPlist.strings" return "Localizable.strings"
でも、iTunesのAppで表示は英語のままなんですよね。CFBundleDisplayNameを見てるんじゃないのか。
それか、iTunes ConnectのManage Localizationsで設定するアプリ名がiTunesで表示されるのかな?
Titanium MobileアプリとMongoDBをSleepy.Mongooseを経由で接続させてみた
MongoDBにSleepy.Mongoose経由で、Titanium Mobileで作ったiPhoneアプリから接続する簡単なアプリを作ってみました。
Sleepy.Mongooseを経由したのは、MongoDBのRESTインターフェースは読み取り専用で外部ツールを使うことが推奨されていたからです。*1
The mongod process includes a simple read-only REST interface for convenience. For full REST capabilities we recommend using an external tool such as Sleepy.Mongoose.
MongoDB、Sleepy.MongooseそしてTitanium Mobileのインストールや設定などにつきましては、それぞれの公式サイトに書いてありますので、割愛させていただきます。あえて注意する事と言えば、MongoDB起動時「--rest」オプションを間違って付けないことでしょうか。
Todoアプリ
タスクの一覧(query)を表示・追加(insert)をするだけのアプリです。あくまでも勉強用なので、実用性は全くありません。また、Titanium Mobileを使ってHello World以外を作ったのもこれが初めてなので、変なコードを書いていたらどうかご勘弁を。
ソース
Titanium Developerを起動し、New ProjectでTodoプロジェクトを作成。Titanium SDKは1.5.1、iPhone SDKは4.2を選択しました。MongoDB、Sleepy.Mongoosそして本アプリは、すべてローカルで動かすことを前提にしています。
APP_ROOT/Resources/app.js
Ti.UI.setBackgroundColor('#000'); var win1 = Ti.UI.createWindow({ url: 'table_view.js', title:'List', backgroundColor:'#fff' }); var tab1 = Ti.UI.createTab({ window:win1 }); win1.hideTabBar(); var tabGroup = Ti.UI.createTabGroup({}); tabGroup.addTab(tab1); tabGroup.open();
タスクを追加するウィンドウ「taskWindow」に「done」というカスタムイベントを追加しているのは、「close」イベントを追加すると、「Back」ボタンをタップしたのか「Done」ボタンをタップしたのかわからないからです。
「Done」ボタンをタップしてタスクを追加したら、MongoDBからオブジェクトIDが戻りますので、doneイベントを発動する際、引数にこのIDを渡して、一覧を更新する方がいいんですが、面倒なんで全部再取得してtableViewに詰め込んでます。
APP_ROOT/Resources/table_view.js
var win = Ti.UI.currentWindow; var xhr = Ti.Network.createHTTPClient(); var tableView = null; function updateTable() { var find = 'http://localhost:27080/todo/tasks/_find?sort=' + encodeURIComponent('{"_id": -1}'); xhr.open('GET', find); xhr.onload = function() { var todo = JSON.parse(this.responseText); var tasks = todo.results; var data = []; for(var i = 0; i < tasks.length; i++) { var row = Ti.UI.createTableViewRow(); var label = Ti.UI.createLabel({ text: tasks[i].text }); row.add(label); data.push(row); } if(tableView == null) { tableView = Ti.UI.createTableView({ data: data }); win.add(tableView); } else { tableView.data = data; } }; xhr.send(); } var taskButton = Ti.UI.createButton({ systemButton: Ti.UI.iPhone.SystemButton.ADD }); taskButton.addEventListener( 'click', function() { var taskWindow = Ti.UI.createWindow({ url: 'task_window.js', title: 'Add Task', backgroundColor: '#fff' }); taskWindow.addEventListener( 'done', function() { updateTable(); } ); Ti.UI.currentTab.open(taskWindow); } ); win.rightNavButton = taskButton; var conn = 'http://localhost:27080/_connect'; xhr.open('POST', conn); xhr.onload = function() { updateTable(); }; xhr.send({"data":"localhost:27017"});
APP_ROOT/Resoruces/task_window.js
var win = Ti.UI.currentWindow; var textArea = Ti.UI.createTextArea({ height: 180, width: 300, top: 10, font:{ fontSize:20 }, borderWidth: 1, borderColor: '#bbb', borderRadius: 5, autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE, suppressReturn: false }); win.addEventListener('open', function(e) { textArea.focus(); }); win.add(textArea); var doneButton = Ti.UI.createButton({ systemButton: Ti.UI.iPhone.SystemButton.DONE }); doneButton.addEventListener( 'click', function() { if(textArea.value) { var url = 'http://localhost:27080/todo/tasks/_insert'; var xhr = Ti.Network.createHTTPClient(); xhr.open('POST', url); xhr.send({"docs": '[{"text": "' + textArea.value + '"}]'}); xhr.onload = function() { win.fireEvent('done'); win.close(); }; } } ); win.rightNavButton = doneButton;
動作確認
Titanium Developer経由でiPhoneシミュレータを起動し、タスクを追加。MongoDBのシェルでinsertされているか確認。試しに、日本語の文章をテキストエリアに貼付けて追加してみましたが、文字化けはしませんでした。なお、Androidのemulatorでは動きません。
$ mongo todo MongoDB shell version: 1.6.5 connecting to: todo > db.tasks.find().sort({id:-1}) { "_id" : ObjectId("4d5b3b3a5a7a992baf000005"), "text" : "Go to Ginza." } { "_id" : ObjectId("4d5b3b4d5a7a992baf000006"), "text" : "Visit my girl friend." } { "_id" : ObjectId("4d5b50045a7a993380000000"), "text" : "日本語のテスト" }
余談
JavaScriptでiPhone/Androidアプリが作れちゃうってすごいですね。
このアプリもそうですが、やはりプラットフォーム固有の設定・動作があり、Googleでドキュメントを検索していた時、if文で分岐させているソースを見ました。でも、例えばretainCountを気にしなくてよいとか、Objective-Cでを書いてた時と比べたら格段に楽かなぁ...
MongoDBのスケーラビリティについては、また後日、時間をとって勉強してみたいと思っています。セキュリティまわりは、MongoDBにユーザ認証がありますし、Sleepy.MongooseはSSLに対応してますから大丈夫なんじゃないかと。ただ、Titanium.Network.HTTPClientがHTTPSに対応しているのかは未確認です。
コマンドラインでツイートするRubyスクリプト(つづき)
先日、コマンドラインでツイートするRubyスクリプトを作ってみましたが、引数の最後がURLだった場合、Google URL Shortener APIを使って短縮するようにしてみました。
このままでも動きますが、GoogleはAccess Keyの取得を推奨し、設定するとAnonymous状態と比べて制限*1が拡大されます。先日、公開したスクリプトを既にお使いで、Access Keyを設定したい場合は、お手数ですが「~/.twrc」ファイルに「google_api_access_key」を追記してください。
google_api_access_key: <YOUR ACCESS KEY>
ソース
#!/usr/bin/env ruby $KCODE = 'u' require 'fileutils' require 'yaml' require 'rubygems' require 'oauth' require 'json' class Tw CONFIG_FILE = '.twrc' # Twitter API KEY = 'consumer_key' SECRET = 'consumer_secret' TOKEN = 'access_token' TOKEN_SECRET = 'access_token_secret' MAX_LENGTH = 140 TWITTER_API = 'http://api.twitter.com' UPDATE_URL = 'https://api.twitter.com/1/statuses/update.xml' # Google URL Shortener API ACCESS_KEY = 'google_api_access_key' GOOGLE_APIS = 'www.googleapis.com' SHORTENER_PATH = '/urlshortener/v1/url' attr_accessor :config def initialize config_file = File.join(File.expand_path('~'), CONFIG_FILE) begin @config = YAML.load(File.read(config_file)) rescue @config = { KEY => prompt(KEY), SECRET => prompt(SECRET), TOKEN => prompt(TOKEN), TOKEN_SECRET => prompt(TOKEN_SECRET), ACCESS_KEY => prompt(ACCESS_KEY) } File.open(config_file, 'w') {|f| f.write(@config.to_yaml)} FileUtils.chmod(0600, config_file) puts "saved to #{config_file}" end end def get_token consumer = OAuth::Consumer.new(@config[KEY], @config[SECRET], :site => TWITTER_API) token_hash = { :oauth_token => @config[TOKEN], :oauth_token_secret => @config[TOKEN_SECRET] } access_token = OAuth::AccessToken.from_hash(consumer, token_hash) end def shorten_url(long_url) params = {'longUrl' => long_url} params.store('key', @config[ACCESS_KEY]) if @config[ACCESS_KEY] && !@config[ACCESS_KEY].empty? response = '' http = Net::HTTP.new(GOOGLE_APIS, 443) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE response = http.post(SHORTENER_PATH, params.to_json, {'Content-Type' => 'application/json'}) return Net::HTTPOK == response.class ? (JSON.parse(response.body))['id'] : '' end private def prompt(string) print string.gsub('_', ' ').capitalize + ":" gets.strip end end URL_REGEXP = /(^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$)/ix tw = Tw.new if URL_REGEXP =~ (ARGV.length > 0 ? ARGV[ARGV.length - 1] : '') status = (ARGV[0, ARGV.length - 1].join(' ') + ' ' + tw.shorten_url($1)).strip else status = ARGV.join(' ') end unless status.empty? && status.split('').length > Tw::MAX_LENGTH response = tw.get_token.post(Tw::UPDATE_URL, {:status => status}) exit(Net::HTTPOK == response) else exit(false) end
設定
アプリをTwitter Applicationsで登録してください。アプリケーションの種類は、「クライアントアプリケーション」です。
Access TokenとAccess Token Secretは、アプリを登録した後、右サイドメニューにある「My Access Token」をクリックすると取得できます。
Access Keyは、Google APIs Consoleで取得できます。
引数なしで起動し、Consumer Key、Consumer Secret、Access Token、Access Token Secret、Google Api Access Keyを入力してください。Access Keyは省略する場合、単にエンターキーでスキップしてください。
使い方
Twitterに投稿したいメッセージを引数として、起動してください。複数の引数は、半角の空白で連結します。ただし、半角括弧が文中にある場合は、文章全体をダブルクォーテーションで囲ってください。
注意点
当たり前のことですが、「AS IS」*2です。
バレンタインチョコ欲しい!*3
欲しいプレゼントは…
Apple iPod touch 64GB MC547J/A
- 出版社/メーカー: Apple Computer
- 発売日: 2010/09/02
- メディア: エレクトロニクス
- 購入: 12人 クリック: 580回
- この商品を含むブログ (101件) を見る
Cucumberをはじめてみた(つづき)
先日に引き続き、『はじめる! Cucumber』(諸橋恭介著、達人出版界、2010年、v0.9.1)の第5章以降をRuby on Rails 3でやってみました。
redirect_to :backで悩む
p.60およびp.63のウォッチを作成・削除した後、一覧に戻るため「redirect_to :back」をしていますが、なぜかこれでテストが通りません。もちろん、ブラウザで実行した場合は、きちんと戻ってくれました。
expected: "/users/bob/messages" got: "/watchings" (using ==) (RSpec::Expectations::ExpectationNotMetError) ./features/step_definitions/web_steps.rb:260:in `/^(?:|I )should be on (.+)$/' features/watching_users.feature:18:in `ならば"bobのメッセージ一覧"ページを表示していること'
そこで、リダイレクト先のURLを生成してテストを通過させました。
APP_ROOT/app/controllers/watchings_controller.rb
class WatchingsController < ApplicationController before_filter :authenticate def create watching = current_user.watchings.create!(params[:watching]) redirect_to user_messages_url(watching.watchee.login) #redirect_to :back end def destroy watching = current_user.watchings.find(params[:id]).destroy redirect_to user_messages_url(watching.watchee.login) #redirect_to :back end end
config/route.rbの書き換え
Rails 3からconfig/route.rbの書き方が変わりましたので、/users/:login名/messagesなどは以下のようにしました。
root :to => 'messages#index' resources :users do resources :messages end resources :messages do collection do get :home end end resources :sessions resources :watchings
細かい所*1
- APP_ROOT/public/index.htmlは削除しておくこと。
- 「named_scope」は、「scope」に変更。
- 「<% form_tag」だとDEPRECATION WARNINGが表示されるので、「<%= form_tag」に変更。
- p.42 リスト5.1中の「でき」は、「前提」。
- p.52 リスト5.15の前段落、「features/support/trasform.rb」は、「features/support/transform.rb」。
- 2011年2月10日9時現在、p.87 用意されたCSSのURL「http://goo.gl/YmQM」にアクセスすると404エラーが発生。本書の最初に書いてあるGitHubのURL*2からapplication.cssを取得した。
- p.90の「lib/tasks/cucumber_profile.rake」は、「lib/tasks/cucumber.rake」。
余談
PDFをA4に小冊子印刷して、半分に折り、輪ゴムで止めて勉強してました。
Railsを理解した上でなら、集中してやれば2日でやれる分量で、Cucumberのとっかかりには最適な本だと思いました。ただし、RSpecによるテストの解説は無いので、Michael Hartl氏のRuby on Rails 3 Tutorial: Learn Rails by Exampleなどが別途必要になるでしょう。
The RSpec Bookのp.10に、BDDサイクルの図が掲載されていますが、いきなり第1章でこの図を見せられてもイマイチ、私の頭には入りませんでした。
しかし、『はじめる! Cucumber』の演習部分を一通りやった後、第9章のp.92にあるBDDサイクルについて読むと(以下、抜粋)、「あ、自分ならこうテストを書いて実装するな」というイメージがわきました。
1. 外側からの受け入れ条件をCucumberで記述する
2. RSpecを使いながら内部処理を実装していく
3. Cucumberで定義している受け入れ条件を満たしているか確認する
*1:第4章以前の部分につきましては、先日書いた『Cucumberをはじめてみた』をご覧下さい。
*2:https://github.com/moro/begin-cucumber-sample/raw/master/public/stylesheets/application.css
Cucumberをはじめてみた
昨日、『はじめる! Cucumber』(諸橋恭介著、達人出版界、2010年、v0.9.1)を買い、本のタイトルどおり、Cucumberを勉強しはじめました。とりあえず、第4章までやってみたので、Ruby on Rails 3での注意点など書きたいと思います。第5章以降は、また後日。
プロジェクトの作成
プロジェクト作成時、「-T」を付けてTest::Unitファイルの作成をスキップ。
$ rails new cuke-handson -T
Gemfile
gemコマンドでインストールするcucumberなどを、Gemfileに追加し、「bundle install」。
group :development do gem 'rspec-rails' gem 'cucumber-rails' gem 'i18n_generators' end group :test do gem 'rspec' gem 'cucumber' gem 'webrat' gem 'launchy' end
読み替え
ruby script/generateは、rails generateに。例えば:
$ ruby script/generate cucumber ja --webrat --rspec
これを、以下のように読み替えて実行しました。
$ rails generate cucumber ja --webrat --rspec
Webratのバグ修正*1
Webratがインストールされているディレクトリ*2にある、lib/webrat/core/session.rbを修正。
def current_host URI.parse(current_url).host || @custom_headers["Host"] || default_current_host end def default_current_host adapter.class==Webrat::RackAdapter ? "example.org" : "www.example.com" end
Cucumberのバグ修正*3
APP_ROOT/features/support/env.rb
require 'webrat' require 'webrat/core/matchers' Webrat.configure do |config| config.mode = :rack config.open_error_files = false # Set to true if you want error pages to pop up in the browser end World(Webrat::Methods) World(Webrat::Matchers)
細かい所
- 本書はRSpecによるテストは無いのですが、「bundle install」した後、「rails generate rspec:install」を実行。
- p.7の「RAILS_ENV=cucumber rake gems:install」はやらなかった。
- p.21でi18nジェネレータを実行すると、config/application.rbを上書きしようとするので「Y」をする。
- p.22でapp/views/messages/new.html.erbを編集しているけど、同ディレクトリの_form.html.erbを編集する。
- HTTP_ACCEPT_LANGUAGEヘッダを参照するはずなのに、p.24のlocalized_steps.rbで「HTTP_」が抜けてる。
- p.31のi18n_scaffoldが動かない。本書とは関係ありませんが、i18nジェネレータのlib/generatorsにi18n_scaffoldがないんですが...なんでだろう?
- p.32-33のヘルパーをApplicationHelperに書き、ApplicationControlerでincludeした。
「rake db:migrate」した後は、「rake db:test:prepare」した。本書には書いてないんですが、これって必要なかったのかなぁ...*4
余談
RSpecとCucumberの棲み分けというか、使い方の違いが段々と実感できるようになってきました。これはあくまでも私の主観で、間違っているかもしれませんが、Selenium IDEでやることをCucumberでやってる感じかなぁ...
*1:Webrat and Rails: Using assert_contain after click_button gives me “You are being redirected”
*2:「$ bundle show webrat」でわかります。
*3:undefined method `visit' for cucumber
*4:(2/10追記)APP_ROOT/lib/tasks/cucumber.rakeで、db:test:prepareしているので必要なかった。
コマンドラインでツイートするRubyスクリプト
komatagaさんのブログ記事、『非同期コミュニケーションツール』を読んで、「vimでプログラミング中でも、外部コマンド実行でツイートできたらなぁ...」と思い、作ってみました。
[追記:2/11 1:54]
Google URL Shortener APIでURLを短縮化するバージョンを作ってみました。-> 『コマンドラインでツイートするRubyスクリプト(つづき)』
ソース
#!/usr/bin/env ruby $KCODE = 'u' require 'fileutils' require 'yaml' require 'rubygems' require 'oauth' class Tw KEY = 'consumer_key' SECRET = 'consumer_secret' TOKEN = 'access_token' TOKEN_SECRET = 'access_token_secret' MAX_LENGTH = 140 UPDATE_URL = 'https://api.twitter.com/1/statuses/update.xml' attr_accessor :config def initialize config_file = File.join(File.expand_path('~'), '.twrc') begin @config = YAML.load(File.read(config_file)) rescue @config = { KEY => prompt(KEY), SECRET => prompt(SECRET), TOKEN => prompt(TOKEN), TOKEN_SECRET => prompt(TOKEN_SECRET) } File.open(config_file, 'w') {|f| f.write(@config.to_yaml)} FileUtils.chmod(0600, config_file) puts "saved to #{config_file}" end end def get_token consumer = OAuth::Consumer.new(@config[KEY], @config[SECRET], :site => 'http://api.twitter.com') token_hash = { :oauth_token => @config[TOKEN], :oauth_token_secret => @config[TOKEN_SECRET] } access_token = OAuth::AccessToken.from_hash(consumer, token_hash) end private def prompt(string) print string.gsub('_', ' ').capitalize + ":" gets.strip end end tw = Tw.new unless (status = ARGV.join(' ')).empty? && status.split('').length > Tw::MAX_LENGTH response = tw.get_token.post(Tw::UPDATE_URL, {:status => status}) exit(Net::HTTPOK == response) else exit(false) end
設定
アプリをTwitter Applicationsで登録してください。アプリケーションの種類は、「クライアントアプリケーション」です。
引数なしで起動し、Consumer Key、Consumer Secret、Access Token、Access Token Secretを入力してください。
Access TokenとAccess Token Secretは、アプリを登録した後、右サイドメニューにある「My Access Token」をクリックすると取得できます。
使い方
Twitterに投稿したいメッセージを引数として、起動してください。複数の引数は、半角の空白で連結します。ただし、半角括弧が文中にある場合は、文章全体をダブルクォーテーションで囲ってください。
注意点
当たり前のことですが、「AS IS」*1です。
なんでLokkaのプラグインを作っているのか
これまでLokkaのプラグインを...15個も作ってた...そもそも、私がLokkaのプラグインを作っている理由は:
- Sinatra/DataMapperの勉強
- 会社のHPをLokkaにしたい
素のLokkaは...
- カレンダーがない。→ lokka-calendar
- 画像ファイルをを投稿できない。→ URLを書けばできるけど、面倒。→ lokka-picasa_filesなど
- 管理画面の左サイドバーが長い。→ 表示中のページだけメニュー項目を表示する。→ lokka-elastic_admin_menu
この他、mixiチェック(lokka-mixi_check)とかGravatar画像(lokka-gravatar_image)などを作りましたが、これらはSinatraの勉強がてら作ったものです。
また、バク修正パッチなどをGitHubでPull Requestしてたら、Collaboratorとして私を登録していただき...それがモチベーションアップになってプラグイン作成に拍車がかかり...
気がついたら16個目を作ってました。
正規表現で指定したURLパスでアクセスがあった時、スラッグ(Slug)を指定してあるURLへリダイレクトするプラグイン「lokka-redirectors」を作ってみました。
これも、私の会社HPをLokkaにするのに必要なので作りました。会社の定款に「当社の公告は、電子公告によって行う。」と書いてあり、届け出たURLパスが「/company/statement」でした。Apacheなら.htaccessファイルでリダイレクトさせますが、Herokuで動かす場合はそれができません。したがって、「/company/statement」を「/company」にリダイレクトしたいなと。
インストール
データベースに「redirects」テーブルを作成する為、rake db:migrateをお忘れずに。
$ cd APP_ROOT/public/plugin $ git clone git://github.com/nkmrshn/lokka-redirectors.git $ cd lokka-redirectors $ bundle exec rake db:migrate
設定
プラグインをインストールしLokkaを(再)起動すると、Lokka管理画面の左サイドメニューに、「URLリダイレクト」という項目が表示されます。
左サイドメニューの[一覧]をクリックすると、登録してあるURLリダイレクトの一覧が表示されます。
URLリダイレクトの追加は、左サイドメニューの[登録]をクリックしてください。
余談
後は、メールフォームか...これが一番、面倒なんだよなぁ...マッシュアップで解決しようかなぁ...