Friday, November 04, 2005

ri18n

คุณยุทธถามมาเรื่อง ruby module ri18n
ที่ใช้ทำ localization
ได้เรื่องเลย เรื่องนี้เป็นเรื่องที่ผม postpone มานานแล้ว
เห็นทำ app ภาษาไทยได้ก็พอใจแล้ว ยังไม่ได้มองกรณีทำหลายภาษาเลย

หลังจากสั่ง

gem install ri18n


ก็ได้ source code มาจำนวนหนึ่ง
ตัวเอกสารไม่ต้องพูดถึง มีน้อยมาก
การใช้งานพึงแกะจาก source code เป็นหลัก

Note: ผมแกะมาพอให้เริ่มใช้งานได้นะครับ
ยังมีรายละเอียดอีกจำนวนหนึ่งที่ยังไม่ได้ดู
เช่น catalog, interpolation, phurals form


การใช้งาน ri18n กับ RoR
  • Environment Setup
    แก้ไข file config/environment.rb
    เพิ่มเนื้อหาส่วนนี้ลงไป
    # Include your app's configuration here:
    require 'i18nservice'

    I18nService.instance.po_dir="#{RAILS_ROOT}/i18n"

    ให้สังเกตว่าเราจะมี directory ที่ชื่อ i18n ที่จะไว้ใช้เก็บ
    po file (translation file)
    ข้อสังเกตอีกอัน ก็คือ I18nService ใช้ sigleton pattern
    ดังนั้นเราจึง access ผ่าน instance attribute

  • การแสดง message ใน views
    ในการ render output ใน rhtml
    แทนที่จะเขียนออกไปตรงๆ เราจะใช้สัญญลักษณ์ _('msgid') แทนที่ msg ที่เราต้องการ
    <html>
    <head>
    <title>hello</title>
    </head>
    <body>
    <p>
    <%= _('hello') %>
    </p>
    </body>
    </html>


  • เพิ่ม Task i18n ใน Rakefile
    โดย ri18n ได้เตรียม Rake Task ที่ชื่อ gettext ไว้แล้ว
    gettext นี้มีหน้าที่ scan file เพื่อหา msg ที่ต้องการทำ translation
    เพื่อทำการ genereate .po file ให้เรา
    ...
    require 'gettext'
    require 'i18nservice'

    ...
    RAILS_ROOT=File.dirname(__FILE__)
    ...

    desc "generate po (i18n) file"
    Rake::GettextTask.new { |gettext|
    gettext.source_files = FileList['**/*.rhtml']
    gettext.new_langs = ['th','en']
    I18nService.instance.po_dir="#{RAILS_ROOT}/i18n"
    }

    desc "create directory for i18n po file"
    task :mki18ndir do
    Dir.mkdir("i18n") unless File.exists?("i18n")
    end

    task :i18n => [:mki18ndir, :gettext]

    จะเห็นได้ว่าเรามีการ config task class ที่ชื่อ Rake::GettextTask
    โดยระบุว่าให้ scan message ที่ต้องการแปลงจาก file ที่มีนามสกุลเป็น rhtml
    และระบุด้วยว่า ให้สร้าง po file สำหรับ language 2 ภาษา ก็คือ thai (th) กับ english (en)
    Note: ผมทำแบบ struts ก็คือ ใน views ระบุเป็น message id
    เราสามารถทำอีกแบบได้ ก็คือ ใน views เป็น english แล้วทำเฉพาะส่วนแปลเป็น th


  • generate po file
    โดยใช้คำสั่ง rake i18n
    จะได้ file ออกมา 2 file ก็คือ th.po กับ en.po

  • ทำการแปล
    โดยการ add message ที่ต้องการลงไปใน po file
    ขั้นนี้ผมยังไม่ได้ลองเรื่อง encoding เลย
    ที่ทดลองทำก็คือแค่ใช้ editor ที่ save encoding เป็น utf-8 ได้
    ทำการ edit file เพิ่มเนื้อหา ภาษาไทย ลงใน file th.po
    ส่วน file en.po ก็เพิ่มส่วน ภาษาอังกฤษ ลงไป
    ตัวอย่าง file th.po
    Ôªø# SOME DESCRIPTIVE TITLE.
    # Copyright (C) YEAR Free Software Foundation, Inc.
    # This file is distributed under the same license as the PACKAGE package.
    # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
    #
    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    "POT-Creation-Date: 2002-12-10 22:11+0100\n"
    "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    "Language-Team: LANGUAGE <LL@li.org>\n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=ASCII\n"
    "Content-Transfer-Encoding: 8bit\n"
    "Plural-Forms: nplurals=2; plural=n == 0;\n"

    msgid "hello"
    msgstr "สวัสดีครับ"

    Note: ผมยังไม่ได้ดูเรื่อง header ใน po file เลย
    ใช้ค่า default ไปก่อน (ซึ่งมันก็ทำงานได้)


  • การเลือกใช้ language
    เราสามารถเลือกใช้ language ที่ต้องการได้โดย
    การสั่ง I18nService.instance.lang='th'
    โดยในตัวอย่างนี้ผมใส่ไว้ใน controller โดย check ว่ามี
    parameter ที่ชื่อ lang pass มาหรือไม่ ถ้าไม่มีก็ให้ default เป็น thai
    class MainController < ApplicationController
    def index
    lang = params[:lang]
    lang = "th" if lang == nil
    I18nService.instance.lang=lang
    end
    end



