Creating a language switcher for i18n app - javascript

I am learning how to implement Lingui(i18n) on apps. Everything is setup, but I wanted to know how I should create a language swticher to change between language catalogs on my app.
This is my index.js file
import React, { useEffect } from "react";
import { render } from "react-dom";
import App from "./App";
import { I18nProvider } from "#lingui/react";
import { i18n } from "#lingui/core";
import { defaultLocale, dynamicActivate } from "./i18n";
const Translation = () => {
useEffect(() => {
dynamicActivate(defaultLocale);
}, []);
return (
<I18nProvider i18n={i18n}>
<App />
</I18nProvider>
);
};
render(<Translation />, document.getElementById("root"));
My App.js file
import "./App.css";
import { Trans } from "#lingui/macro";
function App() {
return (
<div className="App">
<header className="App-header">
<h1>
<Trans>HELLOO</Trans>
</h1>
<p>
<Trans>it's me.</Trans>
</p>
</header>
</div>
);
}
export default App;
and i18n.ts file
import { i18n } from '#lingui/core';
export const locales = {
en: "English",
es: "Spanish",
fr: "French",
};
export const defaultLocale = "fr";
/**
* We do a dynamic import of just the catalog that we need
* #param locale any locale string
*/
export async function dynamicActivate(locale: string) {
const { messages } = await import(`./locales/${locale}/messages`)
i18n.load(locale, messages)
i18n.activate(locale)
}
everytime I specify a es,en or fr defaultLocale the language changes, but I would like to have a language button to change this automatically on the page with select.
ex: "export const defaultLocale = "fr";" (in i18n.ts)

You can use i18n.activate() method to switch to needed locale.
i18n object API is defined in #js-lingui/core.
You also need to load the locale if it was not loaded before.
In case of your project you can use handy dynamicActivate() function you've already created.
Your component output will look like this:
<div>
<Trans>Switch to:</Trans>
<button
onClick={() => dynamicActivate('en')}>
English
</button>
<button
onClick={() => dynamicActivate('fr')}>
Français
</button>
<button
onClick={() => dynamicActivate('es')}>
Espanol
</button>
</div>
It will render 3 buttons [English] [Français] [Espanol] each one will load and activate needed locale.
It is a best practice to keep the button captions in their own languages, so users can find a language they understand.
As an addition to the above it probably makes sense to highlight currently-selected language and disable the button.
I'm using useLingui() to get i18n.locale which indicates current language and set disabled flag on one of the buttons bellow.
Here is the full code of LanguageSelector.js component for you, you can use it in App.js as <LanguageSelector />. Good luck with your project/learnings.
import React from "react"
import { useLingui } from "#lingui/react"
import { Trans } from "#lingui/macro";
import { dynamicActivate } from "./i18n";
const LanguageSelector = () => {
const { i18n } = useLingui();
return <div>
<Trans>Switch to:</Trans>
<button
onClick={() => dynamicActivate('en')}
disabled={i18n.locale === 'en'}>
English
</button>
<button
onClick={() => dynamicActivate('fr')}
disabled={i18n.locale === 'fr'}>
Français
</button>
<button
onClick={() => dynamicActivate('es')}
disabled={i18n.locale === 'es'}>
Espanol
</button>
</div>
};
export default LanguageSelector
UPDATED:
Additionally you can persist selected locale to browser's LocalStorage
We should save locale each time the dynamicActivate() gets called:
const LOCAL_STORAGE_KEY = 'lang';
function dynamicActivate(locale: string) {
// existing code here
window.localStorage.setItem(LOCAL_STORAGE_KEY, locale);
}
Apparently the #lingui/detect-locale library has very good coverage for detecting locale from many sources, including LocalStorage.
Here's how it can be applied here:
import { detect, fromUrl, fromStorage, fromNavigator } from '#lingui/detect-locale';
// existing code from i18n.ts
export const locales = {
en: "English",
es: "Spanish",
fr: "French",
};
export const defaultLocale = "en";
const LOCAL_STORAGE_KEY = 'lang';
// checks that detected locale is available
const isLocaleValid = (locale: string | null) => `${locale}` in locales;
// returns locale
export function getLocale() {
const detectedLocale = detect(
fromUrl("lang"), // for example http://localhost:3000/?lang=es
fromStorage(LOCAL_STORAGE_KEY),
fromNavigator(), // from system settings
() => defaultLocale,
);
return isLocaleValid(detectedLocale) ? detectedLocale : defaultLocale;
}
The last step is to call getLocale() instead of using defaultLocale all the time.
useEffect(() => {
// With this method we dynamically load the catalogs
dynamicActivate(getLocale());
}, []);

Related

How to use Preact signals with astro to share state

