vue show page after i18n translations loaded from backend - vue.js

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

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...

Related

Easier way to use plugins with the composition api in vue 3

When adding vuex or vue-router as plugin in vue and using the options api you could access these plugins with the this keyword.
main.js
import { createApp } from 'vue';
import i18n from '#/i18n';
import router from './router';
import store from './store';
app.use(i18n);
app.use(store);
app.use(router);
RandomComponent.vue
<script>
export default {
mounted() {
this.$store.dispatch('roles/fetchPermissions');
},
}
</script>
The this keyword is no longer available with the composition api which leads to a lot of repetitive code. To use the store, vue-router or the i18n library I have to import and define the following:
RandomComponent.vue with composition api
<script setup>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const router = useRouter();
const store = useStore();
const { t } = useI18n();
const { handleSubmit, isSubmitting, errors } = useForm('/roles/create', role, CreateRoleValidationSchema, () => {
store.dispatch('notifications/addNotification', {
type: 'success',
title: t('create_success', { field: t('role', 1) }),
});
router.push({ name: 'roles' });
});
</script>
Is there a way to avoid these imports and definitions and have a way to easily use these plugins like I could do with the options api?
There is no built-in way of doing that in Composition API and script setup.
What you could do is:
Create a plugins.js file that exports common bindings you want to import. For example:
export * from 'vuex'
export * from 'vue-router'
export * from 'vue-i18n'
And then you have only 1 import to do:
<script setup>
import { useStore, useRouter, useI18n } from './plugins'
const router = useRouter();
const store = useStore();
const { t } = useI18n();
const { handleSubmit, isSubmitting, errors } = useForm('/roles/create', role, CreateRoleValidationSchema, () => {
store.dispatch('notifications/addNotification', {
type: 'success',
title: t('create_success', { field: t('role', 1) }),
});
router.push({ name: 'roles' });
});
</script>
You can go even further by initiating the plugins like:
import { useStore, useRouter, useI18n } from './plugins'
export function initiateCommonPlugins() {
const router = useRouter();
const store = useStore();
const { t } = useI18n();
return { router, store, t }
}
And your code then will look like this:
<script setup>
import { router, store, t } from './initiate-plugins'
const { handleSubmit, isSubmitting, errors } = useForm('/roles/create', role, CreateRoleValidationSchema, () => {
store.dispatch('notifications/addNotification', {
type: 'success',
title: t('create_success', { field: t('role', 1) }),
});
router.push({ name: 'roles' });
});
</script>
Use unplugin-auto-import plugin
This plugin can eliminate all imports you want and is highly customizable. Haven't tried it yet but I have seen people recommend it.
Stick to Options API
Using Vue 3 doesn't mean that you have to use Composition API for creating components. You can use Options API along with script setup for composables instead of Mixins.
So Options API for components, Composition API for reusing code or advanced use-cases.

Load i18n for all components Vue3

When using vue-i18n, I need to add this to every single component:
setup() {
const { t, locale } = useI18n();
return { t, locale };
}
Is there any way to preload the t function to all my components?
You can load and switch languages as below.
in i18n.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages () {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
const locale = localStorage.getItem('language') || 'tr'
export default new VueI18n({
locale: locale,
fallbackLocale: locale,
messages: loadLocaleMessages()
})
in main.js
import i18n from './i18n'
new Vue({
el: "#app",
router,
store,
i18n,
render: h => h(App),
})
createI18n() creates a Vue plugin that makes vue-i18n available globally for all components:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createI18n } from 'vue-i18n'
const i18n = createI18n(/*...*/)
createApp(App).use(i18n).mount('#app')
t is then available in the template as $t, and locale is $i18n.locale.
demo

Vue2 Composition API Apollo client with id default not found

