Get i18n locale as global variable in all pages in Next.js - reactjs

I want to get the locale as a global variable in any component of my app without using any other external libraries.
I have json files for translations en.js and es.js:
es.js
export default {
home: 'Inicio',
signin: 'Iniciar Sesion',
welcome: 'Bienvenido',
};
I have already set next.config.js:
i18n: {
locales: ['en', 'es'],
defaultLocale: 'es',
},
And I can get the locale in any component by router.locale:
home.js
import en from "../lib/i18n/en";
import es from "../lib/i18n/es";
const HomePage = () => {
const router = useRouter()
const { locale } = router;
const t = locale === 'en' ? en : es;
return (
<div>t.home</div>
)
}
All good, the question is, how can I replicate this in all the components without having to call the router all the time and replicating the logic to get t. A solution that would allow me something like this:
home.js
import t from "../lib/i18n/t";
const HomePage = () => {
return (
<div>t.home</div>
)
}
Making t a global variable or that all the components have the context to use it.

I am doing it generally different:
//localized.js
import { useRouter } from 'next/router';
export const words = {
login: {
de: 'Login',
en: 'Login',
},
register: {
de: 'Register',
en: 'Registrieren',
},
includeUpdate: {
de: 'Update Link anhΓ€ngen.',
en: 'Include update link.',
},
// ......
}
export default function t(text) {
const { locale } = useRouter();
return text?.[locale] || text;
}
Nor you can use it like so:
import {words, t} from 'path/to/localized.js'
cont Test = () => {
return (
<div>{t(words.login)}</div>
<div>{t({de: "Ein anderer Text.", en: "Some other text."})}</div>
)
}

Related

Clevertap & Segment initialization conflict in NextJS PWA