ทำเสร็จแล้ว ก็ start server ทดสอบได้เลย
ผลลัพท์ที่ได้ก็เป็นที่น่าพอใจ


ส่วนประเด็นที่เหลือ ก็อาจจะเป็นการเรื่องการจัดการ po file
กรณีที่มีการแก้ไขเพิ่มเติม views แล้วเราสามารถ regenerate เป็นแบบ partial
ได้ไหม (อันนี้ยังไม่ได้ลองแกะดู แต่เท่าที่ลองสั่งมันมี message แปลก display ขึ้นมา)

Related link from Roti

การแสดง source code ใน Post

คุณ ohm ถามว่าเราจะแสดง xml tag, html tag ใน
post ของ blogger อย่างไร
เผอิญไม่มี email ของคุณ ohm ก็เลยขอตอบในที่นี้แล้วกัน

เริ่มแรกสุดก็คือเราจะแสดงผล xml หรือ html tag ใน post ได้อย่างไร
สมมติเราต้องการแสดงผลแบบนี้
<x>
<y>hello</y>
</x>

เวลาเราเขียน blog ก็ต้องทำการ encoding ให้เรียบร้อยก่อน
แล้วค่อยใส่ไว้ใน pre tag
หน้าตาของ post ที่จะแสดงผลเป็น xml แบบข้างบน ก็จะเขียนแบบนี้
<pre>&lt;x&gt;
&lt;y&gt;hello&lt;/y&gt;
&lt;/x&gt;</pre>


ซึ่งวิธีการทำแบบข้างบนนี้ ผมเขียน script find & replace เข้าไป editor
แล้วก็ทำ menu ไว้ เวลาจะแปลงก็เลือก highlight text ส่วนที่ต้องการ
แล้วก็เรียกใช้ script

ขั้นที่ 2 ก็คือ เกิดเราต้องการ syntax highlight หล่ะ
เราจะทำอย่างไร อันนี้ต้องหา tool เข้ามาช่วย
ผมเคย post วิธีที่ผมใช้แล้ว ลองอ่านดูนะครับ

ขั้นตอนเวลาผม post ส่วนที่เป็น source code
ผมก็จะเขียน code ลงใน jedit ก่อน
จากนั้นก็เรียกใช้ action จาก plugin ที่ผมเขียนขึ้น
ซึ่งมันจะทำการแปลง source code ให้อยู่ในรูป syntax highlight
จากนั้นก็ใส่ไว้ใน clipboard
เพื่อที่ผมจะได้ใช้คำสั่ง copy จาก clipboard ลง file ที่ต้องการได้เลย

