I'm trying to implement vue-i18n (or possibly i18next) in a project in a certain way but I can't figure it out
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');
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>
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.
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;
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