當一個網站需要面向不同國家或不同語言的使用者,我們就需要做國際化 (Internationalization),將網站內容進行翻譯或語言的對應,讓使用者能理解網頁上的操作或內容,I18n 意謂著「Internationalization」這個單字中,I 和 n 之間有 18 個字母,也正是各個框架的實作多國語系套件的一個常用名稱,例如 Vue 生態就有 vue-i18n,而這篇文章將講述如何在 Nuxt 3 中整合 Vue I18n 來協助實作多國語系。
想要在 Vue 3 使用 Vue I18n 獲得比較好的支援度需要使用正處於 Beta 階段的 v9 版本,因此有幾種方式可以在 Nuxt 整合 Vue I18n,分別是依照 Vue I18n 官方指引、使用 Nuxt Community提供的 @nuxtjs/i18n@next 模組及 @intlify/nuxt3 模組。
比較早以前,Nuxt 3 社群模組還沒有支援比較新的 Vue I18n,所以需要自己額外的製作插件來安裝,後來也有一些相容性的問題,所以 Vue I18n v9 的作者 kazupon 發布了 @intlify/nuxt3 模組來方便 Nuxt 3 整合vue-i18n-next (Vue I18n v9),目前仍然可以使用 @intlify/nuxt3 來進行整合,但我最後選擇使用@nuxtjs/i18n@next 模組,除了看好是由Nuxt Community 提供的支援及未來的發展外,也能使用到 @nuxtjs/i18n 提供的一些組合函數等。
目前 @nuxtjs/i18n
模組的穩定版本在 v7.3.0,而使用 Vue 3 和 Vue I18n v9 的模組則是下一個迭代版本 v8,所以使用 NPM 安裝的時候需要稍微注意一下。
下圖於 2022/10/10 擷取自 NPM - @nuxyjs/i18n
使用 next
標籤來安裝 v8 版本
npm install -D @nuxtjs/i18n@next
於 nuxt.config.ts
中的 modules
屬性添加 '@nuxtjs/i18n'
,參考如下:
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n']
})
當添加完模組後,我們可以先設置 @nuxhjs/i18n 模組的選項,來測試是否能正常運作,於 nuxt.config.ts
中,添加 i18n
屬性,用以設置**@nuxhjs/i18n** 模組;i18n.vueI18n
設置的選項也可以參考 Vue I18n v9 官方文件。
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
vueI18n: {
legacy: false,
locale: 'zh',
messages: {
en: {
hello: 'Hello!',
language: 'Language'
},
zh: {
hello: '你好!',
language: '語言'
}
}
}
}
})
記得設置
i18n.vueI18n.legacy
為false
來關閉使用較舊的 API 模式。
pages
目錄當完成 Step 3. 的模組設置後,就可以啟動伺服器來看看效果;不過若是 Nuxt 3 專案目錄下缺少了 pages
目錄,可能會無法啟用 Vue Router 自動產生路由,而出現下圖的錯誤。
我們只需要新增 ./pages/ingex.vue
路由頁面,就可以解決錯誤囉!
我們安裝與配置好 @nuxtjs/i18n 後,可以嘗試建立頁面來看看效果,以下以 en
與 zh
作為英語和繁體中文兩個語系的使用範例。
新增 ./pages/index.vue
內容如下:
<template>
<div class="flex flex-col items-center bg-white">
<h1 class="mt-48 text-8xl font-medium text-blue-500">
{{ $t('hello') }}
</h1>
</div>
</template>
我們就可以使用 $t()
來傳入我們在 nuxt.config.ts
中的 i18n.vueI18n.messages
定義語系與對應的文字,而我們預設是 zh
繁體中文語系,畫面上就出現 hello: '你好!'
對應的「你好!」文字囉!
新增按鈕來實現切換不同的語系,完整的程式碼如下:
<template>
<div class="flex flex-col items-center bg-white">
<h1 class="mt-48 text-8xl font-medium text-blue-500">
{{ $t('hello') }}
</h1>
<div class="mt-24 space-x-4">
<button
type="button"
class="inline-flex items-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="setLocale('en')"
>
English
</button>
<button
type="button"
class="inline-flex items-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="setLocale('zh')"
>
繁體中文
</button>
</div>
<div class="mt-4 flex flex-row justify-center">
<label class="text-gray-600">{{ $t('language') }}</label>
<span class="ml-4 font-bold text-gray-800">{{ locale }}</span>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
</script>
若沒有特殊需求,切換語系你可以選擇直接設定 locale
來自動響應或使用 setLocale()
來設定語系。
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, setLocale } = useI18n()
locale.value = 'en'
// or
setLocale('en')
</script>
完成後我們就可以切換 en
與 zh
語系,當然你也可以製作成選單來放置更多的語系來支援切換。
隨著專案增大,語系的翻譯數量也會增多,而全部放在 nuxt.config.ts
中也實在太過冗長,所以我習慣獨立放置在一個資料夾內做處理。
稍微調整一下 nuxt.config.ts
如下:
export default defineNuxtConfig({
modules: [@nuxtjs/i18n'],
i18n: {
langDir: 'locales',
locales: [
{
code: 'en',
iso: 'en-US',
file: 'en.json'
},
{
code: 'zh',
iso: 'zh-TW',
file: 'zh.json'
}
],
defaultLocale: 'zh',
strategy: 'no_prefix'
}
})
接著我們就可以建立各個語系放置的資料夾對應著 i18n.langDir
屬性,目錄名稱為 locales
,再依序建立 en.json
與 zh.json
檔案,最終目錄的結構大概會長這樣:
nuxt-app/
├── ...
├── locales/
│ ├── en.json
│ └── zh.json
├── ...
└── nuxt.config.ts
./locales/en.json
內容如下:
{
"hello": "Hello!",
"language": "Language"
}
./locales/zh.json
內容如下:
{
"hello": "你好!",
"language": "語言"
}
翻譯的語系檔案,也可以在根據需求使用 .js、yaml 或 json 格式的檔案,來搭配接受參數來產生翻譯文字。
當使用者切換語系之後,我們希望儲存這個語系設定,讓使用者下次瀏覽網站時,能套用儲存的語系,而不用再次切換,我們可以藉由瀏覽器的 LocalStorage 或 Cookie 來持久化。
Nuxt 3 的伺服器渲染,會導致第一次的請求無法在伺服器端取得 LocalStorage 的設定。
我們可以透過使用 cookie 的方式來儲存語系,這樣伺服器端在接受第一次請求時也能解析出使用者的偏好設定,回傳正確的語系翻譯文字。
你可以自己使用 Nuxt 3 提供 useCookie 等方式操作 cookie 來實現這套流程,不過呢,@nuxtjs/i18n 模組預設是啟用 cookie 相關的設定,提供的 setLocaleCookie()
與 getLocaleCookie()
可以來協助儲存語系設定至 cookie。
調整 ./pages/index.vue
程式碼如下,在我們自訂的 changeLanguage()
方法內,同時呼叫 setLocaleCookie(localeCode)
來儲存語系。
<template>
<div class="flex flex-col items-center bg-white">
<h1 class="mt-48 text-8xl font-medium text-blue-500">
{{ $t('hello') }}
</h1>
<div class="mt-24 space-x-4">
<button
v-for="localeItem in locales"
:key="localeItem.code"
type="button"
class="inline-flex items-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-700 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@click="changeLanguage(localeItem.code)"
>
{{ localeItem.name }}
</button>
</div>
<div class="mt-4 flex flex-row justify-center">
<label class="text-gray-600">{{ $t('language') }}</label>
<span class="ml-4 font-bold text-gray-800">{{ locale }}</span>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, locales, setLocaleCookie } = useI18n()
const changeLanguage = (localeCode) => {
locale.value = localeCode
setLocaleCookie(localeCode)
}
</script>
調整 app.vue
呼叫 getLocaleCookie()
從 cookie 解析的設定值,並再進行套用。
<template>
<div>
<NuxtPage />
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, getLocaleCookie } = useI18n()
const defaultLocaleCode = locale.value
locale.value = getLocaleCookie() ?? defaultLocaleCode
</script>
當我們使用 setLocaleCookie()
儲存語系至 cookie,會將 localeCode 儲存在 cookie 的 i18n_redirected
值上,當我們重新進入網頁,cookie 也將自動夾帶了這個 cookie 過去給伺服器端,並由 getLocaleCookie()
解析再由我們進行語系套用。
其實 @nuxtjs/i18n 因為已將 detectBrowserLanguage.useCookie 預設為 true 等啟用了 cookie 儲存的相關設定,當使用者進入網站時,就會嘗試檢查瀏覽器的 cookie 來將使用者儲存的偏好語言進行套用或重定向。
因此,我們在 app.vue
也不需要特別從 cookie 取出再進行套用,@nuxtjs/i18n 會自動幫我們完成。
<template>
<div>
<NuxtPage />
</div>
</template>
<script setup>
// 除非你有特需的需求,不然這邊都不需要自己來了
// import { useI18n } from 'vue-i18n'
// const { locale, getLocaleCookie } = useI18n()
// const defaultLocaleCode = locale.value
// locale.value = getLocaleCookie() ?? defaultLocaleCode
</script>
而當使用者調整語系時,在第一個範例有提到,如果沒有特別需求,我們可以直接使用 locale
來設定語系,來自動響應變更語系,再使用 setLocaleCookie(localeCode)
將語系保存至 cookie 之內。
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, locales, setLocaleCookie } = useI18n()
const changeLanguage = (localeCode) => {
locale.value = localeCode
setLocaleCookie(localeCode)
}
</script>
但我們只要使用 setLocal()
方法來套用語系,就會一同更新至 cookie 中進行儲存,非常方便。最後我們將 ./pages/index.vue
中的 調整如下,一樣可以達到在 cookie 中儲存使用者語系並在下次瀏覽網頁時自動套用。
<script setup>
import { useI18n } from 'vue-i18n'
const { locale, locales, setLocale } = useI18n()
const changeLanguage = (localeCode) => {
setLocale(localeCode)
}
</script>
使用 setLocaleCookie()
與 getLocaleCookie()
操作的預設設定,我們可以透過 nuxt.config.ts
中的 i18n.detectBrowserLanguage
進行調整,例如,調整 cookieKey
來變更儲存的名稱 i18n_redirected
等,更多選項可以參考這裡。
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
// ...
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected'
},
// ...
}
})
@nuxtjs/i18n 模組提供的 cookie 操作語系持久話,甚至能做到一些重導向等行為,較 Nuxt 3 提供 useCookie 等方式自己來實現這套流程能更擁有更多功能。
這篇太深入沒有講述 Vue I18n 的功能,而是針對 Nuxt 3 的整合與常見的設定做一個分享,如果你已經在 Vue 3 上熟悉 Vue I18n,根據這篇可以加快與 Nuxt 3 整合的速度,更多 Vue I18n v9 與 @nuxtjs/i18n 功能大家可以參照著官方文件,也常注意正式發布前的一些改版訊息,期待 Vue I18n 與 Nuxt Community 提供的 @nuxtjs/i18n 能越來越穩定,讓網站邁向國際化的過程能更順利,大家都能方便的實現多國語系。
感謝大家的閱讀,這是我第一次參加 iThome 鐵人賽,請鞭小力一些,也歡迎大家給予建議 :)
如果對這個 Nuxt 3 系列感興趣,可以訂閱接收通知,也歡迎分享給喜歡或正在學習 Nuxt 3 的夥伴。
動態改變文字
會不會對於 SEO 不好
如果指的動態,是同一個頁面或元件,因為取得不同的資料而動態渲染的文字,對 SEO 來說還是以首次所顯示的或爬取到的資訊為主。
也就是說,一個部落格文章的頁面元件,當請求進來時,如果是後端渲染,則是後端打 API 取得文章資料,動態的渲染出 SEO 要的 Title 或 Meta,那麼爬蟲蒐集到的基本上沒什麼大問題。
倘若是有在客戶端渲染再進行加料的動態文字,雖然網頁標題也會更改,但是搜尋引擎收錄的文字,多不會涵蓋這邊動態修改的文字。
請問您有遇到這個型別問題嗎?
我看官方文件也都沒特別說明...
實在無法解決
補充: 語系有正常顯示
如果運作正常,比較大的可能性應該是編輯器(VS Code)需要重新載入。
建議您先移除 @nuxtjs/i18n
npm uninstall @nuxtjs/i18n
同時也建議您可以嘗試完整移除專案下的 node_modules
重新安裝一次最新的版本
npm install -D @nuxtjs/i18n@next
重新開啟編輯器(VS Code),看看原本錯誤的提示是否還存在
另外不確定您所有的套件的版本跟設定,想詢問您在使用本篇文章的範例專案也會有一樣的情況嗎?
其他可能性也可以建議您檢查看看,例如
tsconfig.json
是否有做一些額外的設定