How to properly use ChainedBackend with i18next when managing server side translations with local fallback - i18next

I am working in a React Native application and managing our translations using i18next, though I am having some difficulty with the following scenario...
What I wish to do is, when the app loads up and i18next is initialised to attempt to load the latest translations by fetching the translation files from our server, but fallback to a local collection of these files in case there's a problem with the user's connection. (it would seem to me this would be a relatively common use case)
So, this is my i18n.ts file
import i18n from 'i18next';
import storage from '#react-native-firebase/storage';
import ChainedBackend from 'i18next-chained-backend';
import backend from 'i18next-http-backend';
import resourcesToBackend from 'i18next-resources-to-backend';
import AsyncStoragePlugin from 'i18next-react-native-async-storage';
import namespaces from '../config/translations/namespaces.json';
const firebaseStoragePath = '/assets/translations/latest';
const enTranslations = require('../config/translations/generated/en.json');
const enEuTranslations = require('../config/translations/generated/en-EU.json');
const frTranslations = require('../config/translations/generated/fr-FR.json');
const beTranslations = require('../config/translations/generated/nl-BE.json');
const nlTranslations = require('../config/translations/generated/nl-NL.json');
const prebuildLanguages = {
jsons: {
en: enTranslations,
'en-EU': enEuTranslations,
'fr-FR': frTranslations,
'nl-BE': beTranslations,
'nl-NL': nlTranslations,
},
namespaces,
};
const loadResources = async (language: string, namespace: string) => {
return storage()
.ref(`${firebaseStoragePath}/${language}/${namespace}.json`)
.getDownloadURL()
.then((result) =>
fetch(result)
.then((response) => {
return response.json();
})
.catch(() => {
return prebuildLanguages.jsons[language][namespace];
})
)
.catch((error) => {
return prebuildLanguages.jsons[language][namespace];
});
};
const backendOptions = {
loadPath: '{{lng}}|{{ns}}',
request: (options: any, url: any, payload: any, callback: any) => {
try {
const [lng, ns] = url.split('|');
loadResources(lng, ns).then((response) => {
callback(null, {
data: response,
status: 200,
});
});
} catch (e) {
callback(null, {
status: 500,
});
}
},
};
i18n
.use(AsyncStoragePlugin('en'))
.use(ChainedBackend)
.init({
ns: namespaces,
defaultNS: 'app_common',
supportedLngs: ['en', 'en-EU', 'fr-FR', 'nl-BE', 'nl-NL'],
fallbackLng: 'en',
debug: true,
saveMissing: false,
backend: {
backends: [backend, resourcesToBackend(prebuildLanguages.jsons)],
backendOptions: [backendOptions, null],
},
});
export default i18n;
The idea here is that, by using the ChainedBackend library I would first try load the backend using the backendOptions, which would attempt to load the resources from our Firebase Storage path. If I understand correctly, if this backend "fails", it should then load the local files using the resourcesToBackend(prebuildLanguages.jsons) backend.
The problem I'm facing here is that if I shut off my internet on my device I notice that the loadResources function can take time to resolve because the getDownloadURL() I think has a timeout of a few seconds.
So during this time, my app loads but the i18n is not yet initialised and so the app would throw the following errors..
'i18next: hasLoadedNamespace: i18next was not initialized', [ 'en' ]
'i18next::translator: key "new_onboarding_landing_title" for languages "en" won\'t get resolved as namespace "app_onboarding" was not yet loaded', 'This means something IS WRONG in your setup. You access the t function before i18next.init / i18next.loadNamespace / i18next.changeLanguage was done. Wait for the callback or Promise to resolve before accessing it!!!'`
This is my App file where Im passing in the i18n object
import React, { useEffect, useState, Suspense } from 'react';
import { StatusBar, Platform, LogBox, Appearance, useColorScheme } from 'react-native';
import { I18nextProvider } from 'react-i18next';
import { RootReduxProvider } from './redux';
import { i18n } from './utils';
const App = () => {
return (
<I18nextProvider i18n={i18n}>
<React.Fragment>
<ApplicationProvider mapping={mapping} theme={appTheme}>
<RootReduxProvider>
<WrappedRootNavigator theme={themeSettings} />
</RootReduxProvider>
</ApplicationProvider>
</React.Fragment>
</I18nextProvider>
);
};
export default App;
So I get what's going on... the app is trying to initialise i18n using the primary backend option, but the initialisation is taking too long due to fetch request timing out when there is no connection.
I thought then to switch the backends around and initialise with the local translations,
ie
backend: {
backends: [resourcesToBackend(prebuildLanguages.jsons), backend],
backendOptions: [null, backendOptions],
},
But then of course, from what im seeing, it doesnt even attempt to load the remote backend because why would it if nothing went wrong with the local backend I guess?
So my question I have, what is the best way to handle this scenario?
I think one solution would be to always load the local resource/backend first, then at some point later try fetch any updated files and then overwrite the local files with the new translations, but I was hoping to try do it this way where I would load/init the local resources first while the remote backend resolves. I just not sure how to do it that way elegantly.
Any help would be greatly appreciated :)
thanks!

Related

Socket.io with Vue3

I have a Vue 3 app and an express server. The server does not serve any pages just acts as an API so no socket.io/socket.io.js file is sent to client.
I am trying to set up socket.io in one of my vue components but whatever I try does not work. Using vue-3-socket.io keeps giving 't.prototype is undefined' errors.
I have tried vue-socket.io-extended as well with no luck.
Any advice would be appreciated as to the reason and solution for the error above, I have tried various SO solutions without success, and the best way forward.
You can use socket.io-client. I have used socket.io-client of 4.4.1 version.
step: 1
Write class inside src/services/SocketioService.js which returns an instance of socketio.
import {io} from 'socket.io-client';
class SocketioService {
socket;
constructor() { }
setupSocketConnection() {
this.socket = io(URL, {
transports: ["websocket"]
})
return this.socket;
}
}
export default new SocketioService();
Step 2:
Import SocketioService in App.vue. You can instantiate in any lifecycle hook of vue. I have instantiated on mounted as below. After instantiation, I am listening to welcome and notifications events and used quasar notify.
<script>
import { ref } from "vue";
import SocketioService from "./services/socketio.service.js";
export default {
name: "LayoutDefault",
data() {
return {
socket: null,
};
},
components: {},
mounted() {
const socket = SocketioService.setupSocketConnection();
socket.on("welcome", (data) => {
const res = JSON.parse(data);
if (res?.data == "Connected") {
this.$q.notify({
type: "positive",
message: `Welcome`,
classes: "glossy",
});
}
});
socket.on("notifications", (data) => {
const res = JSON.parse(data);
let type = res?.variant == "error" ? "negative" : "positive";
this.$q.notify({
type: type,
message: res?.message,
position: "bottom-right",
});
});
},
};
</script>

Understanding context and app methods in NUXT

I am trying to use bugsnagClient and its notify method in plugins/axios.js I have this code in plugins/bugsnag.js
import Vue from "vue"
import bugsnag from "#bugsnag/js"
import bugsnagVue from "#bugsnag/plugin-vue"
// const bugsnagClient = bugsnag(`${process.env.BUGSNAG_API_KEY}`)
var bugsnagClient = bugsnag({
apiKey: "",
notifyReleaseStages: ["production"]
})
bugsnagClient.use(bugsnagVue, Vue)
I want to attach a method to app or context as
export default ({ app }, inject) => {
function bugsnagNotify(error) {
return bugsnagClient.notify(new Error(error))
}
// Set the function directly on the context.app object
app.bugsnagNotify = bugsnagNotify
}
And I want to use it in plugins/axios.js
export default function({ store, app }) {
if (store.getters.token) {
console.log(app.bugsnagNotify("ss"))
app.$axios.setToken(store.getters.token, "Bearer")
} else {
//app.$bugsnag.notify(new Error("Bearer tooken is missing in Axios request."))
}
}
In this file, when I do console.log for just app
I can see bugsnagNotify: ƒ bugsnagNotify(error)
but when I call app.bugsnagNotify("error") I only get error such as VM73165:37 TypeError: app.bugsnagNotify is not a function
I have also tried this in plugins/bugsnag.js
export default (ctx, inject) => {
inject('bugsnag', bugsnagClient)
}
I only get an error as
app.$bugsnag.notify(new Error("Bearer tooken is missing in Axios request."))
If you are injecting into context inside one plugin and want to use that function inside another, you need to make sure that the plugin in which you are injecting comes first inside nuxt.config.js
...
plugins: [
'~/plugins/bugsnag.js',
'~/plugins/axios.js'
],
...

How to initialize the ability instance rules when the application starts?

I am trying to implement CASL with vuex and Nuxt. I get an issue when trying to initialize ability's rules when my application starts and I am already logged in.
Basically, I would like to get the rules and updates the Ability instance when the app starts. However, when I try to get the rules from the store, it returns null. At the moment, I need to log out and log in to get the rules.
store/ability.js
import ability from '../config/ability'
export const updateAbilities = store => {
ability.update(store['users/getRules']) // this does not work and returns null
return store.subscribe(mutation => {
if (mutation.type === 'users/setRules') {
ability.update(mutation.payload)
}
})
}
config/ability.js
import { Ability } from '#casl/ability'
export default new Ability()
store/index.js
import { updateAbilities } from './ability'
export const plugins = [updateAbilities]
Thanks for your help.
I ended up by getting the rule from the Local Storage and it works.
import ability from '../config/ability'
export const updateAbilities = store => {
const vuexData = JSON.parse(localStorage.getItem('vuex'))
const rules = vuexData.users.rules
ability.update(rules)
return store.subscribe(mutation => {
if (mutation.type === 'users/setRules') {
ability.update(mutation.payload)
}
})
}

Not able to access i18 plugin from mutation in classic mode store in Nuxt application

I'm trying to implement Vuex i18n package within my Nuxt application. In my nuxt.conf.js file in plugins array I have:
{
src: '#/plugins/i18n.js',
ssr: false
},
plugins/i18n.js file is:
import Vue from "vue";
import vuexI18n from "vuex-i18n/dist/vuex-i18n.umd.js";
import toEnglish from "../translations/toEnglish";
import toSpanish from "./../translations/toSpanish";
import toGerman from "./../translations/toGerman";
export default ({ store }) => {
Vue.use(
vuexI18n.plugin,
store,
{
onTranslationNotFound: function (locale, key) {
console.warn(`vuex-i18n :: Key '${key}' not found for locale '${locale}'`)
}
}
);
// register the locales
Vue.i18n.add('en', toEnglish);
Vue.i18n.add('de', toGerman);
Vue.i18n.add('es', toSpanish);
// Set the start locale to use
Vue.i18n.set('de');
Vue.i18n.fallback('en');
}
Last thing is my store. I'm using classic mode of vuex store in Nuxt:
import Vuex from "vuex";
const store = () => {
return new Vuex.Store({
state: () => ({
currentLanguage: ''
}),
mutations: {
changeLang(state, response) {
if (response) {
console.log(this);
state.currentLanguage = response;
this.i18n.set(response);
}
}
}
})
};
export default store;
As you can see in store file in mutation I'm trying to access i18n plugin with this keyword. Unfortunetally in print error in console:
TypeError: Cannot read property 'set' of undefined
this which I consoled also inside mutation is:
I changed this.i18n.set(response); to state.i18n.locale = response; inside my mutation and now it seems working.
For some reason when I call this mutation my video.js player refresh. I will try to find out why.

Nuxt: Vuex commit or dispatch message outside vuejs component

I have an application in nuxt that I want to connect to a websocket, I have seen examples where the callback to receive messages is placed inside a component, but I do not think ideal, I would like to place the callback inside my store, currently my code is something like this
//I'm using phoenix websocket
var ROOT_SOCKET = `wss://${URL}/socket`;
var socket = new Socket(ROOT_SOCKET);
socket.connect()
var chan = socket.channel(`connect:${guid}`);
chan.join();
console.log("esperando mensj");
chan.on("translate", payload => {
console.log(JSON.stringify(payload));
<store>.commit("loadTranslation",payload) //<- how can I access to my store?
})
chan.onError(err => console.log(`ERROR connecting!!! ${err}`));
const createStore = () => {
return new Vuex.Store({
state: {},
mutations:{
loadTranslation(state,payload){...}
},
....
})}
how can I access to my store inside my own store file and make a commit??? is it possible?...
I know there is a vuex plugin but I can't really understand well the documentation and I'll prefer build this without that plugin
https://vuex.vuejs.org/guide/plugins.html
thank you guys...hope you can help me...
You can do it in nuxt plugin https://nuxtjs.org/guide/plugins/
export default {
plugins: ['~/plugins/chat.js']
}
// chat.js
export default ({ store }) => {
your code that use store here
}