場景:對(duì)于一個(gè)已經(jīng)做好靜態(tài) I18n 的 Rails 項(xiàng)目,需要對(duì)動(dòng)態(tài)數(shù)據(jù)內(nèi)容也適配國際化。
首先,動(dòng)態(tài)內(nèi)容的數(shù)據(jù)肯定是存在數(shù)據(jù)庫中的,并且字段名也采用統(tǒng)一的 fieldname_#{I18n.locale} ,方便統(tǒng)一管理。
問題的核心是如何在盡量不修改現(xiàn)有代碼的情況下動(dòng)態(tài)讀取模型某些字段的當(dāng)前 locale 的值。
比如:原來的 user.name #=> Marry ,現(xiàn)在需要根據(jù)當(dāng)前 locale (比如 cn)變成 user.name # => 翠花 。
首先接口肯定不能改,view 層中有很多很多的 user.name 的調(diào)用,就算用批量修改的方式改為類似 user.name_#{I18n.locale} 的寫法,
也很不優(yōu)雅,何況不只是 name 屬性需要國際化,以后每增加一個(gè)字段或模型的國際化都將成為很大的負(fù)擔(dān)。
說到如何在原有類的基礎(chǔ)上增加功能,那自然會(huì)想到使用裝飾器模式了。
關(guān)于裝飾器模式的實(shí)現(xiàn),一種方式是使用 delegate :
require 'delegate'
class User < ApplicationRecord
# attribute name
end
class InternationalUserDecorator < SimpleDelegator
def name
__getobj__.send("name_#{I18n.locale}")
end
end
user = InternationalUserDecorator.new(User.find(12345))
I18n.locale = :en
user.name # => 'Marry'
I18n.locale = :cn
user.name # => '翠花'
但是這顯然是不行的,因?yàn)檫@需要修改每一個(gè) User 實(shí)例的生成,使用 Decorator 去顯式地包裝它。
類似的,還有一種通過繼承 module 的方式,同樣需要顯示的修改每一個(gè)模型的實(shí)例,這樣的改動(dòng)對(duì)原代碼改變很大,也不能使用。
module EnglishUser
def name
"Marry"
end
end
module ChineseUser
def name
"翠花"
end
end
user = User.find(123)
user.extend(EnglishUser) #=> name "Marry"
user.extend(ChineseUser) #=> name "翠花"
可見,需要在獲取模型實(shí)例時(shí)進(jìn)行修改的思路是行不通,如果項(xiàng)目一開始就使用倉儲(chǔ)模式的話,修改起來會(huì)容易很多,不過這超出了本文的范圍。
想要盡可能小的修改原代碼,那只能使用元編程了,我們需要一個(gè) Module,來動(dòng)態(tài)生成 locale 對(duì)應(yīng)的 field 供模型調(diào)用。
當(dāng) locale 是 cn 的時(shí)候, user.name => user.name_cn
當(dāng) locale 是 en 的時(shí)候, user.name => user.name_en
當(dāng)然,不是模型所有的 field 都需要做國際化,必須可以指定需要國際化的字段
client 端的代碼應(yīng)該是這樣的:
class User < ApplicationRecord
# attributes :name, :position, :age
include I18nDecorator.new(:name, :position)
end
這里的難點(diǎn)在于,需要傳參數(shù)給這個(gè) Module,可 include 的時(shí)候是不能傳參的。
在這里,我們把 I18nDecorator 定義為 Module 的一個(gè)子類,這樣就可以通過 new 的時(shí)候的 initialize 方法中,
對(duì)父類進(jìn)行元編程,動(dòng)態(tài)定義 Module 的方法,這樣就能在模型中進(jìn)行調(diào)用了。
class I18nDecorator < Module
def initialize(*attrs)
super() do
attrs.each do |attr|
define_method attr do
send("#{attr}_#{I18n.locale}")
end
end
end
end
end
現(xiàn)在,當(dāng)任何 User 類的實(shí)例調(diào)用 name 或者 position 的方法時(shí),就會(huì)被 I18nDecorator 動(dòng)態(tài)轉(zhuǎn)發(fā)給 name_cn 或者 position_en 的屬性上了。
就這樣,一共十幾行代碼,我們完成了一個(gè) Rails 項(xiàng)目簡單的動(dòng)態(tài) I18n 的功能,以后需要增加模型或?qū)傩缘臅r(shí)候,都只需要 include 這一行代碼就可以了。