Search code examples
vue.jsfetchvue-i18n

vue show page after i18n translations loaded from backend


In my application translations can be edited by admin, so I need to preload them from my backend and initialize in the vue apps I have. So I found that vue i18n package has the ability to lazy load messages (link) . But on their example, they already initialize vue with default (en) messages, but in my case, I do not have default messages preloaded. So I want to load messages from the backend at a moment of creating a new instance of the VueI18n.

Here is the i18n.js file:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import {syncGetData} from "@/apiUtil";
import NProgress from 'nprogress'
import './sass/nprogress.scss'

Vue.use(VueI18n)


let loadedLanguages = []

export const i18n = new VueI18n({
  locale: navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE,
  fallbackLocale: navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE,
  messages: loadLanguage(navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE)
})

function setI18nLanguage (lang) {
  i18n.locale = lang
  document.querySelector('html').setAttribute('lang', lang)
  return lang
}

export function loadLanguage(lang) {
  // If the language was already loaded
  if (loadedLanguages.includes(lang)) {
    return Promise.resolve(setI18nLanguage(lang))
  }
  NProgress.start();
  return syncGetData('/api/translations/' + lang)
    .then(function(data) {
      setI18nLanguage(lang)
      loadedLanguages.push(lang)
      return  data;
    })
    .catch(function (error) {
      console.error(error)
    }).finally(function () {
      NProgress.done();
    });
}

also here is the main.js (simplified):

import Vue from 'vue'
import App from './App.vue'
import {i18n} from './i18n'
new Vue({
  i18n,
  render: h => h(App)
}).$mount('#app')

The loading of the translations working, but Vue render the template before they loaded, and instead of actual translations I see translation keys. I tried to use v-cloak but it did not help.

So what I want to achieve is, while translations loading users should see only the loading bar (I'm using NProgress, you can see the usage in the i18n.js) and render the app only after translations loaded (maybe not render but initialize). What I'm getting instead: both loading bar and rendered app without actual translations

Thanks in advance!

Updates after Michal Levý asnwer

Now, i18n.js looks like this:

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { syncGetData} from "@/apiUtil";
import NProgress from 'nprogress'
import './sass/nprogress.scss'

Vue.use(VueI18n)


let loadedLanguages = [];

export function initI18n() {
  const lang = navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE

  return syncGetData('/api/translations/' + lang)
    .then(function(data) {
      console.log(data)
      setI18nLanguage(lang)
      loadedLanguages.push(lang)
      return new VueI18n({
        locale: lang,
        fallbackLocale: lang,
        messages: data
      })
    })
    .catch(function (error) {
      console.error(error)
    });
}

function setI18nLanguage (lang) {
  document.querySelector('html').setAttribute('lang', lang)
  return lang
}

export function loadLanguage(lang) {
  // If the language was already loaded
  if (loadedLanguages.includes(lang)) {
    return Promise.resolve(setI18nLanguage(lang))
  }
  NProgress.start();
  return syncGetData('/api/translations/' + lang)
    .then(function(data) {
      setI18nLanguage(lang)
      loadedLanguages.push(lang)
      return  data;
    })
    .catch(function (error) {
      console.error(error)
    }).finally(function () {
      NProgress.done();
    });
}

and main.js is:

import Vue from 'vue'
import App from './App.vue'
import {initI18n} from './i18n'
import NProgress from "nprogress";
import './sass/nprogress.scss'

NProgress.start();
initI18n().then(function(i18n) {
  new Vue({
    i18n,
    render: h => h(App)
  }).$mount('#app')
}).finally(function () {
  NProgress.done();
});

I can confirm that now messages are loaded before the Vue initalized, b-z in my App.vue I have a console.log in mounted method:

  mounted() {
    console.log(this.$i18n.messages)
  }

and the output of console log is Object { main_title: "Calculation", tab_you: "You", tab_friend: "Your Friend",...} , before it was empty object.

But still <h2 class="title-form">{{ $t('main_title') }}</h2> renders main_title instead of Calculation


Solution

  • Replace VueI18n innitialization with something like this:

    export function initI18n() {
      const lang = navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE
      
      return loadLanguage(lang).then(function(data) {
        return new VueI18n({
          locale: lang,
          fallbackLocale: lang,
          messages: data
        })
      })
    }
    

    main.js

    import Vue from 'vue'
    import App from './App.vue'
    import { initI18n } from './i18n'
    
    initI18n().then(function(i18n) {
      new Vue({
        i18n,
        render: h => h(App)
      }).$mount('#app')
    })
    

    Edit after Q update:

    It seems the format of your localization is wrong. VueI18 is able to use multiple languages at once so all your translation keys should be contained in "language object"

    Instead of:

    { 
      main_title: "Calculation", 
      tab_you: "You", 
      tab_friend: "Your Friend",
      ...
    }
    

    your API should return

    { 
      "en": {
        main_title: "Calculation", 
        tab_you: "You", 
        tab_friend: "Your Friend",
        ...
      }
    }
    

    ...where "en" is language identifier

    Check your Browser console, I believe VueI18n should warn about missing keys...