How can I mock Window in #testing-library or update my configurei18n.ts so it works with #testing-library - testing

Summary
I have a codebase that I'm working on and the i18next/react-i18next. The internationalization is working, but having issues with #testing-library. Normally, the i18n resource files are in .json format, but these are different and can't be changed, so the changes have be make with i18n config or #test-library mocks.
The code currently works by looking at the window, checking the resources folder, and then looking for the language. So if the language is en, it's going to look for the file en in resources = window.Resources.en.
Error
TypeError: Cannot read properties of undefined (reading 'en')
> i18n.addResourceBundle(cultureName, 'translation', window['Resources'][cultureName]);
Project File Structure:
./Resources/en.js
./Code
./Scripts/configurei18n.ts
.package.json
configurei18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
export default function configurei18n(cultureName: string): typeof i18n {
i18n
.use(initReactI18next)
.init({
lng: cultureName,
fallbackLng: cultureName,
supportedLngs: [cultureName],
interpolation: {
escapeValue: false
},
initImmediate: false,
react: {
useSuspense: false
}
});
i18n.addResourceBundle(cultureName, 'translation', window['Resources'][cultureName]);
return i18n;
}
Resource File (English) (en.js)
(function () {
if (typeof window === 'undefined')
window = {};
window.Resources = window.Resources || {};
return window.Resources.en = window.Resources.en || {
"home_title": "REGISTRATION",
"help_header": {
"part1": "Need Help?",
"part2": "Please Contact Customer Service at",
"part3": "Call Customer Service at"
},
}
};
})();

Related

Get i18n language from Async Storage

I am using i18n to translate a React Native app. In i18n.js file:
const getLang = async () => {
const language = await AsyncStorage.getItem("locale");
// console.log(`language |==> `, language);
return "pt";
};
// passes i18n down to react-i18next
i18n.use(initReactI18next).init({
resources: {
en,
pt,
},
lng: getLang(), // getting language from local storage
interpolation: {
escapeValue: false,
},
react: { useSuspense: false },
});
export default i18n;
using getLang() function I try to access Async Storage and get the user's selected language, but I am getting the below error:
I couldn't copy-paste the error, so this is the error I get in the simulator. How do I resolve above stated issue?
Thank You
Because i was looking for help but couldn't find any, i did the following.
In use({}) added a custom languageDetector:
i18n
.use(initReactI18next)
.use({
type: 'languageDetector',
name: 'customDetector',
async: true, // If this is set to true, your detect function receives a callback function that you should call with your language, useful to retrieve your language stored in AsyncStorage for example
init: function () {
/* use services and options */
},
detect: function (callback: (val: string) => void) {
console.log('[LANG] detecting language');
AsyncStorage.getItem('LANG').then((val: string | null) => {
const detected = val || fallbackLanguage;
console.log('[LANG] detected:', detected);
callback(detected);
});
},
cacheUserLanguage: function (lng: string) {
return lng;
},
})
.init({
resources,
fallbackLng: fallbackLanguage,
interpolation: {
escapeValue: false, // react already safes from xss
},
returnObjects: true,
debug: true,
// react-i18next options
react: {
useSuspense: true,
},
detection: {
order: ['customDetector'],
},
})
.then(() => console.log('[INIT] i18n initialized'));
Note it's important the detection.order part because it won't be called otherwise.

AsyncStorage: Invariant Violation: Module AppRegistry is not a registered callable module

