Đặt vấn đề

Vấn đề đặt ra đó là mình cần triển khai đa ngôn ngữ trên web app mà mình đang việc, vì vậy để giải quyết vấn đề này mình đã đi tìm một package có thể nhanh chóng hỗ trợ mình xử lý công việc này và mình đã tìm ra vue-i18n.

Internationalization plugin of Vue.js
Hỗ trợ DateTime localization
Number localization
Locale messages syntax

Bài viết hôm nay mình xin phép được chia sẻ cách thức mình đã implement vue-i18n để đa ngôn ngữ ứng dụng của mình như thế nào và cũng rất mong nhận được sự góp ý từ mọi người để cách tiếp cận trở nên tốt hơn.

Kết quả demo test của mình

https://dibochit.herokuapp.com

Cài đặt

Khởi tạo ứng dụng

Mình sử dụng vue-cli để việc khởi tạo ứng dụng một cách nhanh chóng và dễ dàng

$ npm install -g vue-cli
$ vue init webpack-simple dibo-vue-i18n

Cài đặt vue-i18n

Để có thể sử dụng vue-i18n trong project của mình thì có một số cách cài đặt như sau:

  • Download trực tiếp hoặc sử dụng link cdn
  • Cài đặt thông qua npm
  • Cài đặt thông qua yarn
  • Hay clone trực tiếp từ github về và build

Với mình thì mình đã lựa chọn sử dụng npm

$ cd /path/to/dibo-vue-i18n
$ npm install --save vue-i18n

Tạo locales folder để lưu trữ file ngôn ngữ dạng *.json

Cấu trúc thư mục project của mình giờ có dạng

dibo-vue-i18n
|
|__src
    |
    |__lang
        |
        |__vn.json
        |__en.json

Khởi tạo vue-i18n

Việc khởi tạo vue-18n là khá đơn giản:

  • Tạo mới một instance
  • Thêm locale messages: lựa chọn ngôn ngữ sẽ sử dụng ở đây mình sẽ chọn là en hay vn
  • Thêm fallback lang là thằng language sẽ mặc định được nhận nếu ứng dụng không xác định được ngôn ngữ đã được lựa chọn
  • Thông báo với Vue instance là mình sử dụng instance vủa vue-i18n vừa được khởi tạo và kết thúc
# main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import VueI18n from 'vue-i18n'
import vnMessage from './lang/vn.json'
import enMessage from './lang/en.json'

Vue.use(VueI18n)

const messages = {
  vn: vnMessage,
  en: enMessage,
}
const i18n = new VueI18n({
  locale: 'vn', // set locale
  messages,
  fallbackLocale: 'vn',
})

new Vue({
  el: '#app',
  i18n,
  router,
  render: h => h(App)
})

Một số lưu ý

  • Bạn cần code đúng thứ tự import vue trước sau đó import vue-i18n
  • Khai báo Vue.use(VueI18n) trước khi tạo mới instance của VueI18n

Thêm nội dung cho files lang

Mình lấy ví dụ với file vn.json và tương tự cho file en.json

# vn.json

{
  "common": {
    "select_lang": "Ngôn ngữ"
  },
  "login": {
    "title": "Đăng nhập vào dibochit",
    "buttons": {
      "login": "Đăng nhập"
    },
    "input_text": {
      "email": "Địa chỉ gmail",
      "password": "Mật khẩu"
    },
    "messages": {
      "none_account": "Tạo tài khoản mới",
      "register": "Đăng ký"
    }
  }
}

Translate

Thay thế những nội dung mà mình muốn đa ngôn ngữ sử dụng template như sau

<p>{{ $t("common.select_lang") }}</p>

Và kết quả sẽ render ra dạng

<p>Ngôn ngữ</p>

Nếu muốn translate giá trị thuộc tính của thẻ ví dụ placeholder chẳng hạn sẽ làm như sau

<input type="text" :placeholder="$t('common.select_lang')" />

Kết quả hiện tại

Chú ý

  • Việc tự động localization tùy tiện HTML sẽ rất nguy hiểm do dễ bị mắc phải lỗ hổng tấn công XSS. Chỉ sử dụng với content chắc chắn tin cậy và tuyệt đối không dùng với dữ liệu do người dùng cung cấp.
  • Có một cách tổ chức file lang cũng hợp lý đó là chia ra theo component, mỗi component sẽ tương ứng 1 file
  • Có nhiều format cho localization như

HTML formating

Named formatting truyền theo key

# lang_x.json
hello: '{msg} world {year}'

#  component
<p>{{ $t('message.hello', { msg: 'hello', year: '2017' }) }}</p>

List formatting

hello: '{0} world'