Problem
When i change the tag value it only changes on the select component but not in the index.astro
I have folder signals where i export signal
export const tagSignal = signal<string>("all");
I use it like this in Select.tsx component, and here evryting changes
import { tagSignal } from "#signal/*";
const setTagValue = (value: string) => {
tagSignal.value = value;
console.log("select", tagSignal.value);
};
export const Select = () => {
const [display, setDisplay] = useState(false);
const [selectedName, setSelectedName] = useState("all"); // this will be change to only signals still under refator
setTagValue(selectedName);
-------
------
but when I import it to index.astro like this I get only "all" value witch is inital value
---
import { Icon } from "astro-icon";
import { Picture } from "astro-imagetools/components";
import Layout from "#layouts/Layout.astro";
import { Select } from "#components/Select";
import Card from "#components/Card.astro";
import { getCollection } from "astro:content";
import { getProjectsByTag } from "#utils/*";
import { tagSignal } from "#signal/*";
const projects = await getCollection("projects");
const filteredProjects = getProjectsByTag(projects, tagSignal.value);
// TODO: add links
console.log("index", tagSignal.value);
---
/// some code here
<section id="projects" class="projects">
<Select client:only="preact" />
<div class="projects-wrapper">
{
filteredProjects.map(({ data: { title, heroImage } }) => (
<Card name={title} bg_path={heroImage} />
))
}
</div>
</section>
---
I see two issues here.
You are depending on dynamic JS variables in an .astro file. It doesn't work the way you are expecting—all the javascript in .astro files, with the exception of the "islands," e.g., your Select.tsx component, is being evaluated when the page is being built. So Astro grabs the initial value of tagSignal, but makes it a static string.
People can get bitten by, e.g., the trivial ©2010—{new Date().getFullYear()} in the footer – it won't magically update on the new year's eve if used in .astro file.
The state (signal's current value) is not shared accross the islands. If you want to share it, you need either a global state solution (I haven't used it in Astro yet), or just create a common parent for the Select and the filtering logic, e.g.:
{/* the signal will be declared (or imported) in `DynamicSection`*/}
<DynamicSection client:only="preact">
<Select />
<div class="projects-wrapper">
{
filteredProjects.map(({ data: { title, heroImage } }) => (
<Card name={title} bg_path={heroImage} />
))
}
</div>
</ DynamicSection>
(The simplest global state solution would be probably using the url with a query string, and derive the state from its value).

NextJS: Why i18next works only on http://localhost:3000/de (landingpage) but not as http://localhost:3000/de/about

Sorry for being noob but I am really confused on how to work on this. So I followed the instructions on this https://github.com/i18next/next-i18next but confused when it comes to index.js. Whenever I click my toggle switch for /de in my landing page it translates alright in url "http://localhost:3000/de".
But in another page like "About" or in any other page it doesn't translate but the url switch to "http://localhost:3000/de/about". It doesnt go to my 404 error page. But I don't get it why it doesn't translate.
In my index.js if I removed "Service" component which contained all the components of landing page. And replace with other component file like "About" component page it translate alright.
It seems "http://localhost:3000/de" url only works in translation. But in different url path it doesn't. How to do this? Thank you..
Kindly see my code..
My locales path
public/locales/de/common.json
src/pages/_app.js
import nextI18NextConfig from '../../next-i18next.config'
<Component {...pageProps} />
export default appWithTranslation(App, nextI18NextConfig);
src/pages/index.js
import React from 'react';
import Service from 'views/Service';
import i18nextConfig from '../../next-i18next.config';
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
const IndexPage = () => {
return (
<Service/> <— this contains my landing page the only can be translated as “localhost:/3000/de” (src/pages/views/service)
)
};
export async function getServerSideProps({ locale }) {
return { props: {
...(await serverSideTranslations(locale, ['common', 'footer', 'stripe', ‘navbar'], i18nextConfig))
} }
}
export default IndexPage;
in my navbar it is in global component I put my toggle language switcher
src/layouts/Main/components/Navbar/Navbar.js
const onToggleLanguageClick = (locale) => {
const { pathname, asPath, query } = router
router.push({ pathname, query }, asPath, { locale })
}
const changeTo = router.locale === 'de' ? 'en' : 'de'
return (
<button onClick={() => onToggleLanguageClick(changeTo)}>
{t('change-locale', { changeTo })}
</button>
)
this is my next-i18next.config
const path = require('path');
module.exports = {
i18n: {
locales: ['en', 'de'],
defaultLocale: 'en',
localePath: path.resolve('./public/locales')
},
}
my next.config.js
const nextConfig = {
i18n,
…some code
}
module.exports = nextConfig
src/pages/_document.js
import i18nextConfig from '../../next-i18next.config';
export default class MyDocument extends Document {
render() {
const currentLocale = this.props.__NEXT_DATA__.query.locale ?? i18nextConfig.i18n.defaultLocale;
return (
<Html lang={currentLocale}>
.....
First of all, remove the second argument in the appWithTranslation. There is also no need to change the language in the html tag (src/pages/_document.js). i18 does it itself.
public/locales/en ---common.json & /de ---common.json
Wrong. Use the default paths 'public/locales/en/common.json'. Also you can remove the 'localePath' in i18 config file.
reference: https://github.com/i18next/next-i18next
I figured out my problem. Got confused with many of my file paths. Maybe it can help someone.
Add these to your component
export async function getServerSideProps({ locale }) {
....code
}
that doesn't translate your "http://localhost:3000/de/componentname", which in the obvious path: src/pages/componentname.js
... just like my src/pages/index.js above.

React Testing Library: Testing boolean state

I am currently writing a test using the testing-library/react package.
On the second click of my test, the text should not be in the document, but it is.
Here is the component I am testing:
import { useState } from 'react'
import { InfoType } from '../../constants'
type Props = {
type: InfoType
}
const HelpWidget = ({ type }: Props) => {
const [isHover, setHover] = useState<boolean>(false)
return (
<div className="help-widget">
<img
src={require('../../../public/images/info.svg')}
onMouseEnter={() => setHover(true)}
onMouseOut={() => setHover(false)}
onClick={() => setHover(!isHover)}
/>
{isHover ? <div className="info">{type}</div> : <div></div>}
</div>
)
}
export default HelpWidget
Here is the test I wrote:
import React from 'react'
import { screen, render, waitFor } from '#testing-library/react'
import userEvent from '#testing-library/user-event'
import HelpWidget from '../components/helpWidget/helpWidget'
// Testing if the HelpWidget conditionally renders based on the isHover boolean state.
test('helpWidget conditionally renders to the page', () => {
const epk4Post = 'Select a persona to post this'
render(<HelpWidget type={epk4Post} />)
const imageElement = screen.getByRole("img")
userEvent.click(imageElement)
screen.debug()
expect(screen.getByText(/select a persona to post this/i)).toBeInTheDocument()
userEvent.click(imageElement)
screen.debug()
});
And here is what I get when I run screen.debug() to see the DOM output. Here you can clearly see that on the second click, the text is still within the document when it should not be:
Please share your thoughts! Thank you.

How to reuse a const object between pages in React?

I'm using this snippet below to translate my website content. However, there's a way to avoid copying and pasting this code between pages?
import SampleSection from "../section/sample"
import { useRouter } from 'next/router'
import en from '../locales/en'
import pt from '../locales/pt'
export default function Home() {
const router = useRouter()
const { locale } = router
const t = locale === 'en' ? en : pt
const changeLanguage = (e) => {
const locale = e.target.value;
router.push({ pathname, query }, asPath, { locale: locale })
}
return (
<>
<SampleSection
title={t.home.title}
/>
</>
)
}
This is a good opportunity to create your own hook. Basically just extract that logic that you'd need to re-use, stick it in a function and export it. Then you can call it as a hook in as many components as you need to.
More information about creating your own hooks can be found here https://reactjs.org/docs/hooks-custom.html.
Complementing what Justin Formentin said, you'll need to name the hook as useABC where ABC is the name you wanna use.

How to test HTML content with React testing library

Currently I am writing a test to test the content that is inside (HTML content), but it seems I cannot test that properly with React testing library. It can find the id value of that, but how do I get the HTML content inside that element.
import React from 'react';
export const TopBar = () => {
return (
<div className="dashboard-title-component">
<div>
<div data-testid="title-content">Dashboard Menu</div>
</div>
</div>
)
}
import React from "react";
import { render } from "#testing-library/react";
import { TopBar } from "./TopBar";
import { Provider } from "react-redux";
import { store } from "../../Store";
import { screen } from "#testing-library/dom";
import "#testing-library/jest-dom/extend-expect";
test("It should check if content matches", () => {
render(
<Provider store={store}>
<TopBar/>
</Provider>
)
const checkContent = screen.getAllByTestId("title-content");
expect(checkContent.text()).toBe("Dashboard Menu");
});
You're using "#testing-library/jest-dom/extend-expect" which provides a set of custom jest matchers that you can use, fore example you have toHaveTextContent(text: string | RegExp, options?: {normalizeWhitespace: boolean}) that you can use here:
const checkContent = screen.getByTestId("title-content");
expect(checkContent).toHaveTextContent("Dashboard Menu");
It is possible to also test whole HTML node structure this way:
const checkContent = screen.getByTestId("title-content");
expect(checkContent.outerHTML)
.toEqual("<div data-testid=\"title-content\">Dashboard Menu</div>");
This is using standard web API Element.outerHTML
Use getByText
test("It should check if content matches", () => {
const { getByText } = render(<Provider store={store}><TopBar /></Provider>)
expect(getByText(/dashboard menu/i)).toBeTruthy();
});
You can use within to get the text Dashboard Menu. Try this:
test("It should check if content matches", () => {
const { getByTestId } = render(
<Provider store={store}>
<TopBar/>
</Provider>
)
const { getByText } = within(getByTestId('title-content'))
expect(getByText('Dashboard Menu')).toBeInTheDocument()
});

Categories