Vue3 i18n Unit test 與 CSP 問題

tags: Vue i18n CSP
category: Front-End
description: Vue3 i18n 測試與 CSP (Content Security Policy) 問題
created_at: 2023/09/01 15:30:00

cover image


前言

最近在做學校系統的 i18n,做完之後發現跑測試就掛掉了,然後修好讓測試跑過之後,嘗試 build 之後跑看看,發現了 CSP 的問題,所以就來紀錄一下碰到的問題跟解決方式,未來在碰到就回來看

使用的測試套件是 vitest,然後用 Vue3 原生的 test-utils 來測試。

然後這個專案的 vite 版本為 3.x,所以後面用到的套件會是支援 vite 3.x 的版本。


i18n 在測試碰到的問題

問題一

錯誤訊息(節錄)

SyntaxError: Need to install with `app.use` function

因為忘了在 mount 的時候加上 i18nplugin,所以你需要在 mount 的時候加上 i18nplugin,像是下面這樣:

註: 只是示意,不是完整的程式碼

import { mount } from '@vue/test-utils';
import { createI18n } from 'vue-i18n';
import { messages } from '@/i18n';

const i18n = createI18n({
    locale: 'zh-TW',
    messages,
});

const wrapper = mount(Component, {
    global: {
        plugins: [i18n],
    },
});

或是在全域設定 i18nplugin,像是下面這樣:

import { config } from "@vue/test-utils";

config.global.plugins = [i18n];

問題二

錯誤訊息(節錄)

TypeError: _ctx.$t is not a function

template 當中直接使用 $t() ,但是他沒抓到 $t,所以你需要給他 mock 一下,這邊一樣簡單掛到 global 上面,像是下面這樣:

import { config } from "@vue/test-utils";

config.global.mocks = {
  $t: (msg) => msg,
};

如果你的測試資料不存在 i18n 設定的 messages 當中,會跳出 [intlify] Not found 'text' key in 'xx' locale messages. 這個訊息,而用這個方式也可以另類的解決他。 (畢竟就跳過了)

我是覺得測試的時候不太 care 這個問題,除非你有特別的目的,不然就直接 mock 掉就好了。


CSP 問題

當前面測試都通過了,這時就可以 build 看看了,然後發現了 CSP 的問題。前提是你有設定,不然就不會有問題

錯誤訊息(節錄):

EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive

這個可以看官方,他有提到這個問題,然後他也有提供解決方式,主要是這一段:

IF CSP is enabled, vue-i18n.esm-bundler.js would not work with compiler due to eval statements. These statements violate the default-src 'self' header. Instead you need to use vue-i18n.runtime.esm-bundler.js.

所以你需要改用 vue-i18n.runtime.esm-bundler.js,然後你可以在 vite.config.js 裡面加上下面這段:

import { defineConfig } from 'vite';

export default defineConfig({
    resolve: {
        alias: {
            "@": fileURLToPath(new URL("./src", import.meta.url)),
            "vue-i18n": "vue-i18n/dist/vue-i18n.runtime.esm-bundler.js", // <-- 加入這行
        },
    },
});

然後下 build 之後測試,正當我以為一切順利,後來發現他翻譯的怪怪的(雖然過了CSP)

雖然沒有錯誤訊息,不過就是我們知道同個 key 可以透過 | 來做分隔,然後使用上可以指定要用哪一個翻譯,但是他卻把全部輸出出來,例如:

{
    "foo": "bar | baz"
}

理論上我應該只需要 bar 或是 baz,但是他卻把全部輸出出來,像是下面這樣:

bar | baz

這並不是我要的,所以還需要再處理。

接著我將文件往下翻且嘗試了一下,發現根本不需要做上面的事情XD

接著要使用一個 viteplugin,名字叫做 vite-plugin-vue-i18n,因為這邊 vite 版本是 3.x,所以使用這個。

vite.config 裡面加上下面這段:

import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [
        vueI18n({
            include: resolve(
                dirname(fileURLToPath(import.meta.url)),
                "./src/assets/locales/**"  // <-- your locale directory path
            ),
        }),
    ],
});

然後我跑了 run dev發現一切如此的美好,接著我要 build 來看看成效,卻..

[vite:load-fallback] Could not load vue-i18n/dist/vue-i18n.runtime.mjs (imported by src/main.js): ENOENT: no such file or directory, open 'xxx'
error during build:
Error: Could not load vue-i18n/dist/vue-i18n.runtime.mjs (imported by src/main.js): ENOENT: no such file or directory, open 'xxx'

這時去看看他的設定,發現這個 plugin 預設只跑在 runtime,而我希望 build 的時候也跑,所以多一條設定:

import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [
        vueI18n({
            include: resolve(
                dirname(fileURLToPath(import.meta.url)),
                "./src/assets/locales/**"  // <-- your locale directory path
            ),
            runtimeOnly: false, // <-- 加入這行
        }),
    ],
});

這時在 build 就會過了。


題外話

  • Q: 我是怎麼在 Local 測試 CSP
  • A: 我為前端專案包了一個 Dockerfile,然後在 nginx 裡面加上 CSPheader 設定,這樣基本上我 local 沒問題,部署到 server 上面也應該不會有問題。

結論

其實這是開會的時候提到 i18n ,本來預估是明年中才要上線,但我說我已經做好且 push 到一個 feature 的分支上了,只是還沒發 pr 併到測試機,然後就決定要放上測試機給大家測一測,如果沒問題也能提早上,就碰到了這些問題。

因為這個專案當時沒有導入 huskygit hook,而我當初想說只是套套 i18n,我又沒對測試寫太多過於死的 test case,應該不會碰到什麼問題,所以其實有點偷懶沒在 local 先跑過測試,最後在 CI 階段被我發現,就立刻把他修正。

然後 CSP 也是沒想到會中,就剛好趁這機會一次把他搞定。

偷偷工商: 安裝 husky 或其他開發套件到你的前端專案,也可以透過我做的 lai-cmd,用法非常簡單。

在我合併回 main 之前,都要下 @dev 的標籤,如下:

$ npx lai-cmd@dev init



最後更新時間: 2023年09月01日.