How to use i18next / react-i18next inside MDX / Markdown files with MDXJS? - reactjs

We use the MDXJS package to write content in Markdown and use React components in it.
Is there a way of using the i18next / react-i18next package inside the MDX / Markdown files?

🌍 Using i18next inside MDX:
When you import an MDX file, you just use it as any other React component:
import { default as SomeContent } from './some-content.mdx';
...
<SomeContent />
Therefore, you can also pass down some props, in this case the t function, and use it inside in some specific ways:
import { default as SomeContent } from './some-content.mdx';
export const SomeComponent: React.FC = React.memo((props) => {
const { t } = useTranslation();
return (
<SomeContent t={ t } someProp="Some value" />
);
});
If you want to check if this is working or see which props are accessible from within your MDX file, add this inside it:
<pre>{ JSON.stringify(props, null, ' ') }</pre>
<pre>{ typeof props.t }</pre>
For the example above, it will display:
{"someProp":"Some value"}
function
Note that you can't use these props inside "raw" MD elements, even if you add a wrapper around them:
### Doesn't work: { props.t('some.translation') }
Doesn't work: { props.t('some.translation') }.
Doesn't work: <>{ props.t('some.translation') }</>.
Doesn't work: <Fragment>{ props.t('some.translation') }</Fragment>.
Doesn't work: <span>{ props.t('some.translation') }</span>.
So you would have to write HTML tags instead:
<h3>Works: { props.t('some.translation') }</h3>
<p>Works: { props.t('some.translation') }.</p>
<p>Works: <>{ props.t('some.translation') }</>.</p>
<p>Works: <Fragment>{ props.t('some.translation') }</Fragment>.</p>
<p>Works: <span>{ props.t('some.translation') }</span>.</p>
🧩 Using MDX in i18next:
If you set returnObjects: true in your i18next config, you can also add MDX components inside your translation files:
import { default as ContentEN } from './content.en.mdx';
import { default as ContentES } from './content.es.mdx';
i18next.use(initReactI18next).init({
resources: {
en: {
translation: {
content: ContentEN,
},
},
es: {
translation: {
content: ContentES,
},
},
},
returnObjects: true,
}));
And then you would use it like this in any of your components (and yes, you can also pass down t or any other prop, just as before:
export const SomeComponent: React.FC = React.memo((props) => {
const { t } = useTranslation();
const Content = t('content');
return (
<Content t={ t } someProp="Some value" />
);
});
🏃 Using i18next inside #mdx-js/runtime:
If you are using #mdx-js/runtime, then you would pass down your props as scope:
import { default as SomeContent } from './some-content.mdx';
export const SomeComponent: React.FC = React.memo((props) => {
const { t } = useTranslation();
return (
<MDX components={ ... } scope={ { t, someProp: 'Some value' } }>{ props.mdx }</MDX>
);
});

Related

Is there a way to split a i18n translation?

OK, so I got this component that animating my titles. But know, I want to translate my application with i18n, but problem is, I was using .split() function to make an array of words of my titles, I know that .split() is taking only string, and all I tested return me a JSX Element. So I can't split my pages title.
Is there another way to do it, to keep my translation ?
Here is an exemple of my pages with the component title and what I tried (I also tried with Translation from react-i18next, but same result)
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation, Trans } from "react-i18next";
const About = () => {
const [letterClass, setLetterClass] = useState<string>('text-animate');
const { t, i18n } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslation = (value: string) => {
return <Trans t={t}>{value}</Trans>;
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslation('About.Title').split("")}
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</Div>
)
}
export default About
Before decide to translate, I was making like that :
<AnimatedLetters
strArray={"About us".split("")}
idx={15}
letterClass={letterClass}
/>
AnimatedLetters.tsx
import { FunctionComponent } from "react"
import { Span } from "./AnimatedLetters.elements"
type Props = {
letterClass: string,
strArray: any[],
idx: number
}
const AnimatedLetters: FunctionComponent<Props> = ({ letterClass, strArray, idx }) => {
return (
<Span>
{
strArray.map((char: string, i: number) => (
<span key={char + i} className={`${letterClass} _${i + idx}`} >
{char}
</span>
))
}
</Span>
)
}
export default AnimatedLetters
OK I found it! I put the solution here in the case of someone else needs it!
In fact there is two ways, don't forget that I needed an array to transmet to my component, so the first was to put directly an array into my translations json files, like:
common.json
{
"Title": ["A","b","o","u","t","","u","s"]
}
But i did not thought that was very clean.
So the second way was to create a method that tooks the key of the json file, to return it directly, like this :
const getTranslate = (value: string) => {
return (`${t(value)}`)
}
Then I stringify it to can use .split() to make an array
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).split("")
}
The translate and the array worked nicely, but it returned with double quotes. The last thing was to erase it, with a replace and a little regex, and now : everything works like a charm 😁
All the component looks like it now :
About.tsx
import { useEffect, useState } from "react";
import AnimatedLetters from "../AnimatedLetters/AnimatedLetters"
import { Div } from "../Layout/Layout.elements";
import { useTranslation } from 'react-i18next';
const About = () => {
const [letterClass, setLetterClass] = useState('text-animate');
const { t } = useTranslation();
useEffect(() => {
setTimeout(() => {
setLetterClass('text-animate-hover')
}, 3000)
}, [])
const getTranslate = (value: string) => {
return JSON.stringify(`${t(value)}`).replace(/\"/g, "").split("")
}
return (
<Div>
<div className="container about-page">
<div className="text-zone">
<h1>
<AnimatedLetters
strArray={getTranslate('Title')} // <<--- Called here
idx={15}
letterClass={letterClass}
/>
</h1>
</div>
</div>
</Div>
)
}
export default About

How to fix typescript error 'name' does not exist in type 'ByRoleOptions' when querying by accessible name using getByRole in react-testing-library

I have a component that renders a list of filters as removable chips which I am trying to test using react-testing-library. I am trying to do query by accessible name as explained here using getByRole.
component:
import Chip from '#material-ui/core/Chip';
import PersonIcon from '#material-ui/icons/Person';
import React from 'react';
import './FilterChips.less';
import { Filters } from './types';
export type FilterChipsProps = {
filters: Filters,
};
export const FilterChips = (props: FilterChipsProps) => {
const { filters } = props;
const people = filters.people
? filters.people.map((person: any) => (
<Chip
icon={<PersonIcon />}
label={`${person.Name} (${person.Id})`}
key={person.Id}
className='chips'
role='filter-chip'
/>
))
: [];
return people.length > 0
? (
<div className='filters'>
<span>Filters: </span>
{people}
</div>
)
:
null;
};
Test:
test('that filters are rendered properly', async () => {
const filters = {
people: [
{ Id: '1', Name: 'Hermione Granger' },
{ Id: '2', Name: 'Albus Dumbledore' },
],
};
const props = { filters };
const { getByRole } = render(<FilterChips {...props} />);
const PersonFilter = getByRole('filter-chip', { name: `${filters.people[0].Name} (${filters.people[0].Id})` });
expect(PersonFilter).toBeDefined();
});
But I am getting a typescript error:
Argument of type '{ name: string; }' is not assignable to parameter of type 'ByRoleOptions'.
Object literal may only specify known properties, and 'name' does not exist in type 'ByRoleOptions'
How do I fix this?
I tried a couple of things to fix this. I imported getByRole directly from #testing-library/dom and deconstructed container from rendered component
const { container } = render(<FilterChips {...props} />);
and then tried to do query by accessible name as following
const PersonFilter = getByRole(container, 'filter-chip', { name: '${filters.people[0].Name} (${filters.people[0].Id})' });
But this is also throwing the same error. Why am I getting this error and how do I fix it?
You can simply ignore ts preceding the problematic line with:
//#ts-ignore
const PersonFilter = getByRole('filter-chip', { name: `${filters.people[0].Name} (${filters.people[0].Id})` });
This will ignore all typescript alerts and treat that next line as if it were plain javascript
following this example in the docs (scroll to the end of the section linked and click on the 'React' tab):
import { render } from '#testing-library/react'
const { getByRole } = render(<MyComponent />)
const dialogContainer = getByRole('dialog')
your code should be:
const { getByRole } = render(<FilterChips {...props} />);
const PersonFilter = getByRole(`${filters.people[0].Name} (${filters.people[0].Id})`);

Pass a JSX element to storybook parameters in a custom build addon

I am building a custom Tab
import React from 'react';
import { addons, types } from '#storybook/addons';
import { AddonPanel } from '#storybook/components';
import { useParameter } from '#storybook/api';
export const ADDON_ID = 'storybook/principles';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = 'principles'; // to communicate from stories
const PanelContent = () => {
const { component: Component } = useParameter(PARAM_KEY, {});
if (!Component) {
return <p>Usage info is missing</p>;
}
return <Component />;
};
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
type: types.Panel,
title: 'Usage',
paramKey: PARAM_KEY,
render: ({ active, key }) => {
return (
<AddonPanel active={active} key={key}>
<PanelContent />
</AddonPanel>
);
},
});
});
& then using it in my stories like
storiesOf('Superman', module)
.addParameters({
component: Superman,
principles: {
component: <Anatomy />
},
})
.add('a story 1', () => <p>some data 1</p>)
.add('a story 2', () => <p>some data 2</p>)
The part where I try to pass in a JSX element like
principles: { component: <Anatomy /> }, // this does not work
principles: { component: 'i can pass in a string' }, // this does work
I get an error like below when I pass in a JSX element as a prop
How can I pass in a JSX element to storybook parameters?
Found a way:
regiter.js
import { deserialize } from 'react-serialize'; //<-- this allows json to jsx conversion
// ...constants definitions
...
const Explanation = () => {
const Explanations = useParameter(PARAM_KEY, null);
const { storyId } = useStorybookState();
const storyKey = storyId.split('--')?.[1];
const ExplanationContent = useMemo(() => {
if (storyKey && Explanations?.[storyKey])
return () => deserialize(JSON.parse(Explanations?.[storyKey]));
return () => <>No extra explanation provided for the selected story</>;
}, [storyKey, Explanations?.[storyKey]]);
return (
<div style={{ margin: 16 }}>
<ExplanationContent />
</div>
);
};
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.TAB,
title: ADDON_TITLE,
route: ({ storyId, refId }) =>
refId
? `/${ADDON_PATH}/${refId}_${storyId}`
: `/${ADDON_PATH}/${storyId}`,
match: ({ viewMode }) => viewMode === ADDON_PATH,
render: ({ active }) => (active ? <Explanation /> : null),
});
});
and when declaring the parameter:
{
parameters:{
component: serialize(<p>Hello world</p>)
}
}