I am currently working on a PWA in Next JS where I'm using CleverTap for notification campaigning & Segment for logging customer events.
I am facing errors in malfunctioning of CleverTap or loss of user events in Segment everytime I initialise both on app load. After root cause analysis, I suspect that the issue arises due to the scripts for CleverTap & Segment interfering with each other's initialization.
Here's some sample code regarding the initialization and usage of app, CleverTap, Segment, and pushing Clevertap notifications
For initializing CleverTap
import { Clevertap } from 'data/types/Clevertap';
import { getWindow } from './browserUtils';
import { getClevertapAccountId } from './settings';
const clevertap: Clevertap = {
event: [],
profile: [],
account: [],
onUserLogin: [],
notifications: [],
privacy: [],
};
clevertap.account.push({ id: getClevertapAccountId() });
clevertap.privacy.push({ optOut: false });
clevertap.privacy.push({ useIP: false });
const w = getWindow();
if (w) {
w.clevertap = clevertap;
}
const init = (): void => {
if (!w || !getClevertapAccountId()) return;
const wzrk = w.document.createElement('script');
wzrk.type = 'text/javascript';
wzrk.async = true;
wzrk.src = `${
w.document.location.protocol === 'https:'
? 'https://d2r1yp2w7bby2u.cloudfront.net'
: 'http://static.clevertap.com'
}/js/a.js`;
const s = w.document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wzrk, s);
};
export default clevertap;
export { init };
For initializing Segment
import { min as segmentMinSnippet } from '#segment/snippet';
import { getWindow } from 'utils/browserUtils';
import { getSegmentKey } from 'utils/settings';
const w = getWindow();
const jsSnippet = segmentMinSnippet({
apiKey: getSegmentKey(),
page: true,
load: true,
});
const initSegment = (): void => {
if (!w || !getSegmentKey()) return;
const script = document.createElement('script');
script.innerHTML = jsSnippet;
document.head.appendChild(script);
};
// eslint-disable-next-line import/prefer-default-export
export { initSegment };
Calling both initialization functions on _app.tsx start
import CssBaseline from '#material-ui/core/CssBaseline';
import AppContent from 'features/core/AppContent';
import NavProgress from 'features/core/AppContent/NavProgress';
import WithTheme from 'features/core/theme/WithTheme';
import buildEnvConfig from 'features/core/useEnvConfig/buildEnvConfig';
import EnvConfigContext from 'features/core/useEnvConfig/EnvConfigContext';
import EnvConfigVariantContext from 'features/core/useEnvConfig/EnvConfigVariantContext';
import getEnvConfigVariantFromCookie from 'features/core/useEnvConfig/getEnvConfigVariantFromCookie';
import { setEnvConfig } from 'features/core/useEnvConfig/staticEnvConfig';
import 'i18n';
import type { AppProps } from 'next/app';
import NextHead from 'next/head';
import React, { useEffect, useMemo } from 'react';
import 'styles/global.css';
import { init as initClevertap } from 'utils/clevertap';
import { init as initHotjar } from 'utils/hotjar-manager';
import { initSegment } from 'utils/segment';
const BizMartApp = (props: AppProps): React.ReactNode => {
const { Component, pageProps } = props;
useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement?.removeChild(jssStyles);
}
}, []);
useEffect(() => {
setTimeout(() => {
initHotjar();
initClevertap();
initSegment();
});
}, []);
const envConfigVariant = useMemo(() => getEnvConfigVariantFromCookie(), []);
const envConfig = useMemo(() => {
const ec = buildEnvConfig(envConfigVariant);
setEnvConfig(ec);
return ec;
}, [envConfigVariant]);
return (
<>
<NextHead>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</NextHead>
<WithTheme>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<NavProgress />
<EnvConfigVariantContext.Provider value={envConfigVariant}>
<EnvConfigContext.Provider value={envConfig}>
<AppContent>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Component {...pageProps} />
</AppContent>
</EnvConfigContext.Provider>
</EnvConfigVariantContext.Provider>
</WithTheme>
</>
);
};
export default BizMartApp;
Calling Clevertap notification modal
As a hack, I ran them in a non-blocking mode. I simply initialized CleverTap => added delay of 1 second => initialized Segment.
This seemed to work just fine, except that the user events in the 1st second were not logged by Segment.
I implemented the following hacky solution by calling initSegment after a 1 second delay as opposed to the earlier version in _app.tsx:
Changes in _app.tsx
....
useEffect(() => {
setTimeout(() => {
initHotjar();
initClevertap();
});
setTimeout(() => {
initSegment();
}, 1000);
}, []);
....
While the above hacked worked great in initializing & making CleverTap & Segment work in a non-blocking way & we can even use a debounce on all user Segment events happening in the 1st second, to not lost them; is there a way in which we can achieve both of this without this setTimeout hack ?

react-i18Next: Using an enum for translation key values

