[Vue] 跟著 Vue 闖蕩前端世界 - 11 使用 vue-i18n 打造多國語系網站環境

要讓網站也可以提供給外籍人士使用,不可免俗的需要多國語系的支援,因此本文透過 vue-i18n 來打造一個多國語系網站,並介紹基本的使用情境及用法。

實作方式


首先安裝套件,並且建立 i18n.js 檔案來產生 VueI18n 物件實體。

npm install vue-i18n --save

import Vue from 'vue'
import VueI18n from 'vue-i18n'

// 自訂語言檔
import en from '../i18n/en/lang'
import tw from '../i18n/tw/lang'

// 使用插件
Vue.use(VueI18n)

// 取得預設語系
const locale = localStorage.getItem('locale') || 'tw'

// 建立 VueI18n 實體
const i18n = new VueI18n({
  locale,
  messages: { en, tw }
})

export default i18n

 

中英文語言檔如下

/* /tw/lang.js */
export default {
  __ok: '好',
  __continue: '繼續',
  __cancel: '取消',
  __guest: '訪客'
}
/* /en/lang.js */
export default {
  __ok: 'ok',
  __continue: 'contiune',
  __cancel: 'cancel',
  __guest: 'guest'
}

 

 

語言檔設計要點


 

1. 使用特殊前綴符號

由於專案通常都是多人開發,因此絕對會發生兩個相同「文字」重複被建立,而這情況往往會在最後總整理的時候才會被發現,此時可以使用具有特殊前綴符號的 key 來查詢取代,降低誤查到其他不相關代碼的機率。

 

2. 僅針對中文先處理

翻譯這種東西通常會交給專業的來處理,所以開發人員去填個假的也沒什麼意義,因此乾脆就不要花時間處理了;此時不必擔心網頁切換到英文會呈現一片空白,因為當找不到對應語言檔時會直接呈現所設定的 key 值,同時還可以藉此了解那些文字尚未被納入多國語系處理範圍喔!

 

3. 依照文字意義命名

真的不需要依照頁面或功能作為 key 命名,除非是一整段功能說明文字可以考慮使用類似 xxFuncNote 的命名;因為實際使用上多透過「文字」去尋找「key」值來套用,所以筆者建議不需要在 key 值的命名多加著墨,只要盡量減短並貼近語意讓 key 盡量內含一些意義存在即可。

 

4. 使用單一檔案即可

筆者在開發初期有嘗試依照共用、頁面或組件來區分語言檔,不過在多人開發環境之下,其實很難區分文字是屬於哪種類型,因此一定會出現許多相同文字的翻譯檔散落四處;另外,要如何將分散四處的翻譯檔交給專業的人來處理也是個大問題,因此筆者最終還是回歸到單一語言檔,而這樣的好處就是 key 值永遠不會重複(IDE會幫忙檢查),最終在整理語系文字的時候也只要 sort by 文字就可以理出是否有重複定義的文字,並且在交付翻譯人員的處理上也方便。

 

 

套用語系


會使用到語系的地方約略區分三種,組件 tempate 區塊、組件 script 區塊以及任何 js 檔案中,在組件中可以使用 $t(__key) 方法操作,範例請參考以下各區使用方式。

 

組件 tempate 區塊 

 

組件 script 區塊

 

在任何 js 檔案中

 

 

切換語系


直接 import 先前所建立的 VueI18n 實體,設定 locale 值後隨即進行切換。

import i18n from 'i18n '

/**
 * @description 切換網站語系
 */
const switchLang = (newLang) => {
  i18n.locale = newLang
  localStorage.setItem('locale', newLang)
}


switchLang('tw') // 切換至中文
switchLang('en') // 切換至英文

 

 

特殊用法


多國語系不僅僅只是作為一對一翻譯的資源檔而已,由於英文會因單複數而產生不一樣的單字,另外各語系的語法順序皆不盡相同,所以 vue-i18n 提供了許多便捷的處理方式,以下參考。

 

插入參數

可以在語系文字中插入參數 (ex. 歡迎 XXX 加入開發團隊),官方範例如下:

lang.js

template in *.vue

result

也可以在語系定義檔中使用 {0} 作為標示,調用端就使用陣列依序傳入參數值喔!

 

 

單複數處理

可以針對英文兩(三)種單複數型式 (零、單、複數) 做語系文字的對應,官方範例如下:

lang.js

template in *.vue

result

注意要套用單複數功能要改用 $tc 這個方法喔!

 

 

動態修正語系文字


語系檔通常都會與網站放在同一包,如果要改文字就需要重新打包上板,但有一些情況是必須立即修正文字的,所以如果只是為了改個幾個字就要重新包版上板也是滿不符合經濟效益的,更何況若網站是透過 cordova 包裝成 app 時更是繁瑣,因此我們可以透過 API 的叫用,取得需要調整的語系文字 (key, twValue, enValue) 來覆寫目前預設的文字,這樣的彈性又更好了,請參考以下作法。

// app.vue

import tw from 'i18n/tw/lang' // 存放繁體翻譯
import en from 'i18n/en/lang' // 存放英文翻譯

