vuejs3vitevue-i18n

Vue 3 + vue-i18n & unplugin-vue-i18n


I'm trying to implement vue-i18n (or possibly i18next) in a project in a certain way but I can't figure it out

Problem 1: auto load translations

I would like to load the translations without manually defining them every time a new one is added, but that they are made available automatically, so that it is reusable and independent from the specific project

src/i18n/index.ts

import { createI18n } from 'vue-i18n';

const locales = import.meta.glob('./locales/*.json');

// I don't understand how to convert locales to messages
// it's a Promise but I can't get it to work properly

export default createI18n({
    locale: "en",
    fallbackLocale: "default",
    globalInjection: true,
    legacy: false,
    messages: { }
});

(I also use router and Pinia but I keep them out of these examples)

src/main.ts

import App from './App.vue';
import i18n from './i18n';

const app = createApp(App);

app.use(i18n);
app.mount('#app');


Problem 2: use t function in script

In the template it works according to the documentation but in the code it doesn't work as it should.

In the documentation they talk about the possibility of not having to call the library for each component, but this, $i18n and $t are undefined

src/App.vue

<template>
  <div class="locale-changer">
    <!-- switch language with select option -->
    <select v-model="$i18n.locale">
      <option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale">{{ locale }}</option>
    </select>

    <!-- switch language with buttons -->
    <div v-bind:ref="$i18n.locale">
      <button v-for="locale in $i18n.availableLocales" @click="$i18n.locale = locale">{{ locale }}</button>
    </div>

    <!-- output -->
    <h2>{{ $t('test') }}</h2>
    <h2>{{ testFromRef  }}</h2> <!-- this one doesn't change -->
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';

const { t } = useI18n();
const testFromRef = ref();
testFromRef.value = t('test');
</script>

Lazy loading

In all this I also tried to understand how to implement lazy loading, in case the translations become many and expensive, but I didn't understand exactly how to make it effective

I have read all the documentation, several answers on stackoverflow, reddit and other sites but I have not come to a conclusion, if anyone could enlighten me a little, I've been running around in circles for over a week, thank you.


Solution

  • Solution: auto load translations and lazy loading

    with importedLocales and SUPPORT_LOCALES I generate an array of strings referring to the languages ​​present in the ./locales folder

    then I provide changeLanguage function which loads and sets the language on the fly (lazy loading)

    can cause problems with the localized page title when routing, something probably needs to be changed

    the language change does not happen via url for example /en/settings, /fr/settings, I wanted to avoid having the language in the url

    src/i18n/index.ts

    import { createI18n } from 'vue-i18n';
    import router from '@/router';
    
    const importedLocales = import.meta.glob('./locales/*.json');
    
    const i18n = createI18n({
        locale: navigator.language || 'default', // change as needed
        fallbackLocale: 'default', // change as needed
        globalInjection: true,
        legacy: false,
        messages: {}
    });
    
    changeLanguage(navigator.language); // can be improved (eg. check cookie before)
    
    // load the translation only on language change for lazy loading
    async function loadLocaleMessages(locale: string) {
        const messages = await import(`./locales/${locale}.json`);
        return messages.default;
    }
    
    // manages the language change
    export async function changeLanguage(locale: string) {
        const messages = await loadLocaleMessages(locale);
        i18n.global.setLocaleMessage(locale, messages); // set locale
        i18n.global.locale.value = locale; // set messages
    
        const titleKey = router.currentRoute.value.meta.title;
        if (titleKey) {
            // It doesn't work correctly for me, the title actually changes but not with the translated string
            // in fact the router does not access the messages, it probably depends on how I configured the router
            document.title = i18n.global.t(titleKey as string);
        }
    }
    
    // Array of filenames in locales folder without extension (eg. ["default", "en-GB", "en-US", "it-IT", ...])
    export const SUPPORT_LOCALES = Object.keys(importedLocales).map(str => str.split('/').pop()!.split('.')[0]);
    
    export default i18n;
    

    Solution: change localization in script

    I've used buttons instead select > options but doesn't make difference

    I found the syntax with option api less complex, it can also be done with composition but it makes the code more complicated

    the global $t() method is only accessible from computed

    Component.vue

    <template>
        <div v-bind:ref="$i18n.locale">
            <button v-for="locale in SUPPORT_LOCALES" @click="changeLanguage(locale)" :key="locale">{{ locale }}</button>
        </div>
    </template>
    
    <script lang="ts">
    import { changeLanguage, SUPPORT_LOCALES } from '@/i18n';
    
    export default {
        computed: {
            // from computed $t() is available and it is not undefined
            // adjust for your use case
            menu_primary_items() {
                return [
                    { text: this.$t("menu.item1"), enabled: true },
                    { text: this.$t("menu.item2"), enabled: true }
                ] as MenuItem[];
            }
        }
    };
    </script>
    

    this is about how I created the router, probably here I need to solve the problem of the title which is not translated but remains for example routes.root

    src/router/index.ts

    const routes = [
        {
            path: '/',
            name: 'root',
            meta: { title: i18n.global.t('routes.root') }
        },
        {
            path: '/settings',
            name: 'settings',
            meta: { title: i18n.global.t('routes.ranking') }
        }
    ]
    
    const router = createRouter({
        history: createWebHistory(import.meta.env.BASE_URL),
        routes: routes
    });
    
    // Set the title for each route when change
    router.beforeEach((to, from, next) => {
        document.title = to.meta.title as string;
        next();
    });
    
    export default router;
    

    It's certainly not the most efficient but it works, some improvements can be made