How to use React Ref with Styled Component

I am running React 16.8.6 and Styled Components 4.3.2 currently and am hitting an issue trying to use React.forwardRef.
In Comments.js
import CommentItem from './CommentItem';
class Comments extends Component {
constructor(props) {
super(props);
this.items = [];
}
componentDidMount() {
console.log(this.items);
}
render() {
const { comments } = this.props;
return (
<div>
{comments.length > 0 &&
comments.map((comment, i) => {
this.items[i] = React.createRef();
return (
<CommentItem
ref={this.items[i]}
key={comment.id}
comment={comment}
/>
);
})}
</div>
);
}
}
In CommentItem.js
import styled from "styled-components";
import { Media } from "react-bulma-components";
const CommentMediaWrapper = styled(Media)`
position: relative;
`;
const CommentItem = React.forwardRef(
({ comment }, ref) => {
return (
<CommentMediaWrapper ref={ref}>
<div>...</div>
</CommentMediaWrapper>
);
}
);
export default CommentItem;
In Comments.js will console an array [{ current: null },...]
I don't know how to pass ref in to CommentMediaWrapper. I don't want add new element outer or inner CommentMediaWrapper. Thank for your help.
I think the issue here is more to do with the library element Media from react-bulma-components that you are wrapping. This component exposes a prop named domRef that can be used to pass the DOM reference to the HTML element underneath the wrappers.
const CommentItem = React.forwardRef(
({ comment }, ref) => {
return (
<CommentMediaWrapper domRef={ref}>
<div>...</div>
</CommentMediaWrapper>
);
}
);
Github Reference