I'm using react-i18next to translate a React Native app. It works, but I can't retrieve the user's locale in AsyncStorage when launching the app. The error is:
ERROR TypeError: undefined is not a function, js engine: hermes
ERROR Invariant Violation: Module AppRegistry is not a registered callable module (calling runApplication). A frequent cause of the error is that the application entry file path is incorrect.
This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native., js engine: hermes
The code is:
import i18n, { LanguageDetectorAsyncModule } from "i18next";
import { initReactI18next } from "react-i18next";
import { Platform, NativeModules } from "react-native";
import AsyncStorage from "#react-native-async-storage/async-storage";
import en from "./en.json";
import fr from "./fr.json";
const languages = {
EN: "en",
FR: "fr",
};
const resources = {
en: { translation: en },
fr: { translation: fr },
};
const detectLocale = async () => {
const storedLocale = await AsyncStorage.getItem("locale");
if (storedLocale) {
return JSON.parse(storedLocale);
}
const mobileLocale =
Platform.OS === "ios"
? NativeModules.SettingsManager.settings.AppleLocale ||
NativeModules.SettingsManager.settings.AppleLanguages[0]
: NativeModules.I18nManager.localeIdentifier;
if (mobileLocale) {
return mobileLocale.split("_")[0];
}
return languages.EN;
};
const LanguageDetector = {
type: "languageDetector" as LanguageDetectorAsyncModule["type"],
async: false,
init: () => {},
detect: detectLocale,
cacheUserLanguage: () => {},
};
export default i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: languages.EN,
keySeparator: ".",
whitelist: [languages.EN, languages.FR],
interpolation: { escapeValue: false },
});
So the error comes from the detectLocale function, called in LanguageDetector.detect(). It's async, I use the await keyword. Everything works fine without AsyncStorage, so the error must come from here.
I use React Native CLI, no expo, the version is the most recent one to this date: 0.64.1.
I was having a similiar issue.
In order to use async custom language detector, you need to set async to true. Then you will receive callback function on detect().
const languageDetector: LanguageDetectorAsyncModule = {
type: "languageDetector",
async: true,
// Since we set async to true, detect has now callback function...
detect: async (callback: (lang: string) => void) => {
const language = await AsyncStorage.getItem('language')
callback(lastSelectedLang);
},
init: () => {},
cacheUserLanguage: (lng: string) =>
AsyncStorage.setItem('language', lng),
};

How to make react native app save the language when changed from the app with i18next?

I'm using i18next in react native to use multi languages in the app :
the user can change the language from the app by clicking on a button
in this button I make an action to set the language in AsyncStorage ,
in i18next init file I want to use the value of the AsyncStorage, but its not changing it because AsyncStorage it need async and await so it take long time to change the value ,
this is the code :
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import English from '../Translation/Languages/English.json';
import Spanish from '../Translation/Languages/Spanish.json';
import Arabic from '../Translation/Languages/Arabic.json';
import AsyncStorage from '#react-native-async-storage/async-storage';
let language = null;
const changeLanguage = async () => {
try {
const Lang = await AsyncStorage.getItem('Language');
if (Lang !== null) {
language = Lang;
}
}
catch (error) {
console.log("Error ", error)
}
};
changeLanguage();
i18n
.use(initReactI18next)
.init({
lng: language,
fallbackLng: 'en',
resources: {
en: English,
es: Spanish,
ar: Arabic
}
});
export default i18n;
Checkout this medium-post for a working sample...
Github Repo
https://github.com/hend-elsahli/ReactNativeLocalization
use languageDetector
i18n
.use(languageDetector)
.init(...)
const languageDetector = {
init: Function.prototype,
type: 'languageDetector',
async: true, // flags below detection to be async
detect: async callback => {
const selectedLanguage = await AsyncStorage.getItem('Language');
/** ... */
callback(selectedLanguage);
},
cacheUserLanguage: () => {},
};

Shouldn't i18next wait for language detection result before loading fallback language

I use react-i18next example as a base.
I replaced 'i18next-browser-languagedetector' by custom language detector:
class MyLanguageDetector {
constructor(services, options = {}) {
this.async = true;
this.init(services, options);
}
init(services, options = {}, i18nOptions = {}) { }
detect(callback) {
setTimeout(() => {
callback('de')
}, 5000);
}
cacheUserLanguage(lng, caches) { }
}
MyLanguageDetector.type = 'languageDetector';
Config:
i18n
.use(MyLanguageDetector)
.use(Backend)
.use(reactI18nextModule)
.init({
fallbackLng: 'en',
debug: true,
interpolation: { escapeValue: false },
react: { wait: true }
});
What I see is:
i18next::backendConnector: loaded namespace translation for language en
and after ~5 seconds:
i18next::backendConnector: loaded namespace translation for language de
I expected that 'en' won't be loaded since detected language is 'de'
fallbackLng is always loaded so that it can be used if current language translation file missing the particular key/resource
See related issue

