clojureScript页面国际化(i18n多语言)实现

先看下效果(证明是实践过的)

中文

英文

国际化方案比较多,页面上的国际化一般比较简单,麻烦的还是数据库的数据的国际化。
本地cljs里的国际化采用前端db的atom控制当前语言,所有可见的翻译分为页面部分和数据库部分,最后在通过接口拿到数据库的翻译后跟页面的进行merge。

方案原理

  1. 将当前用户设定的语言保存在本地localstorage,并且保存在页面db中。
  2. 切换语言时,每个需要国际化的文案前增加i18n-str函数调用,实时获取目标文案对应的i18n文案。
  3. 浏览器被刷新时从localstorage中回复已经选择的语音类型。
  4. 可视的多语言文案,分为页面部分数数据库部分,在前端进行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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343