Search code examples
javascriptvue.jsmeteorpromisevue-i18n

How can I chain dynamic imports with meteor?


My use case

I work on a large app where, depending on a user role, I load/import different modules sets. This is a meteor app, with Vue, vue-router & vue-i18n on the frontend, but no store like vuex.

Each module comes with its own routes, translation files, api & UI. That’s why I need to check that every module and its translations are loaded before I display the main UI & navigation (or else, e.g. the navigation items labels related to an unloaded module will not be translated, or the localized routes will return a 404) .

Is there a pattern, as simple as possible, to ensure that everything is loaded?

My code & logic

My use case is more complex than what I can achieve with Promise.all afaik.

I tried to make nested promises with a combination of Promises.all and then().

To sum it up, the order is:

  • load the base bundle
  • login the client
  • import i18n language file for the main bundle then for each module
  • for each module, after its language file is loaded and merged in the related i18n messages, I need to load the module itself (localized routes, UI ...)

The main loading part

Accounts.onLogin(function (user) {
    let userRoles = Roles.getRolesForUser(Meteor.userId())
    let promises = []
    let lang = getDefaultLanguage()
    promises.push(loadLanguageAsync(lang))
    this.modulesReady = false
    for (let role of userRoles) {
        switch (role) {
        case "user":
            import { loadUserLanguageAsync } from "/imports/user/data/i18n"
            promises.push(loadUserLanguageAsync(lang).then(import("/imports/user/")))
            break
        case "admin":
            import { loadAdminLanguageAsync } from "/imports/admin/data/i18n"
            promises.push(loadAdminLanguageAsync(lang).then(import("/imports/admin/")))
            break
        default:
            break
        }
    }

    return Promise.all(promises).then(function (values) {
        this.modulesReady = true // my green flag, attached to the window object
    })
})

the main language loading functions

const loadedLanguages = []

// Load i18n
Vue.use(VueI18n)
export const i18n = new VueI18n()


export const getDefaultLanguage = () => {
    let storedLanguage = window.localStorage.getItem(
        Meteor.settings.public.brand + "_lang"
    )
    return Meteor.user() && Meteor.user().settings && Meteor.user().settings.language
        ? Meteor.user().settings.language
        : // condition 2: if not, rely on a previously selected language
        storedLanguage
            ? storedLanguage
            : // or simply the browser default lang
            navigator.language.substring(0, 2)
}
export const loadLanguage = (lang, langFile) => {
    console.log("LOAD LANGUAGE " + lang)

    // we store agnostically the last selected language as default, if no user is logged in.
    window.localStorage.setItem(
        Meteor.settings.public.brand + "_lang",
        lang
    )
    loadedLanguages.push(lang)
    if (langFile) {
        i18n.setLocaleMessage(lang, Object.assign(langFile))
    }
    i18n.locale = lang


    return lang
}

export const loadLanguageModule = (lang, langFile) => {
    console.log("LOAD LANGUAGE MODULE" + lang)
    i18n.mergeLocaleMessage(lang, Object.assign(langFile))
    return lang
}

export function loadLanguageAsync(lang) {
    if (i18n.locale !== lang) {
        if (!loadedLanguages.includes(lang)) {
            switch (lang) {

            case "en":
                return import("./lang/en.json").then(langFile => loadLanguage("en", langFile))

            case "fr":
                return import("./lang/fr.json").then(langFile => loadLanguage("fr", langFile))

            default:
                return import("./lang/fr.json").then(langFile => loadLanguage("fr", langFile))
            }
        } else {
            console.log("Already loaded " + lang)

        }
        return Promise.resolve(!loadedLanguages.includes(lang) || loadLanguage(lang))
    }
    return Promise.resolve(lang)
}

The user module language loading

const userLoadedLanguages = []

export default function loadUserLanguageAsync(lang) {
    if (i18n.locale !== lang || !userLoadedLanguages.includes(lang)) {
        switch (lang) {
        case "en":
            return import("./lang/en.json").then(langFile => loadLanguageModule("en", langFile))
        case "fr":
            return import("./lang/fr.json").then(langFile => loadLanguageModule("fr", langFile))
        default:
            return import("./lang/fr.json").then(langFile => loadLanguageModule("fr", langFile))
        }
    }
    return Promise.resolve(i18n.messages[lang].user).then(console.log("USER LANG LOADED"))
}
  • Once every module is loaded, I switch a flag that allows my router navigation guard to proceed to the route required (see the main loading part).

The router guard and await async function

router.beforeEach((to, from, next) => {
     isReady().then(
        console.log("NEXT"),
        next()
    )
})
async function isReady() {
    while (true) {
        if (this.modulesReady) { console.log("READY"); return }
        await null // prevents app from hanging
    }
}

I'm quite new to the async logic and I struggle to identify what I am doing wrong. The code here makes the browser crash since I guess my promises values are not the right ones and it goes in an infinite isReady() loop.

I would very much welcome suggestions or advises on the better/correct way to go. Also, feel free to request more details if something is missing.

Thanks!


Solution

  • How can I chain dynamic imports with meteor?

    First consider this answer on Promise chains: How do I access previous promise results in a .then() chain?

    If you rather prever async/wait style you con follow up here: Dynamic imports can be called using await inside an async function. This gives you the opportunity to wrap up your code sync-style and resolve everything in a final Promise:

    Consider a simple JSON file on the relative project path /imports/lang.json:

    {
      "test": "value"
    }
    

    and some example exported constant on the path /imports/testObj.js:

    export const testObj = {
      test: 'other value'
    }
    

    You can dynamically import these using an async function like so (example in client/main.js):

    async function imports () {
      const json = await import('../imports/lang.json')
      console.log('json loaded')
      const { testObj } = await import('../imports/test')
      console.log('testObj loaded')
      return { json: json.default, testObj }
    }
    
    Meteor.startup(() => {
      imports().then(({ json, testObj }) => {
        console.log('all loaded: ', json, testObj)
      })
    })
    

    This will print in sequence

    json loaded
    testObj loaded
    all loaded: Object { test: "value" } Object { test: "other value" }
    

    Since your code example is quite complex and barely reproducible so I suggest you to consider this scheme to rewrite your routine in a more sync-style fashion to avoid the Promises syntax with dozens of .then branches.