<p>{{ $t('message.hello', ['hello']) }}</p>

hay Custom formatting

mình sẽ không nói rõ trong bài viết này mà các bạn có thể truy cập vào địa chỉ link bên trên để xem trong document của vue-i18n đã được viết khá chi tiết và rõ ràng.

Kết hợp vue-i18n với vuex

Nếu mà chỉ thế là xong thì việc tích hợp vue-18n có thể coi là ez game quá nhể hihi 🙋‍♂️
Bây giờ mình cần nghĩ tới là làm thế nào để thay đổi thằng locale trong main.js kia?
Mình có thể làm theo một số cách được nhắc tới trên docuemnt như
Global config
Global method
Instance property
header + dynamic locale

Sự lựa chọn của mình đó là sử dụng kết hợp vuexCentralized State Management for Vue.js

Cấu trúc lại project

Trước tiên mình sẽ tách VueI18n instance ra một file mới để có thể dễ dàng tái sử dụng code. Mình sẽ tạo file i18n.js trong thư mục lang.
Cấu trúc thư mục lang hiện tại sẽ như sau

lang
|
|__i18n.js
|__vn.json
|__en.json

Tạo instance VueI18n và config locale, messages

#i18n.js

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import vnMessage from './vn.json'
import enMessage from './en.json'

Vue.use(VueI18n)

const messages = {
  vn: vnMessage,
  en: enMessage,
}

const i18n = new VueI18n({
  locale: 'vn', // set locale
  messages,
  fallbackLocale: 'vn',
})

export default i18n

Giờ main.js sẽ ngắn gọn hơn, mình chỉ cần import instance VueI18n đươc export trong i18n.js vào và sử dụng. Mình cũng sẽ export Vue instance bằng biến một biến tên là app để lát nữa có thể sử dụng được app.$i18n trong vuex

#main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import i18n from './lang/i18n'

const app = new Vue({
  el: '#app',
  i18n,
  router,
  render: h => h(App)
})

export default app

Cài đặt vuex

Trong khuôn khổ bài viết mình sẽ không trình bày nhiều về vuex, bạn có thể tham khảo tài liệu trên document của vuex

$ npm install --save vuex

Sử dụng kết hợp vuex và vue-i18n

Đơn giản nhất mình sẽ tạo mới file store.js dưới đường dẫn src/store.js

import Vue from 'vue'
import Vuex from 'vuex'
import app from './main'

Vue.use(Vuex)

export default new Vuex.store({
  const mutations = {
    SET_LANG (state, payload) {
      app.$i18n.locale = payload
    }
  },
  
  const actions = {
    setLang({commit}, payload) {
      commit('SET_LANG', payload)
    }
  }
})

Thông báo với VueInstance rằng mình sẽ dùng Vuex store

#main.js
...
import store from './store'

export const app = new Vue({
  el: '#app',
  i18n,
  router,
  store,
  ...

Việc cần làm bây giờ sẽ là call action setLang mỗi khi user selected language. Mình khởi tạo element select language trong App.vue nên giờ mình sẽ setup cho nó

<template>
    ...
    <div class="item" v-for="lang in optionLangs" :value="lang.value" @click.prevent="callSetLangActions">{{ lang.text }}</div>
    ...
</template>

<script>
    export default {
        data: () => ({
          optionLangs: [
            {
              text: 'Vietnamese',
              value: 'vn'
            },
            {
              text: 'English',
              value: 'en'
            }
          ]
        }),
        methods: {
          callSetLangActions (event) {
            this.$store.dispatch('setLang', event.target.getAttribute('value'))
          }
        }
    }
</script>

Kết quả

Mở extention Vue Devtools của google chrome lên và cảm nhận :3

Kết luận

  • Cảm ơn bạn đã dành thời gian đọc hết bài chia sẻ của mình.
  • Trên đây là bài chia sẻ của mình về một cách tiếp cận đơn giản để đa ngôn ngữ trong ứng dụng Vue sử dụng vue-i18nvuex
  • Đây chưa phải là cách tốt nhất các bạn có thể tìm hiểu thêm và lựa chọn phương pháp phù hợp nhất với riêng mình

Mở rộng

  • Các bạn có thể tìm hiểu thêm sử dụng vue-localstorage để set expires time cho vuex store
  • Khi ứng dụng của bạn phải đa ngôn ngữ cho nhiều ngôn ngữ khoảng 10 chẳng hạn, và mỗi file ngôn ngữ (*.json) lại tương đối lớn do đó bạn sẽ nghĩ tới việc không muốn load chúng vào trong ứng dụng một cách đồng thời thì lúc này hãy nghĩ tới dynamic loading, theo dõi issue này

Tài liệu tham khảo