export default {
  name: 'App',
  mounted () {

    // 透過 API 更新語系檔文字 (hotfix)
    this.updateModifiedLangs()

  },
  methods: {

    // 更新語系 [非"常態"行為,僅供緊急性質的語文修正]
    updateModifiedLangs: async function () {
      const res = await this.$api.common.getModifiedLangs()
      const {header: { code }, body: modifiedLangs} = res
      if (code.IsSuccess() && modifiedLangs) {
        modifiedLangs.forEach(lang => {
          tw[lang.key] = lang.tw
          en[lang.key] = lang.en
        })
      }
    }
}
緊急修正的文字最終需要回歸到本地端的語系檔案中喔! (避免兩份文件難以維護)

 

 

語系查詢工具


開發過程中套用多國語系是非常花時間的事情,尤其若是要自己去一堆語系檔中找尋是否已有定義過的文字是非常考驗耐性的工作,因此筆者為減輕自己及開發團隊的負擔而開發了小工具,訴求如下。

  1. 依據關鍵字自動查詢已存在的語系文字 (包含即列入顯示清單)
  2. 自動選擇相同的語系文字並產出語法
  3. 有按鈕可以自動複製語法 (省去滑鼠圈選時間)
  4. 若不存在可以透過介面建立新語系文字並產出語法
  5. 新建的語系文字可記錄起來累積到一定的量在貼回語系檔中

 

操作方式如下

 

原始碼如下,有相同困擾的朋友可以參考一下

<!--HTML-->
<template>
  <div id="container">
    <div id="condition" >
      key:
      <input type="text" v-model="targetKey"> value:
      <input type="text" v-model="targetValue">
      <br>
    </div>
    <div id="htmlCode" v-show="!!htmlCode" >
      <input type="button" @click="copyHtmlCode" value="COPY">{{htmlCode}}
    </div>

    <div id="langCode" v-show="!!langCode" >
      <input type="button" @click="copyLangCode" value="COPY">{{langCode}}
      <input type="button" @click="appendStoreLangCode" value="ADD TO LIST">
    </div>
    <select name="sometext" v-model="selectedLangKey" size="20" style="width: 500px">
      <option v-for="(value, key) in matchedLangs" :key="key" :value="key"> {{value}} </option>
    </select>

    <div id="storedLangCode">
      <input v-if="storedLangCodes" type="button" @click="copyStoredLangCodes" value="COPY">
      <input v-if="storedLangCodes" type="button" @click="removeStoreLangCodes" value="Remove All">
      <p> {{storedLangCodes}} </p>
    </div>

  </div>
</template>

<!--JavaScript-->
<script>
import tw from 'i18n/tw/lang' // 存放繁體翻譯

export default {
  name: 'LangMaker',
  data () {
    return {
      langs: tw,
      targetValue: '',
      targetKey: '',
      selectedLangKey: '',
      storedLangCodes: ''
    }
  },
  watch: {
    matchedLangs: function (newMatchedLangs) {
      if (this.targetValue) {
        for (var key in newMatchedLangs) {
          let langValue = newMatchedLangs[key].toString().trim().toUpperCase()
          let targetLangValue = this.targetValue.toString().toUpperCase()
          // 完全命中就直接選擇那筆語系 (產生 html 代碼)
          if (langValue === targetLangValue) {
            this.selectedLangKey = key
          }
        }
      }
    }
  },
  mounted () {
    this.getStoreLangCodesFromLocalStorage()
  },
  computed: {
    matchedLangs: function () {
      let matchedKeyValuePairs = {}
      if (this.targetValue && !this.targetKey) {
        for (var key in this.langs) {
          let langValue = this.langs[key].toString().trim().toUpperCase()
          let targetLangValue = this.targetValue.toString().toUpperCase()
          // 有包含就列出 (搞不好可以通用)
          if (langValue.indexOf(targetLangValue) !== -1) {
            matchedKeyValuePairs[key] = this.langs[key]
          }
        }
      }
      return matchedKeyValuePairs
    },
    htmlCode: function () {
      if (this.selectedLangKey && this.matchedLangs && Object.keys(this.matchedLangs).length > 0) {
        // 有選到
        return `{{$t('${this.selectedLangKey}'/*${this.matchedLangs[this.selectedLangKey]}*/)}}`
      } else {
        // 沒選到 (依據輸入數值產 htmlCode)
        if (this.targetValue && this.targetKey) {
          return `{{$t('__${this.targetKey}'/*${this.targetValue}*/)}}`
        }
      }
      return ''
    },
    langCode: function () {
      // 沒選到 (依據輸入數值產 langCode)
      if (this.targetValue && this.targetKey) {
        return `__${this.targetKey}: '${this.targetValue}'`
      }
      return ''
    }
  },
  components: {},
  methods: {
    copyHtmlCode: function () {
      window.getSelection().selectAllChildren(document.getElementById('htmlCode'))
      document.execCommand('copy')
    },
    copyLangCode: function () {
      window.getSelection().selectAllChildren(document.getElementById('langCode'))
      document.execCommand('copy')
    },
    getStoreLangCodesFromLocalStorage: function () {
      this.storedLangCodes = window.localStorage.getItem('_storedLangCodes')
    },
    setStoreLangCodesToLocalStorage: function () {
      window.localStorage.setItem('_storedLangCodes', this.storedLangCodes)
    },
    appendStoreLangCode: function () {
      this.getStoreLangCodesFromLocalStorage()
      this.storedLangCodes += (this.langCode + ', ')
      this.setStoreLangCodesToLocalStorage()
    },
    removeStoreLangCodes: function () {
      this.storedLangCodes = ''
      this.setStoreLangCodesToLocalStorage()
    },
    copyStoredLangCodes: function () {
      window.getSelection().selectAllChildren(document.getElementById('storedLangCode'))
      document.execCommand('copy')
    }
  }
}
</script>

<style lang="scss" scoped>
#container {
  padding: 30px;
}

#condition {
  padding-top: 25px;
  padding-bottom: 25px;
}

#htmlCode {
  padding-bottom: 25px;
}

#langCode {
  padding-bottom: 25px;
  color: blueviolet;
}

#storedLangCode {
  padding-top: 25px;
  padding-bottom: 25px;
  p {
    padding-top: 10px;
  }
}
</style>

 

 

參考資訊


vue-i18n v6.0+ Doc

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !