DEV Community

Cover image for I18next translations inside maps & React dynamic import
Tomek Poniatowicz for GraphQL Editor

Posted on

I18next translations inside maps & React dynamic import

Recently while browsing through one of our projects I found some atrocious piece of code I wrote a few months back. As ugly as it looks I think it can serve as a decent example of how much you can progress in a short time, even as a ‘copywriter-turned-not-really-a-dev’ like myself, and also how much you can do wrong in just one simple component.

While the code here serves as an example for the sake of the blogpost, it is an actual piece of my code from one of our websites. It's a simple component with four tiles with an image, some text and icons that illustrate the features of one of our applications. In case you're wondering why fixing it matters, I can tell you that if there's one thing that impacts SEO - it's poorly written code. It can worsen performance causing our website to work too slow and incur something Google calls Cumulative Layout Shift. It's basically a measurement of how much crawlers see your website's layout change as components or elements are loading in, which may happen if your code is bloated. Google considers that bad for user experience and can penalize your website's positioning for it. Aside from that obviously fixing bad code just for the sake of it is reason enough (especially if you don't want to get yelled at during code review). So here's the code for the component:

import { useTranslation } from 'next-i18next';
import { AutomationIcon, FastIcon, ProcessorIcon, AccurateIcon } from '@/assets';

export const FeaturesSection = () => {
    const { t } = useTranslation('landing');
    return (
        <FeatureSectionWrapper>
            <FeatureText>
                <h2 className="header" dangerouslySetInnerHTML={{ __html: t('features.header') }} />
                <div className="desc">{t('features.description')}</div>
                <ul>
                    <li>
                        <div>
                            <AccurateIcon />
                            {t('features.features.accurate')}
                        </div>
                        <p>{t('features.features.accurateText')}</p>
                    </li>
                    <li>
                        <div>
                            <FastIcon />
                            {t('features.features.fast')}
                        </div>
                        <p>{t('features.features.fastText')}</p>
                    </li>
                    <li>
                        <div>
                            <ProcessorIcon />
                            {t('features.features.ai')}
                        </div>
                        <p>{t('features.features.aiText')}</p>
                    </li>
                    <li>
                        <div>
                            <AutomationIcon />
                            {t('features.features.automated')}
                        </div>
                        <p>{t('features.features.automatedText')}</p>
                    </li>
                </ul>
            </FeatureText>
            <FeatureImg
                src="/images/results.png"
                alt="feature image"
            />
        </FeatureSectionWrapper>
    );
};
Enter fullscreen mode Exit fullscreen mode

As you can see even though I had some basic understanding of html and the useTranslation hook, but I made a mess of it because I didn't do any mapping and just wrote out every segment of the component as a list item. The component is rather simple, it has two columns - one is an image and the other has a header, description and four smaller tiles with titles, descriptions and icons. First we need to properly map the translated text and for that we have to restructure the json file with the translations:

"featuresSection": {
        "header": "lorem ipsum header",
        "description": "lorem ipsum desc",
        "features": {
            "fast": {
                "name": "Fast",
                "text": "lorem ipsum dolor",
                "icon": "FastIcon"
            },
            "accurate": {
                "name": "Accurate",
                "text": "lorem ipsum dolor",
                "icon": "AccurateIcon"
            },
            "ai": {
                "name": "AI powered",
                "text": "lorem ipsum dolor",
                "icon": "ProcessorIcon"
            },
            "automated": {
                "name": "Fully automated",
                "text": "lorem ipsum dolor",
                "icon": "AutomationIcon"
            }
        }
    },
Enter fullscreen mode Exit fullscreen mode

Now that we have everything neatly organized in our json files it's time to map it using the i18next useTranslation hook. Lets use a const for the section of the landing json file we're using so that we can then access it via just curly braces like in the h2 and desc div here:

import { useTranslation } from 'next-i18next';

const { t } = useTranslation('landing');
const featSection = t('featuresSection');
<h2 className="header" dangerouslySetInnerHTML={{ __html: featSection.header }} />
<div className="desc">{featSection.description}</div>
Enter fullscreen mode Exit fullscreen mode

Alternatively you can use the built-in i18next attribute KeyPrefix. I think this works the same way regardless (unless there's some key difference I’m unaware of) but I just like using the const as that way I can access every translation string by just calling the const name in curly braces {featSection.whatever} instead of doing {t(‘whatever’)} but that’s just personal preference and you can do it like this as well:

const { t } = useTranslation('landing', {keyPrefix: 'featuresSection'});
<h2 className="header" dangerouslySetInnerHTML={{ __html: t('header') }} />
<div className="desc">{t('description')}</div>
Enter fullscreen mode Exit fullscreen mode

With that done let's change the html code for the list items. We can simplify it since all the list items are structured the same way:

<li key={key}>
  <div>
    <IconComponent />
    {name}
  </div>
  <p>{text}</p>
</li>
Enter fullscreen mode Exit fullscreen mode

Now for the whole list we still need to map these elements, at first I went with this:

const iconComponents = {
        AutomationIcon,
        FastIcon,
        ProcessorIcon,
        AccurateIcon,
};
<ul>
    {Object.keys(featSection.features).map((key) => {
        const { icon, name, text } =
            featSection.features[key as keyof typeof featSection.features]; const
                IconComponent = iconComponents[icon as keyof typeof iconComponents];
        return (
            <li key="{key}">
                <div>
                    <IconComponent />
                    {name}
                </div>
                <p>{text}</p>
            </li>
        );
    })}
</ul>
Enter fullscreen mode Exit fullscreen mode

While that worked it required an extra const and a somewhat hacky workaround for a TypeScript type error via as keyof typeof and pointing it to featSection.features. We can definitely do better! In general there’s three methods of mapping objects: keys, values and entries (which contain both keys and values). As you can see at first I went with keys, but using values makes more sense since that way TypeScript can infer the type values and we can just use idx which will also let it identify the key values. That approach lets us get rid of the const and simplify the code again:

const iconComponents = {
        AutomationIcon,
        FastIcon,
        ProcessorIcon,
        AccurateIcon,
};
<ul>
    {Object.values(featSection.features).map(({ icon, name, text }, idx) => {
        const IconComponent = iconComponents[icon as keyof typeof iconComponents];
        return (
            <li key={idx}>
                <div>
                    <IconComponent />
                    {name}
                </div>
                <p>{text}</p>
            </li>
        );
    })}
</ul>
Enter fullscreen mode Exit fullscreen mode

That's nice and simple but as you can see I'm still defining the icon types and values via an extra const and the hacky as keyof typeof workaround. To get rid of that we can use React's dynamic imports. Since the json file already defines what icon should be in each component we can use that to import the icons dynamically. So now instead of using import for each icon individually we can import them dynamically by using this:

import * as Icons from '@/assets';
<ul>
    {Object.values(featSection.features).map(({ icon, name, text }, idx) => {
        return (
            <li key={idx}>
                <div>
                    {Icons[icon as keyof typeof Icons]()}
                    {name}
                </div>
                <p>{text}</p>
            </li>
        );
    })}
</ul>
Enter fullscreen mode Exit fullscreen mode

Now we can even use different icons for different language versions if we want to, by simply specifying their names in that language json file. Let's look at the whole component:

import { useTranslation } from 'next-i18next';
import * as Icons from '@/assets';

export const FeaturesSection = () => {
    const { t } = useTranslation('landing');
    return (
        <FeatureSectionWrapper>
            <FeatureText>
                <h2 className="header" dangerouslySetInnerHTML={{ __html: t('features.header') }} />
                <div className="desc">{t('features.description')}</div>
                <ul>
                    {Object.values(featSection.features).map(({ icon, name, text }, idx) => {
                        return (
                            <li key={idx}>
                                <div>
                                    {Icons[icon as keyof typeof Icons]()}
                                    {name}
                                </div>
                                <p>{text}</p>
                            </li>
                        );
                    })}
                </ul>
            </FeatureText>
            <FeatureImg
                src="/images/results.png"
                alt="feature image"
            />
        </FeatureSectionWrapper>
    );
};
Enter fullscreen mode Exit fullscreen mode

That's a lot cleaner and less painful to look at, and it only took a couple simple changes. Obviously it's a reach to call this a refactor or really anything but a small fix, but going over earlier mistakes makes things easier to remember (at least for me). It's also a small step towards writing cleaner and simpler code which is a good thing to keep in mind. It's also the best route to take especially in regards to SEO where better safe than sorry is the only correct approach. You can be sure that it's a whole lot easier than figuring out which piece of code on your website is causing the dreaded layout shifts Google has flagged you for.

Top comments (0)