We are using i18Next in our React project and I wonder if there is way to use an enum for keys of the translation file to avoid typos while using it like this:
export enum AppLocaleKey {
test = 'test'
}
...
import translationEN from './locales/en/translation';
const resources = {
en: { translation: translationEN },
...
};
i18n
.use(initReactI18next)
.init({
resources,
...
})
...
const translation = {
[AppLocaleKey.test]: 'Test...',
};
export default translation;
...
import { AppLocaleKey } from './locales/localeKeys';
import { useTranslation } from 'react-i18next';
const App = (props: Props) => {
const { t, i18n } = useTranslation();
return (
<>
<p>{t(AppLocaleKey.test)}</p>
<>
)
}
But this didn't work. Do you know any similar method?
If you are using TS you can with ts4.1 declare all the keys of the json as a valid inputs.
Check out the official example,
And working example out of it.
I believe this is what you are looking for
export enum VisitorType {
SCHOOL_VS,
SCHOOL_NOT_VS,
GROUP,
PRIVATE
}
...
resources: {
en: {
translation: {
visitorType: {
[VisitorType.SCHOOL_VS]: 'State school',
[VisitorType.SCHOOL_NOT_VS]: 'Non state school',
[VisitorType.GROUP]: 'Private groups',
[VisitorType.PRIVATE]: 'Private'
},
}
},
...
t(`visitorType.${VisitorType.SCHOOL_VS}`)

Nextjs getStaticProps error when passing an object as prop instead of plain string

I hope you're doing fine πŸ‘‹.
Playing around with the new internalization NextJS feature, I found out an error I was not expecting at all.
If I pass a plain string as prop from the getStaticProps to the Page Component everything works fine in the default locale, but if I pass down an object instead of a plain string, it breaks.
I leave both codes here.
The following code is the one that works fine πŸ‘‡:
it passes down a string value
works fine in both locales /en and /es even without it (it picks the default locale)
import { useRouter } from "next/dist/client/router";
export async function getStaticPaths() {
return {
paths: [
{ params: { name: `victor`, locale: "es" } },
{ params: { name: `victor`, locale: "en" } },
],
fallback: true,
};
}
export async function getStaticProps(context) {
const { params } = context;
const { name } = params;
return {
props: {
name,
},
};
}
/*
* βœ… The following URLs work
* - localhost:3000/victor
* - localhost:3000/en/victor
* - localhost:3000/es/victor
*
* TypeError: Cannot destructure property 'name' of 'person' as it is undefined.
*/
export default function PageComponent({ name }) {
const router = useRouter();
return (
<>
<div>The name is: {name}</div>
<div>Locale used /{router.locale}/</div>
</>
);
}
The following code is the one that DOESN'T WORK πŸ‘‡:
It passes down an object
Works fine in '/en' and without explicit locale (default locale) but doesn't work with /es
import { useRouter } from "next/dist/client/router";
export async function getStaticPaths() {
return {
paths: [
{ params: { name: `victor`, locale: "es" } },
{ params: { name: `victor`, locale: "en" } },
],
fallback: true,
};
}
export async function getStaticProps(context) {
const { params } = context;
const { name } = params;
return {
props: {
person: {
name,
},
},
};
}
/*
* βœ… The following URLs work
* - localhost:3000/victor
* - localhost:3000/en/victor
*
* πŸ‘Ž The following URLs DOESN'T work
* - localhost:3000/es/victor
*
* TypeError: Cannot destructure property 'name' of 'person' as it is undefined.
*/
export default function PageComponent(props) {
const router = useRouter();
const { person } = props;
const { name } = person;
return (
<>
<div>The name is: {name}</div>
<div>Locale used /{router.locale}/</div>
</>
);
}
It is because you are using fallback: true.
The page is rendered before the data is ready, you need to use router.isFallback flag to handle this situation inside your component:
if (router.isFallback) {
return <div>Loading...</div>
}
Or you can use fallback: 'blocking' to make Next.js wait for the data before render the page.
More info in the docs

Next.JS useRouter will only returns an empty object for a dynamic route

I am making a bit of a specialized calendar application in Next.JS and I am have some problems with dynamic routing that I can't seem to figure out.
I have two similar pages with similar routes, where one is working perfectly fine and the other not working at all.
First page (working):
// pages/date/[year]/[month]/[dayOfMonth].tsx
import { useRouter } from "next/router";
import { Day } from "../../../../components/Day";
type StringQueryParams = Record<keyof QueryParams, string>;
interface QueryParams {
year: number;
month: number;
dayOfMonth: number;
}
const transformParams = ({ year, month, dayOfMonth }: StringQueryParams): QueryParams => ({
year: parseInt(year),
month: parseInt(month),
dayOfMonth: parseInt(dayOfMonth),
});
const DayPage: React.FC = () => {
const { query } = useRouter();
const params = transformParams(query as StringQueryParams);
return <Day {...params} />;
};
export default DayPage;
Second page (not working)
// pages/date/[year]/[month].tsx
import { useRouter } from "next/router";
import { Month } from "../../../components/Month";
import { validateMonth } from "../../../lib/month";
type StringQueryParams = Record<keyof QueryParams, string>;
interface QueryParams {
month: string;
year: number;
}
const MonthDisplay: React.FC = () => {
const router = useRouter();
debugger;
const { query } = router;
const { month: rawMonth, year: rawYear } = query as StringQueryParams;
console.log("query:", query);
console.log("router:", router);
const month = validateMonth(rawMonth);
const year = parseInt(rawYear);
return <Month name={month} year={year} />;
};
export default MonthDisplay;
With output
query: {}
router: ServerRouter {
route: '/date/[year]/[month]',
pathname: '/date/[year]/[month]',
query: {},
asPath: '/date/[year]/[month]',
basePath: '',
events: undefined,
isFallback: false
}
I cannot for the life of me seem to figure out why the dynamic routing for the second page is not working at all, and not returning any query from the useRouter() hook.
Moving pages/date/[year]/[month].tsx to pages/date/[year]/[month]/index.tsx should work
Your tree should be look like this
Pages
[years]
[months]
index.tsx --> your 2nd page
[days]
indes.tsx --> your first page
Documentation

Registering React Native Code Push with React Native Navigation by Wix

I use react-native-code-push. which is:
This plugin provides client-side integration for the CodePush service,
allowing you to easily add a dynamic update experience to your React
Native app(s).
but In some of native implementations of navigation like react-native-navigation there isn't any root component.
the app will start calling a function like this:
// index.js
import { Navigation } from 'react-native-navigation';
Navigation.startTabBasedApp({
tabs: [
{
label: 'One',
screen: 'example.FirstTabScreen', // this is a registered name for a screen
icon: require('../img/one.png'),
selectedIcon: require('../img/one_selected.png'), // iOS only
title: 'Screen One'
},
{
label: 'Two',
screen: 'example.SecondTabScreen',
icon: require('../img/two.png'),
selectedIcon: require('../img/two_selected.png'), // iOS only
title: 'Screen Two'
}
]
});
// or a single screen app like:
Navigation.registerComponent('example.MainApplication', () => MainComponent);
Navigation.startSingleScreenApp({
screen: {
screen: 'example.MainApplication',
navigatorButtons: {},
navigatorStyle: {
navBarHidden: true
}
},
})
since there is no root component, It's not clear where should I call CodePush, since normally I should wrap my whole root component with CodePush like a higher order component.
what I used to do was:
// index.js
class MyRootComponent extends Component {
render () {
return <MainNavigator/> // a navigator using react-navigation
}
}
let codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
installMode: CodePush.InstallMode.ON_NEXT_RESUME
}
export default CodePush(codePushOptions)(MyRootComponent)
Is there a proper way to solve this problem!?
I know I could do this:
Navigation.registerComponent('example.MainApplication', () => CodePush(codePushOptions)(RootComponent));
Navigation.startSingleScreenApp({
screen: {
screen: 'example.MainApplication',
navigatorButtons: {},
navigatorStyle: {
navBarHidden: true
}
},
})
but then I should use a Navigator only for projecting my root component, and It doesn't look like a good idea. I think this problem probably has a best-practice that I'm looking for.
UPDATE
I think there are some complications registering a tab navigator inside a stacknavigator in react-native-navigation at least I couldn't overcome this problem. example tabBasedApp in react-native-navigation with react-native-code-push, will be all that I need.
Thanks for the previous code snippets. I was able to get code push check on app resume and update immediately with react-native-navigation V2 with the below code without requiring wrapper component for codePush. This is the relevant part of the app startup logic.
Navigation.events().registerAppLaunchedListener(() => {
console.log('Navigation: registerAppLaunchedListener ')
start()
})
function checkCodePushUpdate () {
return codePush.sync({
checkFrequency: codePush.CheckFrequency.ON_APP_RESUME,
installMode: codePush.InstallMode.IMMEDIATE,
deploymentKey: CODEPUSH_KEY,
})
}
function start () {
checkCodePushUpdate ()
.then(syncStatus => {
console.log('Start: codePush.sync completed with status: ', syncStatus)
// wait for the initial code sync to complete else we get flicker
// in the app when it updates after it has started up and is
// on the Home screen
startApp()
return null
})
.catch(() => {
// this could happen if the app doesn't have connectivity
// just go ahead and start up as normal
startApp()
})
}
function startApp() {
AppState.addEventListener('change', onAppStateChange)
startNavigation()
}
function onAppStateChange (currentAppState) {
console.log('Start: onAppStateChange: currentAppState: ' + currentAppState)
if (currentAppState === 'active') {
checkCodePushUpdate()
}
}
function startNavigation (registered) {
console.log('Start: startNavigation')
registerScreens()
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'FirstScreen,
},
}],
},
},
})
}
I got it working this way, although this is for RNN v2
// index.js
import App from './App';
const app = new App();
// App.js
import CodePush from 'react-native-code-push';
import { Component } from 'react';
import { AppState } from 'react-native';
import { Navigation } from 'react-native-navigation';
import configureStore from './app/store/configureStore';
import { registerScreens } from './app/screens';
const appStore = configureStore();
registerScreens(appStore, Provider);
const codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
updateDialog: true,
installMode: CodePush.InstallMode.IMMEDIATE
};
export default class App extends Component {
constructor(props) {
super(props);
// Set app state and listen for state changes
this.appState = AppState.currentState;
AppState.addEventListener('change', this.handleAppStateChange);
this.codePushSync();
Navigation.events().registerAppLaunchedListener(() => {
this.startApp();
});
}
handleAppStateChange = nextAppState => {
if (this.appState.match(/inactive|background/) && nextAppState === 'active') {
this.handleOnResume();
}
this.appState = AppState.currentState;
};
codePushSync() {
CodePush.sync(codePushOptions);
}
handleOnResume() {
this.codePushSync();
...
}
startApp() {
Navigation.setRoot({
root: {
stack: {
children: [
{
component: {
name: 'MyApp.Login'
}
}
]
}
}
});
}
}
// app/screens/index.js
import CodePush from 'react-native-code-push';
import { Navigation } from 'react-native-navigation';
import Login from './Login';
function Wrap(WrappedComponent) {
return CodePush(WrappedComponent);
}
export function registerScreens(store, Provider) {
Navigation.registerComponentWithRedux(
'MyApp.Login',
() => Wrap(Login, store),
Provider,
store.store
);
...
}
I found the answer myself.
Look at this example project structure:
.
β”œβ”€β”€ index.js
β”œβ”€β”€ src
| └── app.js
└── screens
β”œβ”€β”€ tab1.html
└── tab2.html
you can register you code-push in index.js.
//index.js
import { AppRegistry } from 'react-native';
import App from './src/app';
import CodePush from 'react-native-code-push'
let codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
installMode: CodePush.InstallMode.ON_NEXT_RESUME
}
AppRegistry.registerComponent('YourAppName', () => CodePush(codePushOptions)(App));
now you can start react-native-navigation in app.js like this:
import {Navigation} from 'react-native-navigation';
import {registerScreens, registerScreenVisibilityListener} from './screens';
registerScreens();
registerScreenVisibilityListener();
const tabs = [{
label: 'Navigation',
screen: 'example.Types',
icon: require('../img/list.png'),
title: 'Navigation Types',
}, {
label: 'Actions',
screen: 'example.Actions',
icon: require('../img/swap.png'),
title: 'Navigation Actions',
}];
Navigation.startTabBasedApp({
tabs,
tabsStyle: {
tabBarBackgroundColor: '#003a66',
tabBarButtonColor: '#ffffff',
tabBarSelectedButtonColor: '#ff505c',
tabFontFamily: 'BioRhyme-Bold',
},
appStyle: {
tabBarBackgroundColor: '#003a66',
navBarButtonColor: '#ffffff',
tabBarButtonColor: '#ffffff',
navBarTextColor: '#ffffff',
tabBarSelectedButtonColor: '#ff505c',
navigationBarColor: '#003a66',
navBarBackgroundColor: '#003a66',
statusBarColor: '#002b4c',
tabFontFamily: 'BioRhyme-Bold',
}
});

Resources