กรณีที่คุณ ohm ไม่ถนัด java
ผมเคยใช้ perl module ที่ชื่อ vimcolor
ตัวนี้มันจะใช้ vim syntax feature ในการทำ syntax highlight

Note: กรณีต้องการ source code ของ plugin
ก็ให้ทิ้ง mail ไว้ครับ จะได้ส่งไปให้

Related link from Roti

Higher Order Messaging

อ่านเจอเรื่อง Higher Order Messaging ของ Mistaeks I hav Made
ก็เลยมาลองทำดูบ้าง เพื่อจะได้ซาบซื้งยิ่งขึ้น

ขั้นแรก ก็ต้องรู้ก่อนว่า Higer Order Message มีนิยามว่าอะไร
ถ้าตามไปดูใน paper ที่เขาอ้าง ก็จะพบประโยคนี้
Higher Order Messages allow user-defined message dispatch mechanism to be expressed using an optimally compact syntax that is a natural extension of plain messaging and also have a simple conceptual model.

คนอื่นเป็นไงไม่รู้ แต่ผมอ่านแล้ว ก็มึน
ดู definition อันนี้บ้าง
A higher order message is a message that takes another message as an "argument". It defines how that message is forwarded on to one or more objects and how the responses are collated and returned to the sender. They fit well with collections; a single higher order message can perform a query or update of all the objects in a collection.

ฟังดูง่ายขึ้นอีกนิด
ประมาณว่าเกี่ยวกับ การ update หรือ query Collections
โดยใช้กลไก forward message

ลองดูตัวอย่าง จะเห็นภาพกว่า
สมมติว่าเรามี Array ของ Class Person
ซึ่งภายในประกอบด้วย object จำนวนหนึ่ง
แล้วเราต้องการค้นหาชื่อเฉพาะบุคคลที่ยังเป็น Toddler (เด็กหัดเดิน)
(ตัวอย่างนี้ ได้รับแรงบันดาลใจจากลูกชาย)
ถ้าเขียนโปรแกรมแบบตรงเผงเลย ก็จะเขียนประมาณนี้
names = Array.new
for person in persons
if person.toddler? then
names << person.name
end
end
puts names


บรรดากูรูทั้งหลายเห็น code นี้แล้วบอกว่า ยาวไป
เห็นแล้วไม่เกิดแรงบันดาลใจ
ก็เลยไปทำการประดิษฐ์ block เข้ามาช่วย
code ข้างบนก็เลยกลายเป็น
puts persons.select { |p| p.toddler? }.collect {|p|  p.name} 


กูรูคนถัดไปเห็นแล้ว ก็บอกว่า block มันเกะกะ
อยากได้อะไรที่มันสร้างแรงบันดาลใจได้มากกว่านี้
ก็เลยเกิด Higher Order Message ขึ้น
ซึ่ง code ที่ได้จะเป็นดังนี้
puts persons.where.toddler?.extract.name


ตอนนี้ก็เลยเป็นหน้าที่ของโปรแกรมเมอร์อย่างเรา
ที่จะต้องเรียนรู้แล้วว่า HOM นั้น implement อย่างไร
จากตัวอย่างนี้ ผม implement ดังนี้ (จริงๆแล้ว ก็ลอกๆเขามานั่นแหล่ะ)

เริ่มด้วยสร้าง base class ก่อน
class HigherOrderMessage
def HigherOrderMessage.is_vital(method)
return method =~ /__(.+)__|method_missing/
end

for method in instance_methods
undef_method(method) unless is_vital(method)
end

def initialize(handler)
@handler = handler
end
end


จริงๆแล้ว class นี้จะเขียนแค่นี้ก็ได้
class HigherOrderMessage
def initialize(handler)
@handler = handler
end
end

