後台系統在做 i18n 時,常見的問題是在處理翻譯資源怎麼下載的問題。一開始專案小,大家通常會把所有翻譯塞成一包,或至少塞成少數幾包,先能跑再說。但系統一長大,首次載入變慢、翻譯檔越來越難維護。
這篇文章想用比較平實的方式,整理三個概念:
單一包翻譯的缺點
把翻譯塞成一包最大的好處是簡單:不用想 namespace,不用想載入時機,也不用管快取。但後台系統為了不能打斷使用者的操作流程,會需要 runtime 切換語言,這時候一大包翻譯就很容易出現幾個實務問題:
如果是比較不需要操作的網站,比如官網,reload 就沒關係。但後台的動態載入就更重要。
路由層 lazy-load 是什麼?
路由層的做法很直覺:進入某個大功能區(route / feature)時,才載入該大功能需要的所有翻譯資源。例如進到 Inventory 才載 inventory 的翻譯包,進到 CRM 才載 crm 的翻譯包。
這種做法的優點是心智模型清楚,也容易在「進頁面之前」把翻譯載好,因此比較容易避免畫面先出現 key 再補翻譯的情況。
但缺點也很常見:
後台很少是「每個 route 都是一個封閉世界」。共用元件很多,像 table、toolbar、dialog、menu 這些會跨路由使用。只靠 route 層載入,最後很容易走向兩種結果:
翻譯包越做越大(反正路由會載,就把共用的一起塞);
或是某些元件在某些地方突然缺字典(尤其是動態彈窗、overlay);
元件層 lazy-load 是什麼?
元件層 lazy-load 的核心概念是:元件自己宣告它需要的 namespace,元件第一次被用到時才載入。這對後台的好處是,因為後台 UI 共用元件多,很多元件是跨 route 共用、甚至動態生成的。
元件層做得好的話,會得到幾個好處:
但元件層最大的坑也很固定:重複載入。
同一個元件可能在同頁出現多次、或多個元件同時掛載都需要同一份 namespace。沒有快取的話,你會看到一堆一模一樣的請求。
也因此,元件層 lazy-load 幾乎一定要搭配載入快取的設計。
載入快取怎麼設計?
這裡最重要的是先把 cache key 定義清楚。通常最直覺也最安全的是用:
cache key = 語言 + namespace
例如:
en:inventory
zh-Hant:inventory
這樣切語言不會互相覆蓋,而且切回來也不需要重新載入。
但實務上只做「loaded cache」還不夠。因為很多重複請求不是「先載完又載」,而是「同時一起載」。例如某頁面上同時掛載 10 個 component,如果你只在載完後才把結果寫進 cache,那 10 個元件會各自發 10 次請求。
所以通常會再加一層:下載中的去重(載入中也要 cache)。
概念是:
閃key
另外,處理翻譯有個細節,就是剛進頁面時會不會閃 key
lazy-load 一定會碰到 UI 時序問題 -- 翻譯還沒載到,畫面先 render,於是你會看到 raw key。使用者會覺得系統是不是有壞掉,感覺不專業。
處理的方法有很多種,我自己比較偏向在翻譯 ready 之前不要 render 那一塊 UI,或先顯示空白區塊。路由層可以在進頁前 preload,元件層就靠 ready 狀態決定 render 時機。
ngx-atomic-i18n 的出發點
我在做 ngx-atomic-i18n 時,主要就是針對後台的這類需求:
它不是要涵蓋所有 i18n 場景,而是比較專注在 admin/dashboard 這種「使用中切語言」且 UI 組裝很複雜的系統。如果有興趣的話可以來試試看:https://www.npmjs.com/package/ngx-atomic-i18n
有什麼想法很歡迎一起討論。
小結
如果只做路由層 lazy-load,專案小時可能夠用,但一旦共用元件與動態 UI 多起來,很容易不夠精準。元件層 lazy-load 更符合後台的現實,但它一定會逼你正視快取設計,特別是 inflight 去重。