在開發 ROR 應用程式的時候,不免會考慮到 i18n 或 l10n 的問題。翻查了網路上的許多解決方案,最基本的當然就是 ruby-gettext。但這個 gettext 很麻煩的就是得要利用 updatepo 編譯 po 字詞翻譯檔,對於一邊開發一邊增修網頁來說實在煩不甚煩。後來找到這個 I18n 模組, 作者小影 依照 th0fu 在 他的 blog mir.aculo.us 中發表的 L10n 模組 的做法,稍作修改以 YAML 檔案來定義翻譯檔。這個比較符合我的需求,於是很開心地依照說明嘗試在我的環境安裝測試,結果不明原因地失敗,正要放棄,突然湧起一股天無絕人之路的勇氣,試著解讀小影的 I18n 的開放源碼,發現其實也沒有這麼難理解。只是小影可能將 I18n 考慮得比較完整,反倒有些地方複雜了。
我的需求其實很簡單,按照 GetText 的標準語法,在字串前面加上底線 _(),就能夠依照網頁存取瀏覽器的 HTTP_ACCEPT_LANGUAGE 來顯示最喜愛的語系,譬如這樣:
_'Hell world'
於是花了兩天的咖啡店時光(已經連續兩個禮拜白天泡在咖啡店裡研究新程式語言),寫出了以下這個 MultiLingual 模組。很興奮地寄給 hoja 分享,他果然比我厲害,稍作研究後修正了一些地方,使得語法更精練,並且讓模組的調用更簡單,甚至不需要變動到 Object 基本物件的定義,就能夠讓 controller 物件增加 _() 這個 method。
因為這個 MultiLingual 內容不太長,我就直接先 post 出來,有需要的人還是建議下載完整的檔案,裡面我寫了安裝說明,請自行參考。 由此下載 download
require 'yaml'
module MultiLingualdef _(string_to_localize, *args)
@lingual ||= lingual_load('default')
translated =
@lingual_map[string_to_localize] || string_to_localize
return translated.call(*args).to_s if translated.is_a? Proc
translated =
translated[args[0]>1 ? 1 : 0] if translated.is_a?(Array)
sprintf translated, *args
enddef lingual_load(ln=nil)
@lingual = ln.to_s.downcase
path = File.join(RAILS_ROOT, 'config', 'lang')
[File.join(path, "#{@lingual}.yml"), File.join(path, 'default.yml')].each do |file|
if File.exist? file
@lingual_map = YAML.load_file(file)
break
end
end
end
endclass LingualFilter
def self.filter(controller)
controller.lingual_load(controller.request.env['HTTP_ACCEPT_LANGUAGE'].to_s.scan(/\w*\-\w*/).first)
end
end
安裝方法也很簡單:
1.
編輯 "config/environment.rb",在檔案最後面加上以下兩行敘述
require 'multi_lingual'
include MultiLingual
2.
編輯 "app/controller/application.rb",在 class ApplicationController 定義裡面加入以下這行
before_filter LingualFilter
3. 在 config 目錄下新增一 lang 子目錄,裡面放置 .yml 語系翻譯檔,主檔名請依照小寫的語系編碼,例如 "zh-tw.yml", "zh-cn.yml", "en-us.yml"。檔案內容格式可依照 YAML 的方式,以下列方式來撰寫:
hello: 嗨
hell world: 見鬼了這世界
takol lose the job: 塔客贏得全世界
接下來就可以在 rhtml 檔案裡面這樣叫用:
<%=_ 'takol lose the job' %>
很簡單的模組,供有需要的朋友參考。當然也歡迎針對這模組的效能多加討論,日後也可以參照小影的思考方式,加上字典檔 (dictionary) 。我和 hoja 都不太能預測這種做法對於應用程式的整體效能影響如何,原來的做法是否可以將載入的翻譯 hash 雜湊表通用到 app 整體變數中,以避免每個 request 都載入一次語系檔,都有待大家給予我們意見。
最後,最重要的作者資訊:
Takol Liu [takol.liu@gmail.com]
Hoja Liang [hoja.tw@gmail.com]
GOOD, GP+1
我還是對於為了偷懶不加 ( ) 只寫 _'takol lose the job' , 結果照成 _(string_to_localize) Method 必須多這麼多行 code 出來, 感覺有點小礙眼.....
to hoja,
不能只把自己的 method 寫完就沒事呀,ruby 的 通暢特性,如果硬被擠在固定數量 argument 裡面而犧牲,這倒不太漂亮。其實我們參考的前二人做法,最美妙的地方就在他們的 _() 裡面,如果能夠花時間搞清楚他們的寫法,ruby 程式功力應該會長進吧。
我自己是看不太懂啦,
嗯...
我還是覺得這樣做不太好, 如果你有注意 rails api doc 裡面的範例, 大多數有 Variable-Length Arguments 的 Method 雖然都省略了開始結尾的 ( ) 但是其中的參數可都是包的好好的沒有在省略這 ( ) .
以 link_to 為例, rails 的範例應該是長這樣 :
link_to _('Hell world'), home_path
你期望的是長這樣:
link_to _'Hell world', home_path
這樣有幾個缺點:
第一, 會造成 "人" 閱讀上的誤解 .
(到底 home_path 是給 _() 的參數還是 link_to() 的易混淆)
第二, 多花心思在考慮各種情況回傳值的問題, 只會讓 code 更髒, 延長不必要的開發時間, 與 rails 的理念上不太合 .
不過要是當作練功的話就另當別論啦~
等哪天有空了再來想吧~ 
BTW, 原文的程式不難懂, 只是好像不是用來做你說的功能, 原文的 _() method 除了翻譯功能外還兼作 format 輸出 (sprintf), 回傳函式執行結果字串和取出 Array 值(?) 之類的雜事...很妙, 可能這段 code 只是他整個架構的一部份而已吧..
to hoja,
這就是我覺得奧妙的地方了。如果按照你先前給我的 _() code,理論上也應該只接受單一的argument,可是以你的 link_to 範例就是會出錯。改用原文的 _() 也不知道他在哪裡作怪,反正就是可以工作。我覺得只要搞通這點,用你 _() 裡的那兩行 code 倒也沒關係。
原文的 srintf 的確可以做到 _'%d balls', 5 這樣的格式化輸出,老實說的確比較方便,我想還是先保留好了。
至於你說的要不要加 () 來括住調用參數這點,這就有兩歧的看法了。有些人會覺得 easy-to-read 比較重要,有些人會覺得 cool-to-code 比較爽,例如 link_to _'Hell world', home_path 的確在閱讀理解上可以簡單地知道有個 Hell world 的翻譯詞來當作顯示字串,其餘的部份都與 link_to 無異。但若是想要 debug 時,這樣的寫法很難以去 trace,這倒也是實情。
啊啦啦啦啦~ 不管啦,就先這樣了,要改,就改個 v1.1 出來吧。
Hi!
之前被 Spamm Filter 擋住了你的留言, 遲了回應...
啊,用 HTTP HEADER 選 language 確是一個好用的功能!我怎麼沒有想到呢?
原本的 module 的安裝比較麻煩,我把它更新成為 plugin,比較接近一般 Rails plugin 的 standard,可以參考一下: ( http://code.google.com/p/mod-i18n/wiki/InstallationProcedure?updated=InstallationProcedure&ts=1183132468 )
to 小影,
謝謝回訪,稍後來研究一下再報告。