ที่เขาใส่เพิ่มเข้ามา คงไว้ playsafe,
for loop ที่เขาใส่เข้ามาจะทำหน้าที่ block ไม่ให้ object ภายนอก
มองเห็น methods ของ HigherOrderMessage
ยกเว้นแต่ method ที่ชื่อ __send__ กับ __id__
ใครไม่รู้จัก __send__ ลองดูตัวอย่างนี้
[1,2].__send__('length') #=> 2
[1,2].length #=> 2

2 ประโยคข้างบนนั้นเท่าเทียมกัน

กลับมาดูกันต่อว่า where นั้น implement อย่างไร
class Where < HigherOrderMessage
def method_missing(id, *args)
return @handler.select {|o| o.__send__(id, *args)}
end
end

ก่อนอธิบายการทำงาน ต้องดูการนำไปใช้ก่อน
เนื่องจากเราต้องการให้ทำ persons.where ได้
แสดงว่า Array class ต้องมี method ที่ชื่อ where อยู่
ดังนันเราก็เลย declare mixin ขึ้นที่ Array class
class Array 

def where
return Where.new(self)
end

end


ซึ่งก็ใช้ได้ในตัวอย่างเรา
แต่ถ้าต้องการ generic กว่านั้นก็ควรไป declare ใน module Enumerable แทน
module นี้เป็นแหล่งรวม code ที่เกี่ยวกับ iterate collection
(ซึ่ง Array จะ include Enuerable อีกที)
module Enumerable 

def where
return Where.new(self)
end

end


การทำงาน ก็คือถ้ามีการเรียกใช้ where เมื่อไรก็ตาม ก็จะมีการ return Where object
กลับไป โดยภายใน where object จะมี handler ซึ่งเก็บ array object ไว้
และเมื่อมีการเรียกใช้ method อะไรก็ตาม (ดักโดยใช้กลไก method missing ของ ruby)
ก็จะทำการ loop collection นั้น
และ forward ชื่อ method ที่เข้ามาให้กับ object ใน collections นั้นอีกที
ในกรณีของเรา method toddler? ก็จะถูกใช้เป็นตัว screen ใน
select method

ส่วน extract ก็เขียนง่ายๆโดยใช้ collect ช่วย
class Extract < HigherOrderMessage
def method_missing(id, *args)
return @handler.collect {|o| o.__send__(id, *args)}
end
end


หลังจากลอง implement ดู แล้วย้อนกลับไปอ่าน definition อีกที
ก็พบว่า เริ่มซืมเข้าหัวมากขึ้นแล้ว

Related link from Roti

Thursday, November 03, 2005

Implement Bound properties with AspectJ

วันนี้อ่านเจอ post ของ damnhandy
ที่เขียนถึง JavaBean Aspect
โดยเขาตั้งโจทย์ว่า
เขาต้องการใช้ JGoodies เป็นตัวกลางในการ
bind hibernate persistent class เข้ากับ Swing component
ซึ่ง JGoodies Require ว่า class ที่ต้องการ bind
เข้ากับ Swing component ต้องเป็น Java Bean ที่ support
Bound properties (สามารถ add propertyChangeListener ได้)

ซึ่ง solution ที่เขาเขียนนั้นเขาใช้ JBoss AOP
วันนี้ผมก็เลยทดลองใช้ AspectJ implement ดูว่า
AspectJ สามารถทำ feature ประเภทนี้ได้ไหม

solution ของ damnhandy นั้น มี feature
พอสมควร มีการใช้ annotation เข้ามาช่วยด้วย
ผมจะตัด feature บางอันออกก่อน เพราะเราจะทำแค่
ทดลองชิม AspectJ ว่ามีรสประมาณไหนก่อน

โจทย์ที่จะทดลองทำ จะมีแค่
ในกรณีที่เรามี POJO class อยู่แล้ว
และต้องการ add feature bound properties เข้าไป
โดยใช้ AspectJ เราจะต้องทำได้อย่างไร