Can a React portal be used in a Stateless Functional Component (SFC)?

I have used ReactDOM.createPortal inside the render method of a stateful component like so:
class MyComponent extends Component {
...
render() {
return (
<Wrapper>
{ReactDOM.createPortal(<FOO />, 'dom-location')}
</Wrapper>
)
}
}
... but can it also be used by a stateless (functional) component?
Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.
export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
const [container] = React.useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.createElement(el);
});
React.useEffect(() => {
container.classList.add(className)
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
It can be done like this for a fixed component:
const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')
or, to make the function more flexible, by passing a component prop:
const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')
can it also be used by a stateless (functional) component
?
yes.
const Modal = (props) => {
const modalRoot = document.getElementById('myEle');
return ReactDOM.createPortal(props.children, modalRoot,);
}
Inside render :
render() {
const modal = this.state.showModal ? (
<Modal>
<Hello/>
</Modal>
) : null;
return (
<div>
<div id="myEle">
</div>
</div>
);
}
Working codesandbox#demo
TSX version based on #Samuel's answer (React 17, TS 4.1):
// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
interface IProps {
className? : string
el? : string
children : React.ReactNode
}
/**
* React portal based on https://stackoverflow.com/a/59154364
* #param children Child elements
* #param className CSS classname
* #param el HTML element to create. default: div
*/
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
const [container] = React.useState(document.createElement(el))
if ( className )
container.classList.add(className)
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
export default Portal
IMPORTANT useRef/useState to prevent bugs
It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render
//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;
//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
const el = useRef(document.createElement("div"));
useEffect(() => {
modalRoot.appendChild(el.current);
return () => {
modalRoot.removeChild(el.current);
};
}, []);
return ReactDOM.createPortal(
<div
onClick={onOutSideClick}
ref={overlayRef}
className={classes.overlay}
>
<div ref={imageRowRef} className={classes.fullScreenImageRow}>
{renderImages()}
</div>
<button onClick={onClose} className={classes.closeButton}>
<Image width={25} height={25} src="/app/close-white.svg" />
</button>
</div>,
el.current
);
Yes, according to docs the main requirements are:
The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.
In case of stateless component you can pass element via props and render it via portal.
Hope it will helps.
Portal with SSR (NextJS)
If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.
The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.
If it can't find the selector then it will create and attach a div.
NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.
import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface IPortal {
selector?: string;
}
const Portal = (props: PropsWithChildren<IPortal>) => {
props = {
selector: '__ROOT_PORTAL__',
...props
};
const { selector, children } = props;
const ref = useRef<Element>()
const [mounted, setMounted] = useState(false);
const selectorPrefixed = '#' + selector.replace(/^#/, '');
useEffect(() => {
ref.current = document.querySelector(selectorPrefixed);
if (!ref.current) {
const div = document.createElement('div');
div.setAttribute('id', selector);
document.body.appendChild(div);
ref.current = div;
}
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
};
export default Portal;
Usage
The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)
import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above
const modalStyle: CSSProperties = {
padding: '3rem',
backgroundColor: '#eee',
margin: '0 auto',
width: 400
};
const Home = () => {
const [visible, setVisible] = useState(false);
return (
<>
<p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
<Portal>
{visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
</Portal>
</>
);
};
export default Home;
const X = ({ children }) => ReactDOM.createPortal(children, 'dom-location')
Sharing my solution:
// PortalWrapperModal.js
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const PortalWrapperModal = ({
children,
onHide,
backdrop = 'static',
focus = true,
keyboard = false,
}) => {
const portalRef = useRef(null);
const handleClose = (e) => {
if (e) e.preventDefault();
if (portalRef.current) $(portalRef.current).modal('hide');
};
useEffect(() => {
if (portalRef.current) {
$(portalRef.current).modal({ backdrop, focus, keyboard });
$(portalRef.current).modal('show');
$(portalRef.current).on('hidden.bs.modal', onHide);
}
}, [onHide, backdrop, focus, keyboard]);
return ReactDOM.createPortal(
<>{children(portalRef, handleClose)}</>,
document.getElementById('modal-root')
);
};
export { PortalWrapperModal };

Resources