I have started having an issue with apollo client in vue2 when using inside a watch.
I have followed the setup guide for apollo client when using the composition api:
https://v4.apollo.vuejs.org/guide-composable/setup.html#_2-connect-apollo-client-to-vue
so my main.ts looks like this:
import contentfulClient from "./plugins/vue-apollo-contentful";
import apiClient from "./plugins/vue-apollo-api";
Vue.config.productionTip = false;
Vue.use(VueApollo);
new Vue({
router,
store,
vuetify,
setup() {
provide(ApolloClients, {
default: apiClient,
apiClient,
contentfulClient,
});
},
render: (h) => h(App),
}).$mount("#app");
The clients have their own files and are setup the same:
import { ApolloClient } from "apollo-client";
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
const uri = `https://graphql.contentful.com/content/v1/spaces/${process.env.VUE_APP_CONTENTFUL_SPACE_ID}/environments/${process.env.VUE_APP_CONTENTFUL_ENV}?access_token=${process.env.VUE_APP_CONTENTFUL_ACCESS_TOKEN}`;
const link = createHttpLink({
uri,
});
const cache = new InMemoryCache();
const contentfulClient = new ApolloClient({
link,
cache,
});
export default contentfulClient;
I have this component:
import { defineComponent, onMounted, ref, watch } from "#vue/composition-api";
import { useGetCategory } from "#/logic/get-category";
export default defineComponent({
name: "Categories",
setup(_, context) {
const slug = ref(context.root.$route.params.slug);
const result = ref({});
const getCategory = (slug) => {
console.log(slug);
const { category, loading, error } = useGetCategory(slug);
result.value = { category, loading, error };
};
watch(() => context.root.$route.params.slug, getCategory);
onMounted(() => getCategory(slug.value));
return { result };
},
});
When this component loads, it "gets the category" by executing this:
import { useQuery, useResult } from "#vue/apollo-composable";
import * as getCategoryBySlug from "#/graphql/api/query.category.gql";
export function useGetCategory(slug: string) {
const { result, loading, error } = useQuery(getCategoryBySlug, { slug });
const category = useResult(result, null, (data) => data.getCategoryBySlug);
return { category, loading, error };
}
When the page loads, it gets the category fine, but if I change the route parameter (slug) I expect it to get the new category and display it. But instead I get this error:
So I figured that the setup is wrong in main.ts, so I added the non-composition-api aswell, found here:
https://apollo.vuejs.org/guide/installation.html#_1-apollo-client
Now my main.ts looks like this:
import contentfulClient from "./plugins/vue-apollo-contentful";
import apiClient from "./plugins/vue-apollo-api";
Vue.config.productionTip = false;
const apolloProvider = new VueApollo({
defaultClient: apiClient,
});
Vue.use(apolloProvider);
new Vue({
router,
store,
vuetify,
apolloProvider,
setup() {
provide(ApolloClients, {
default: apiClient,
apiClient,
contentfulClient,
});
},
render: (h) => h(App),
}).$mount("#app");
But this does not work. It compiles, but I still get the same error.
Does anyone know what I need to do to get this to work?
What works for me is
setup() {
provide(DefaultApolloClient, apolloClient);
},

How to register translate function in app

I got this translate function, in order to use it I have to import it in every component which is quite annoying.
Looks like
import { useI18n } from "vue-i18n";
export default {
setup() {
const { t } = useI18n();
return { t }
}
}
main.js looks like
import installI18n from "./lang/index";
installI18n(app);
lang/index
const i18n = createI18n({
locale: getLocale(),
legacy: false,
messages,
});
// export default i18n
export default (app) => {
app.use(i18n);
};
Can I register this somehow in my app, so I dont have to put it in setup every time?
You can use globalInjection parameter to enable global injection even in non-legacy (Composition API) mode
const i18n = createI18n({
locale: getLocale(),
legacy: false,
globalInjection: true,
messages,
});
Then you must use functions prefixed with $ in your templates - see Implicit with injected properties and functions
To use app-wide i18n,
locate translate json files in locales folder - locales/en.json
{
"key": "value",
}
create i18n.js file
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages () {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) // Path to the locales folder
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages()
})
And then use it as param while creating Vue app - in app.js
...
import i18n from './i18n'
...
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#admin-app')
And then use $t() in Vue files. - index.vue
<h3>
{{ $t('key') }}
</h3>

Get current locale in a child component

I don't manage to get the locale parameter from vue-i18n in my child component.
I've installed vue-i18n in cli ui. The translation with $t("message") is working but I have error when i try to access to i18n.locale
my enter point (main.js)
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import i18n from './i18n'
new Vue({
router,
i18n,
render: h => h(App)
}).$mount('#app')
my child component
<template>
<div>{{ $t("message") }}</div>
</template>
<script>
import {HTTP} from '#/http-common'
export default
{
name : 'c1',
methods:{
selectMap()
{
console.log(i18n.locale);//=> doesn't work
}
}
</script>
i18n.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
function loadLocaleMessages () {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
export default new VueI18n({
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages()
})
Try this.$i18n.locale, or just $i18n.locale if inside the <template>.
For Composition API this is my solution:
<script setup>
import { useI18n } from "vue-i18n";
const i18nLocale = useI18n();
console.log(i18nLocale.locale.value); // "en"
</script>