Source

i18n/index.js

import Vue from 'vue'
import VueI18n from 'vue-i18n'
import fr from './i18n.fr'
import en from './i18n.en'
import nl from './i18n.nl'
import es from './i18n.es'
import VueCookie from 'vue-cookie'
import moment from 'moment'
import { getQueryStringValueOnce } from '@/utils/querystring'

Vue.use(VueCookie)
Vue.use(VueI18n)

const availableLanguages = ['fr', 'en', 'nl', 'es']
const i18nMessages = {
  fr,
  en,
  nl,
  es,
}
const localeFromCookie = VueCookie.get('locale')
const isLocaleFromCookieValid = isValidLocaleCode(localeFromCookie)
let defaultLocale = (isLocaleFromCookieValid ? localeFromCookie : 'fr') || 'fr'

let initialized = false
let localeFromQueryString =
  getQueryStringValueOnce('locale') ||
  getQueryStringValueOnce('lang') ||
  getQueryStringValueOnce('lg')
if (localeFromQueryString) {
  defaultLocale = localeFromQueryString
  console.info('i18n default locale from querystring', defaultLocale)
}

const i18n = new VueI18n({
  locale: defaultLocale,
  fallbackLocale: 'fr',
  messages: i18nMessages,
})
i18n.availableLanguages = availableLanguages

export default i18n

Vue.use(
  (() => {
    return {
      install() {
        if (!isLocaleFromCookieValid) {
          console.warn(
            `Invalid cookie locale (${localeFromCookie}) skipped (expected: ${availableLanguages.join(
              ', '
            )}). Fallback to ${defaultLocale}`
          )
        }

        /**
         * Translate falling back to a hardcoded default value instead of the value of the default language
         * @param {*} code
         * @param {*} defaultValue
         * @param {*} options
         * @returns
         */
        Vue.prototype.$translation = function (
          code,
          defaultValue = null,
          options = {}
        ) {
          if (this.$te(code)) {
            return this.$t(code, options.variables || {})
          } else {
            return defaultValue || code
          }
        }

        //Inject a better $t version that support multiple i18n keys (fallback keys)
        Vue.prototype.$tOriginal = Vue.prototype.$t
        Vue.prototype.$t = function (codeOrCodes, options = {}) {
          if (codeOrCodes instanceof Array) {
            let firstAvailableCode = codeOrCodes.find((c) => this.$te(c))
            if (firstAvailableCode) {
              return this.$tOriginal(firstAvailableCode, options)
            } else {
              //i.g: ['feature_title','common_title'] => It should fallback to common_title in default language
              return this.$tOriginal(
                codeOrCodes[codeOrCodes.length - 1],
                options
              )
            }
          } else {
            return this.$tOriginal(codeOrCodes, options)
          }
        }

        /**
         * Access to raw i18n translations (selected language)
         */
        Object.defineProperty(Vue.prototype, '$translations', {
          get: function () {
            let currentLanguageCode =
              (this.$root && this.$root.$i18n && this.$root.$i18n.locale) ||
              'fr'
            let obj = i18nMessages[currentLanguageCode]

            if (obj === undefined) {
              obj = i18nMessages.fr
              console.warn(
                'Unable to get i18n messages from current language',
                {
                  currentLanguageCode,
                }
              )
            }

            reduceAttributesIntoNestedProperty(obj, 'datatable')
            reduceAttributesIntoNestedProperty(obj, 'datepicker')
            return obj
          },
        })

        /**
         * Access to current language
         */
        Object.defineProperty(Vue.prototype, '$locale', {
          get: function () {
            return this.$root.$i18n.locale
          },
          set: function (localeCode) {
            setNewLocaleCode(localeCode)
          },
        })
      },
    }
  })()
)

/**
 * @unit
 * @param {String} code i.g es, fr, nc
 * @returns
 */
export function isValidLocaleCode(code) {
  return availableLanguages.includes((code || '').toString().toLowerCase())
}

/**
 * @unit
 * @returns
 */
export function setNewLocaleCode(localeCode) {
  if (!initialized && localeFromQueryString) {
    localeCode = localeFromQueryString //First assignment will use query param if available
    console.debug('i18n locale first assignment from querystring', localeCode)
  }

  initialized = true

  if (!isValidLocaleCode(localeCode)) {
    return console.warn(
      `$locale::ignore-invalid-locale-during-set: ${localeCode} (Expected ${availableLanguages.join(
        ', '
      )})`
    )
  }

  i18n.locale = localeCode

  //WIP: Datepicker vue2-datepicker translation issues (dynamic language switch)
  //window.activateLocale = (str) => import('vue2-datepicker/locale/' + str)
  //window.activateLocale(localeCode)

  VueCookie.set('locale', localeCode)

  if (['en', 'nl', 'fr', 'es'].includes(localeCode)) {
    moment.locale(localeCode)
  }
  //console.info(`Locale changed to ${localeCode}`)
}

/**
 * Used by Datatable library.
 * Datatable expects a language property which is a nested object: https://www.datatables.net/plug-ins/i18n/English
 * But we store translations in a plain object:
 * - datatable.paginate.next
 * - datatable.paginate.prev
 * This function will collect all the keys starting with datatable. and build a nested object similar to the one expected by datatable.
 * @example:
 *  reduceAttributesIntoNestedProperty({foo.bar:1,foo.world:2,bar:2,foo:3})
 *  Output: {foo.bar:1,foo.world:2,bar:2,foo:3, foo:{bar:1,world:2} }
 * @param {Object} obj Plain object
 * @param {String} attribute Object attribute (Computed nested object)
 */
export function reduceAttributesIntoNestedProperty(obj, attribute = '') {
  if (!attribute) {
    throw new Error('ATTRIBUTE_REQUIRED')
  }
  obj[attribute] = Object.keys(obj).includes(`${attribute}.0`) ? [] : {}
  Object.keys(obj)
    .filter(
      (key) =>
        key.includes(attribute + '.') && key.indexOf(attribute + '.') === 0
    )
    .forEach((key) => {
      let count = 0
      let fullKey = key
      let cursor = obj[attribute] //i18n[days]
      key = key.split(attribute + '.').join('') //0
      do {
        let arr = key.split('.') //[datepicker, days, 0], [days, 0] //[0]
        let first = arr.shift() //datepicker, days 0
        key = arr.join('.') //days.0, 0 ''
        let structureValue =
          fullKey.includes(`${first}.0`) + fullKey.includes(`${first}.1`)
            ? []
            : {}

        if (key === '') {
          if (cursor instanceof Array) {
            cursor.push(obj[fullKey])
          } else {
            if (cursor[first] instanceof Array) {
              cursor[first].push(obj[fullKey])
            } else {
              cursor[first] = obj[fullKey] //assign value
            }
          }
        } else {
          let parent = cursor //parent = i18n, parent = datepicker
          cursor = cursor[first] || structureValue //ig datepicker = {}, datepicker[days]
          parent[first] = cursor //ig i18n[datepicker] = {}
        }
        if (count > 100) {
          key = ''
          console.error('Short circuit', {
            attribute,
          })
          return
        }
      } while (key !== '')
    })
  obj[attribute] = Object.freeze(JSON.parse(JSON.stringify(obj[attribute])))
}