先看下效果(证明是实践过的)
国际化方案比较多,页面上的国际化一般比较简单,麻烦的还是数据库的数据的国际化。
本地cljs里的国际化采用前端db的atom控制当前语言,所有可见的翻译分为页面部分和数据库部分,最后在通过接口拿到数据库的翻译后跟页面的进行merge。
方案原理
- 将当前用户设定的语言保存在本地localstorage,并且保存在页面db中。
- 切换语言时,每个需要国际化的文案前增加
i18n-str
函数调用,实时获取目标文案对应的i18n文案。 - 浏览器被刷新时从localstorage中回复已经选择的语音类型。
- 可视的多语言文案,分为页面部分数数据库部分,在前端进行merge处理,保存成一个。
前提
前端使用re-frame、kee-frame、shadow-cljs、antd框架
当前方案核心代码
分页页面部分,切换语言部分(保存db,保存localstorage,防止手动刷新页面时数据还原)。
1. 页面切换代码
继续使用antd组件
(def lang (rf/subscribe [:i18n/lang]))
;; 语言
(def ^:private language
{:zh-cn "中文"
:en-us "English"
:ja-jp "日本語"})
;; 语言菜单
(defn- dropdown-menu []
[:> ant/Menu
{:className "menu"
:onClick (fn [menu]
(let [value (js->clj menu :keywordize-keys true)]
(rf/dispatch [:i18n/change-lang (keyword (:key value))])))}
[:> MenuItem {:key "zh-cn" :title "中文"}
[:span (i18n-str "中文")]]
[:> MenuItem {:key "en-us" :title "英文"}
[:span (i18n-str "英文")]]
[:> MenuItem {:key "ja-jp" :title "日文"}
[:span (i18n-str "日文")]]])
[:div {:style {:margin-left 20
:font-size "14px"
:font-family "PingFangSC-Medium,PingFang SC"
:font-weight 500
:color "rgba(0,0,0,1)"}}
[:> ant/Dropdown {:overlay (reagent.core/as-element [dropdown-menu])}
[:span (i18n-str (or (get language @lang) "中文"))]]]
2. 切换和保存当前语言
;;通过key设置和获取localstorage里的数据
(defn set-local-storage [key value]
(.setItem js/localStorage key value))
(defn get-local-storage [key]
(.getItem js/localStorage key))
;;只要路由变化,就要触发获取当前语言的逻辑
(kf/reg-controller
:lang-controller
{:params (constantly true)
:start [::set-lang-by-local]})
;;如果页面刷新的话从localstorage里获取
(kf/reg-event-fx
::set-lang-by-local
(fn [_ [_ _]]
(when-not @(rf/subscribe [:i18n/lang])
(if (get-current-lang)
(rf/dispatch [:i18n/change-lang (get-current-lang)])
(rf/dispatch [:i18n/change-lang :zh-cn])))
{:dispatch [:request/get {:url (:get-lang-map mutil-lang)
:params {:hostname (.. js/window -location -hostname)} ;;此处根据当前域名获取该域名的对应租户的多语言文案
:callback-event ::save-db-lang}]}))
(kf/reg-event-fx
::save-db-lang
(fn [{:keys [db]} [db-lang-map]]
{:db (-> db
(assoc-in [:db-lang-map] db-lang-map))}))
(rf/reg-event-fx
:i18n/change-lang
(fn [{:keys [db]} [_ data]]
(js/console.log "切换语言到:" data)
(set-current-lang data)
{:db (assoc-in db [:global :lang] data)}))
(rf/reg-sub
:i18n/lang
(fn [data]
(get-in data [:global :lang])))
(rf/reg-sub
:i18n/db-lang-map
(fn [db]
(get-in db [:db-lang-map])))
(defn- merge-lang-map
"对页面上的文案和db里的文案进行一次merge"
[page-lang-map db-lang-map]
(if db-lang-map
(merge page-lang-map
(#(zipmap (map :key %) (map :value %))
db-lang-map))
page-lang-map))
;;返回当前语言的关键字
(defn i18n-str [s]
(let [lang (rf/subscribe [:i18n/lang])
db-lang-map (rf/subscribe [:i18n/db-lang-map])]
(get-in (merge-lang-map language-map @db-lang-map)
[s @lang] s)))
3. 页面文案翻译
上面代码里用到的language-map
类似如下结构:
(def language-map
{ "切换语言" {:en-us "Switch language" :ja-jp "言語を切り替え"}
"中文" {:en-us "Chinese" :ja-jp "中国語"}
"英文" {:en-us "English" :ja-jp "英語"}
"日文" {:en-us "Japanese" :ja-jp "日本語"}
"体验门店" {:en-us "Experience Store" :ja-jp "店を体験する"}
"返回首页" {:en-us "Back to Home" :ja-jp "ホームを戻す"}}
)
4. 数据库返回的文案
即上文中:i18n/db-lang-map
这个event从db中获取的对象,从接口获取的存在前端db中数据结构如下:
[
{
"key": "双排六粒",
"value": {
"en-us": "Double six buttons",
"ja-jp": "w6*3"
}
},
{
"key": "下摆(成衣)",
"value": {
"en-us": "Bottom(garment)",
"ja-jp": "蹴廻し(上がり寸法)"
}
},
{
"key": "平钉纽扣",
"value": {
"en-us": "Level buttons",
"ja-jp": "平钉钮釦"
}
}
]
这样将数据库中的和页面上的进行merge后使用。
当然,我们产品是因为对多个租户,各租户的翻译不同,所以页面上没有往DB里重复保存,采用merge两端的形式。简单的可以只在数据库维护。
改进点
- 页面文案便于扩展新语言
当前三个语言,在数据库采用一行保存一个形式
+----------------+--------------+------+-----+-------------------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+--------------+------+-----+-------------------+-------+
| id | varchar(40) | NO | PRI | NULL | |
| company_id | varchar(40) | NO | | NULL | |
| lang_key | varchar(255) | NO | | NULL | |
| lang_value | varchar(255) | NO | | NULL | |
| lang | varchar(40) | NO | | NULL | |
| delete_flag | varchar(4) | YES | | 0 | |
| create_time | timestamp | NO | | CURRENT_TIMESTAMP | |
| create_user_id | varchar(40) | YES | | NULL | |
| update_time | timestamp | NO | | CURRENT_TIMESTAMP | |
| update_user_id | varchar(40) | YES | | NULL | |
+----------------+--------------+------+-----+-------------------+-------+
一个文案的翻译数据如下:
INSERT INTO `t_store_language`(`id`, `company_id`, `lang_key`, `lang_value`, `lang`, `delete_flag`, `create_time`, `create_user_id`, `update_time`, `update_user_id`) VALUES ('611348', '61', '常规(9个工作日)', 'Regular (9 working days)', 'en-us', '0', '2020-04-01 00:00:00', NULL, '2020-04-01 00:00:00', NULL);
INSERT INTO `t_store_language`(`id`, `company_id`, `lang_key`, `lang_value`, `lang`, `delete_flag`, `create_time`, `create_user_id`, `update_time`, `update_user_id`) VALUES ('611359', '61', '常规(9个工作日)', '普通(9稼動日)', 'ja-jp', '0', '2020-04-01 00:00:00', NULL, '2020-04-01 00:00:00', NULL);
这个是便于扩展的,而页面上就不是那样的,如同上面的language-map
,如果再增加一门比如韩语的话,需要逐项在原来的数据上修改,不利于扩展。
改进方向:
一个语音一个map,最后将多个语言的文案进行合并
仓促下没有考虑太多,如有更好的方案,欢迎交流。QQ:389709260