Aurelia Validation with i18n?

Has anyone gotten Aurelia Validation to work with the i18n Plugin for multi-lingual error messages? My app won't even start when I add in the code from the Aurelia documentation http://aurelia.io/hub.html#/doc/article/aurelia/validation/latest/validation-basics/12.
Here's my main.js:
import environment from './environment';
import {I18N} from 'aurelia-i18n';
import XHR from 'i18next-xhr-backend';
import {ValidationMessageProvider} from 'aurelia-validation';
//Configure Bluebird Promises.
//Note: You may want to use environment-specific configuration.
Promise.config({
warnings: {
wForgottenReturn: false
}
});
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.feature('resources')
.plugin('aurelia-validation');
aurelia.use.plugin('aurelia-i18n', (instance) => {
// register backend plugin
instance.i18next.use(XHR);
// adapt options to your needs (see http://i18next.com/docs/options/)
instance.setup({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
lng : 'en',
ns: ['translation'],
defaultNS: 'translation',
attributes : ['t','i18n'],
fallbackLng : 'en',
debug : false
});
});
// Straight from Aurelia Documentation
const i18n = aurelia.container.get(i18n);
ValidationMessageProvider.prototype.getMessage = function(key) {
const translation = i18n.tr(`errorMessages.${key}`);
return this.parser.parseMessage(translation);
};
// Straight from Aurelia Documentation
ValidationMessageProvider.prototype.getDisplayName = function(propertyName) {
return i18n.tr(propertyName);
};
if (environment.debug) {
aurelia.use.developmentLogging();
}
if (environment.testing) {
aurelia.use.plugin('aurelia-testing');
}
aurelia.start().then(() => aurelia.setRoot());
}
The error I get is vendor-bundle.js:3394 Error: key/value cannot be null or undefined. Are you trying to inject/register something that doesn't exist with DI?(…)
If I delete the two sections marked // Straight from Aurelia Documentation, it works fine (but only in one language).
If you see an error in my code, please point it out. Or, if you have a working example using aurelia-validation and aurelia-i18n working together, please pass on a link. Thanks!
Ran into this issue as well. It appears that the line
// Straight from Aurelia Documentation
const i18n = aurelia.container.get(i18n);
is getting (or more likely creating) a different instance of i18n than the
aurelia.use.plugin('aurelia-i18n', (instance) =>
I fixed this by getting the i18n instance directly from the aurelia.use.plugin() as follows (this is typescript but same principle applies to pure js):
let i18n:I18N = null;
aurelia.use.plugin('aurelia-i18n', (instance:I18N) => {
i18n = instance;
//rest of plugin code here
}
Use the imported I18N instead:
const i18n = aurelia.container.get(I18N);
But indeed, i18n seems to stop working afterward. My solution was to update the i18n singleton instance in the first page (app.js), the first time it gets injected:
constructor(i18n) {
this.i18n = i18n;
this.initAureliaSingletons();
}
/**
* Some configurations breaks in 'main.js'
* singletons can be configure here
* #return {void}
*/
initAureliaSingletons() {
const i18n = this.i18n;
ValidationMessageProvider.prototype.getMessage = function(key) {
const translation = i18n.tr(`validation-${key}`);
return this.parser.parseMessage(translation);
};
}
I put it on my main and it works. I think that the trick was to use the variable that was initialized in the plug-in initialization:
var i18n;
aurelia.use.plugin('aurelia-i18n', (instance) => {
// register backend plugin
instance.i18next.use(Backend.with(aurelia.loader)).use(LngDetector);
i18n = instance;
(...)
aurelia.use.plugin('aurelia-validation');
var standardGetMessage = ValidationMessageProvider.prototype.getMessage;
ValidationMessageProvider.prototype.getMessage = function (key) {
if (i18n.i18next.exists(key)) {
const translation = i18n.tr(key);
return this.parser.parse(translation);
} else {
return standardGetMessage(key);
}
};
ValidationMessageProvider.prototype.getDisplayName = function (propertyName, displayName) {
if (displayName !== null && displayName !== undefined) {
return displayName;
}
return i18n.tr(propertyName);
};