ในตัวอย่างของ AspectJ มีตัวอย่างเรื่องนี้อยู่แล้ว
ในหัวข้อ BoundPoint aspect
แต่เขาทำในลักษณะของ per class, per method
นั่นก็คือ ต้อง declare ทุก class, ทุก method ที่ต้องการ add bound properties
ผมก็เลยทดลอง declare ให้เป็น Generic มากขึ้นดู

สมมติว่าเรามี Class ที่ชื่อ Person
package domain;

public class Person {
private Long id;
private String name;
private String addresss;

public String getAddresss() {
return addresss;
}
public void setAddresss(String addresss) {
this.addresss = addresss;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}


เริ่มด้วย สร้าง interface ที่ชื่อ IValueObject ขึ้นมาก่อน
package domain;

import java.beans.PropertyChangeListener;

public interface IValueObject {

public void addPropertyChangeListener(
PropertyChangeListener listener);

public void addPropertyChangeListener(
String propertyName,
PropertyChangeListener listener) ;

public void removePropertyChangeListener(
PropertyChangeListener listener) ;

public void removePropertyChangeListener(
String propertyName,
PropertyChangeListener listener) ;

}


เตรียม Abstract Aspect ที่ implement method ในส่วนของ IValueObject
package domain;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

import org.apache.commons.beanutils.PropertyUtils;

public abstract aspect ValueObjectChangeAspect {

public PropertyChangeSupport IValueObject.support =
new PropertyChangeSupport(this);

public void IValueObject.addPropertyChangeListener(
PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}

public void IValueObject.addPropertyChangeListener(
String propertyName,
PropertyChangeListener listener) {
support.addPropertyChangeListener(propertyName, listener);
}

public void IValueObject.removePropertyChangeListener(
PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}

public void IValueObject.removePropertyChangeListener(
String propertyName,
PropertyChangeListener listener) {
support.removePropertyChangeListener(propertyName, listener);
}

abstract pointcut setter(IValueObject vo);

void around(IValueObject vo): setter(vo) {
String tmp = thisJoinPointStaticPart.getSignature()
.getName().substring("set".length());
String propName = tmp.substring(0, 1).toLowerCase()
+ tmp.substring(1);
try {
Object newValue = thisJoinPoint.getArgs()[0];
Object oldValue = PropertyUtils.getProperty(vo, propName);
proceed(vo);
vo.support.firePropertyChange(propName, oldValue, newValue);

} catch (Exception e) {
e.printStackTrace();
}
}

}

Aspect ValueObjectChangeAspect จะ delcare java.beans.PropertyChangeSupport instance
ที่จะไว้ใช้ช่วยทำ bound properties
โดยเราจะ declare pointcut ที่ชื่อ setter ไว้
ให้สังเกตุว่า pointcut นี้ยังเป็น abstract อยู่ นั่นก็คือว่ายังไม่มีการระบุให้ชัดว่า
จะเอาไปใช้กับ method อะไร,ที่ไหน
ในส่วนของ advice around จะ trig เมื่อ pointcut setter
ถูกเรียกใช้ ขั้นตอนการทำงาน ก็จะทำการหาค่า oldValue กับ newValue
(ใช้ common BeanUtils ของ apache เข้ามาช่วย)
เพื่อที่จะไว้ firePropertyChange
Note: โปรดระวัง code นี้ไม่สามารถใช้กับ production ได้
เพราะว่า ยังไม่ได้คิดว่า ถ้าเกิด exception แล้วจะทำอะไร


ถึงขั้นนี้ เราก็มี abstract aspect เตรียมไว้แล้ว ขั้นต่อไป ก็เป็นการ
ทำ Aspect PersonChangeAspect ที่เป็นการ implement IValueObject เข้ากับ Person Class
package domain;

public aspect PersonChangeAspect extends ValueObjectChangeAspect {

declare parents: Person implements IValueObject;

pointcut setter(IValueObject vo): call(void Person.set*(*)) && target(vo);

}

ข้างใน aspect ก็จะมีการ declare ว่าให้ Person ไป implement IValueObject เสีย
จากนั้นก็ declare pointcut setter ว่าให้ไปดักการทำงานของ
ทุก method ที่มีชื่อขึ้นต้นว่า set ใน class Person

สุดท้ายก็ทดสอบโปรแกรมดูดังนี้
package domain;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

public class TestRun implements PropertyChangeListener {

/**
* @param args
*/
public static void main(String[] args) {
// initialize object
Person p1 = new Person();
p1.setName("x");

IValueObject vo = (IValueObject) p1;
vo.addPropertyChangeListener(new TestRun());

p1.setName("y");
}

public void propertyChange(PropertyChangeEvent evt) {
System.out.println(evt.getPropertyName());
System.out.println(evt.getOldValue());
System.out.println(evt.getNewValue());
}

}


Output ที่ได้ ก็เป็นดังนี้

name
x
y

Related link from Roti

Wednesday, November 02, 2005

ทำไมไม่ใช้ RoR ในโปรเจคใหญ่

ผมเคย post ไว้ว่า จะเลือกใช้ RoR ในกรณีที่ project เล็กๆ
แล้วก็มี comment ถามเข้ามาว่า ทำไมถึงเลือก choice นั้น
ใช้เหตุผลด้านไหนเป็นตัวตัดสิน

วันนี้อ่านเจอ blog ของ Glenn Vanderburg
หัวข้อเรื่อง The Right Team for Rails
เขาเขียนได้ตรงใจทีเดียว

A good Rails team will consist of programmers who know what they should be doing, and usually have the discipline to do it.


The wrong Rails team is one that doesn’t understand those principles and practices. The fact that Rails makes things easy won’t be enough. In my experience, such teams expend amazing effort and ingenuity to do the wrong thing.


กรณีของผม ที่ผมวางแนวทางไว้ก็คือ
ถ้าเป็น developer มือใหม่ เข้ามาในบริษัทฯ
ผมจะยังไม่ให้ใช้ RoR เลย
จะให้หัดใช้ mvc แบบ Struts ให้เข้าใจ framework เสียก่อน
,ใช้ hibernate ได้อย่างไม่เก้อเขิน
,มอง springFramework เป็นเหมือนสวนหลังบ้าน (เปรียบเทียบเวอร์ไปนิด)
ให้ดี ก็ต้องใช้ tapestry เป็นด้วย (จะได้ซาบซื้งกับหลักการ reuse component)
แล้วก็ผ่านการพัฒนา project ด้วย java มาอย่างน้อยสัก 1 project ก่อน
คราวนี้แหล่ะที่พร้อมจะใช้ RoR ได้แล้ว
(ความหมายของคำว่า "พร้อม" นี้
หมายถึงพร้อมที่จะร่วมพัฒนาระบบงานใหญ่ๆ พร้อมๆกันเป็น team)

แก้ไขเพิ่มเติม ลืมบอกลักษณะของโปรเจคไป
โปรเจคที่ผมทำส่วนใหญ่เป็น web application ทีเป็นพวก Data Entry
มี form เยอะแยะเต็มไปหมด (ส่วนใหญ่จะ replace terminal app หรือ client-server
app ของเดิม

Related link from Roti

Tuesday, November 01, 2005

รูปจาก trip เที่ยวใต้

รูปนี้เป็นรูปอุปกรณ์จัดปลาหมึกของชาวบ้าน
ที่ปราณบุรี
(ใช้หอยร้อยเชือกเป็นสายยาว
เวลาใช้ก็ปล่อยลงไปในน้ำ
เมื่อกู้ขึ้นมา ปลาหมึกจะติดอยู่ในเเปลือกหอย)


ส่วนรูปนี้เป็นรูปปูสูงอายุ ถ้าทางจะอายุมากเชียวหล่ะ

Related link from Roti