Build a Multilingual Website using Gatsby and Contentstack

Gatsby is a blazing-fast static site generator. This example website is built using the contentstack-gatsby plugin and Contentstack. It uses Contentstack to store and deliver the content of the website.

This website also supports multilingual functionality to view content in different languages. This guide further explains the process of adding a new language and configuring the code as per your requirements.

Prerequisites

  • The Gatsby Starter app already installed and working
  • Note Live preview is not supported with the multi-lingual Gatsby app.

Enable Multilingual in your Gatsby Starter

Note Also, translate those entries into French to differentiate the results.

App Changes

Follow the steps to configure the app for multi-lingual support:

  1. Follow the steps below to create a file that maintains a list of locales.
    1. Create a folder named Locales within the src folder.
    2. Create a file named locales.ts within the Locales folder.
    3. Copy and paste the below code to the file in the path:
      src/Locales/locales.ts

      Note In our example, we have used English and French as our locales.

    4. Code Snippet:
      module.exports = {
      
        "en-us": {
      
          code: "en-us",
      
          locale: "English",
      
          defaultLocale: true,
      
        },
      
        "fr": {
      
          code: "fr",
      
          locale: "french",
      
        },
      
      }
  2. Copy and paste the below code to the gatsby-node.ts file to create pages programmatically.

    Code Snippet:

    const path = require("path")
    
    const locales = require("./src/Locales/locales")
    
    
    module.exports.createPages = async ({ graphql, actions }) > {
    
      const { createPage } = actions
    
      const blogPostTemplate = path.resolve("src/templates/blog-post.tsx")
    
      const pageTemplate = path.resolve("src/templates/page.tsx")
    
      const blogPageTemplate = path.resolve("src/templates/blog-page.tsx")
    
      const blogPostQuery = await graphql(`
    
        query {
    
          allContentstackBlogPost {
    
            nodes {
    
              title
    
              url
    
              locale
    
              publish_details {
    
                locale
    
              }
    
            }
    
          }
    
        }
    
      `);
    
    
      const pageQuery = await graphql(`
    
          query {
    
            allContentstackPage {
    
              nodes {
    
                title
    
                url
    
                locale
    
                publish_details {
    
                  locale
    
                }
    
              }
    
            }
    
          }
    
      `);
    
    
      const createBlogPostTemplate = (route, componentToRender, title, data, locale) => {
    
        createPage({
    
          path: `${route}`,
    
          component: componentToRender,
    
          context: {
    
            title: title, 
    
            result: data,
    
            locale: locale
    
          },
    
        });
    
      };
    
    
      const createBlogPageTemplate = (route, componentToRender, title, data, locale) => {
    
        createPage({
    
          path: `${route}`,
    
          component: componentToRender,
    
          context: {
    
            title: title, 
    
            result: data,
    
            locale: locale
    
          },
    
        });
    
      };
    
    
      const createPageTemplate = (route, componentToRender, url, data, locale) => {
    
        createPage({
    
          path: `${route}`,
    
          component: componentToRender,
    
          context: {
    
            url: url,
    
            result: data,
    
            locale: locale
    
          },
    
        });
    
      };
    
    
    
    /**
    
     * Blog page template for route: "/blog" and its locales
    
     */
    
      pageQuery.data.allContentstackPage.nodes.forEach(node => {
    
        const isDefault = locales[node.locale];
    
        const localeList = Object.values(locales)
    
          .filter(locale => !locale.defaultLocale)
    
          .map(locale => locale.code);
    
    
        if (node.url === "/blog") {
    
          if (isDefault.defaultLocale) {
    
            createBlogPageTemplate(node.url, blogPageTemplate, node.title, node,node.locale);
    
          } else {
    
            localeList.forEach(code => {
    
              const localeUrl = `/${code}${node.url}`;
    
              createBlogPageTemplate(localeUrl, blogPageTemplate, node.title, node,node.locale);
    
            });
    
          }
    
        }
    
      });
    
    
    /**
    
     * Page template for route: "/", "/about-us" etc and its locales
    
     */
    
      pageQuery.data.allContentstackPage.nodes.forEach(node => {  
    
        const isDefault = locales[node.locale];
    
        const localeList = Object.values(locales)
    
        .filter(locale => !locale.defaultLocale)
    
        .map(locale => locale.code);
    
        if (node.url !== '/blog') {
    
          if (isDefault.defaultLocale) {
    
            createPageTemplate(`${node.url}`, pageTemplate, node.url, node, node.locale);
    
          } else {
    
            localeList.forEach(code => {
    
              const localeUrl = `/${code}${node.url}`;
    
              createPageTemplate(localeUrl, pageTemplate, node.url, node, code);
    
            });
    
          }
    
        }
    
      }); 
    
    
    /**
    
     * Blog post template for route: "/blog/blog-post" and its locales
    
     */
    
      blogPostQuery.data.allContentstackBlogPost.nodes.forEach(node => {
    
        const isDefault = locales[node.locale];
    
          const localeList = Object.values(locales)
    
          .filter(locale => !locale.defaultLocale)
    
          .map(locale => locale.code);
    
    
        if (isDefault.defaultLocale) {
    
          createBlogPostTemplate(node.url, blogPostTemplate, node.title, node,node.locale);
    
        } else {
    
          localeList.forEach(code => {
    
            const localeUrl = `/${code}${node.url}`;
    
            createBlogPostTemplate(localeUrl, blogPostTemplate, node.title, node,node.locale);
    
          });
    
        }
    
      });
    
    };
  3. Follow the steps below to create a Context provider which handles the multi-lingual changes.
    1. Create a folder named hooks within the src folder.
    2. Create a file named useLocale.tsx within the hooks folder.
    3. Copy and paste the below code to the file in the path:
      src/hooks/useLocale.tsx Code Snippet:
      import React, { createContext, useState, useContext } from "react";
      
      
      import allLocales from "../Locales/locales";
      
      
      const LocaleContext = createContext("");
      
      
      const isBrowser = typeof window !== "undefined";
      
      let pathname: String;
      
      
      const LocaleProvider = ({ children }: any) => {
      
        if (isBrowser) pathname = window?.location?.pathname;
      
      
        // Find a default language
      
        const defaultLang = Object.keys(allLocales).filter(
      
          lang => allLocales[lang].defaultLocale
      
        )[0];
      
      
        // Get language prefix from the URL
      
        const urlLang = pathname?.split("/")[1];
      
      
        // Search if locale matches defined, if not set 'en-us' as default
      
        const currentLang = Object.keys(allLocales)
      
          .map(lang => allLocales[lang].code)
      
          .includes(urlLang)
      
          ? urlLang
      
          : defaultLang;
      
      
        const [locale, setLocale] = useState(currentLang);
      
        const [defaultLocale, setDefaultLocale] = useState(defaultLang);
      
        const [allLocalesList] = useState(allLocales);
      
      
        const changeLocale = (lang: string) => {
      
          if (lang) {
      
            setLocale(lang);
      
          }
      
        };
      
      
        /**
      
         * Wrapped context provider to send values below DOM tree
      
         */
      
        return (
      
          <LocaleContext.Provider
      
            value={{ defaultLocale, allLocalesList, locale, changeLocale }}
      
          >
      
            {children}
      
          </LocaleContext.Provider>
      
        );
      
      };
      
      
      const useLocale = () => {
      
        const context = useContext(LocaleContext);
      
      
        if (!context) {
      
          throw new Error("useLocale must be used within an LocaleProvider");
      
        }
      
      
        return context;
      
      };
      
      
      export { LocaleProvider, useLocale };
  4. For the context provider to work and the hook to receive values from the context, wrap the context provider (copy and paste the below code snippet) in the file wrap-with-provider.js, present in the root directory.

    Code Snippet:

    import React from "react"
    
    import { Provider } from "react-redux"
    
    import { LocaleProvider } from "./src/hooks/useLocale"
    
    
    import createStore from "./src/store/reducers/state.reducer"
    
    
    export default ({ element }) => {
    
      const store = createStore()
    
      return (
    
        <LocaleProvider>
    
          <Provider store={store}>{element}</Provider>
    
        </LocaleProvider>
    
      )
    
    }
  5. Add jsonRteToHtml: true flag under options in the gatsby-config.ts file for the gatsby-source-contentstack plugin if the flag is not already present.

    Code Snippet:

    {
    
          resolve: "gatsby-source-contentstack",
    
          options: {
    
            api_key: CONTENTSTACK_API_KEY,
    
            delivery_token: CONTENTSTACK_DELIVERY_TOKEN,
    
            environment: CONTENTSTACK_ENVIRONMENT,
    
            cdn: `https://${cdnHost}/v3`,
    
            // Optional: expediteBuild set this to either true or false
    
            expediteBuild: true,
    
            // Optional: Specify true if you want to generate custom schema
    
            enableSchemaGeneration: true,
    
            // Optional: Specify a different prefix for types. This is useful in cases where you have multiple instances of the plugin to be connected to different stacks.
    
            type_prefix: "Contentstack", // (default),
    
            jsonRteToHtml: true ,
    
          },
    
        },
  6. Follow the steps below to create a custom link component that would prepend locale code to every route created except the default locale or master locale.

    Note en-us is a default locale. Thus there will be no prefix to the locale code.

    1. Navigate to the components folder within src and create a file named CustomLink.tsx
    2. Copy and paste the below code snippet to the created file:
      import React, { useEffect } from "react"
      
      import { Link, navigate } from "gatsby"
      
      import { useLocale } from "../hooks/useLocale"
      
      
      const CustomLink = ({ to, ...props }: any) => {
      
        const { defaultLocale, locale }: any = useLocale()
      
      
        return (
      
          <Link
      
            to={defaultLocale !== locale ? `/${locale + to}` : `${to}`}
      
            {...props}
      
          />
      
        )
      
      }
      
      
      export { CustomLink }
  7. Open Command Prompt and install the npm package react-dropdown, as given below:
    npm install react-dropdown --legacy-peer-deps
  8. To add the dropdown in the Header for the locale/language switcher, navigate to the Header component and replace it with the below code snippet:
    import React, { useState, useEffect } from "react";
    
    import { useLocation } from "@reach/router";
    
    import { graphql, useStaticQuery, navigate } from "gatsby";
    
    import Dropdown from "react-dropdown";
    
    import { CustomLink } from "./CustomLink";
    
    import parse from "html-react-parser";
    
    import { connect } from "react-redux";
    
    import Tooltip from "./ToolTip";
    
    import jsonIcon from "../images/json.svg";
    
    import { getHeaderRes, jsonToHtmlParse, getAllEntries } from "../helper/index";
    
    import { onEntryChange } from "../live-preview-sdk";
    
    import { actionHeader } from "../store/actions/state.action";
    
    import { DispatchData, Entry, HeaderProps, Menu } from "../typescript/layout";
    
    import { useLocale } from "../hooks/useLocale";
    
    
    const queryHeader = () => {
    
      const query = graphql`
    
        query {
    
          allContentstackHeader {
    
            nodes {
    
              title
    
              uid
    
              locale
    
              publish_details {
    
                locale
    
              }
    
              logo {
    
                uid
    
                url
    
                filename
    
              }
    
              navigation_menu {
    
                label
    
                page_reference {
    
                  title
    
                  url
    
                  uid
    
                }
    
              }
    
              notification_bar {
    
                show_announcement
    
                announcement_text
    
              }
    
            }
    
          }
    
        }
    
      `;
    
      return useStaticQuery(query);
    
    };
    
    
    const Header = ({ dispatch }: DispatchData) => {
    
      const { pathname } = useLocation();
    
      const { allContentstackHeader } = queryHeader();
    
      const { locale, allLocalesList, changeLocale, defaultLocale }: any =
    
        useLocale();
    
      let renderValue;
    
    
      allContentstackHeader?.nodes.map((value, idx) => {
    
        if (value?.locale === locale) {
    
          renderValue = value;
    
          jsonToHtmlParse(value);
    
          dispatch(actionHeader(value));
    
        }
    
      });
    
      const [getHeader, setHeader] = useState(allContentstackHeader);
    
    
      function buildNavigation(ent: Entry, head: HeaderProps) {
    
        let newHeader = { ...head };
    
        if (ent.length !== newHeader.navigation_menu.length) {
    
          ent.forEach(entry => {
    
            const hFound = newHeader?.navigation_menu.find(
    
              navLink => navLink.label === entry.title
    
            );
    
            if (!hFound) {
    
              newHeader.navigation_menu?.push({
    
                label: entry.title,
    
                page_reference: [
    
                  { title: entry.title, url: entry.url, $: entry.$ },
    
                ],
    
                $: {},
    
              });
    
            }
    
          });
    
        }
    
        return newHeader;
    
      }
    
    
      async function getHeaderData() {
    
        const headerRes = await getHeaderRes(locale);
    
        const allEntries = await getAllEntries(locale);
    
        const nHeader = buildNavigation(allEntries, headerRes);
    
        setHeader(nHeader);
    
      }
    
    
      async function sanitizedUrl(localeCode: string) {
    
        let urlToNavigate: string;
    
        if (localeCode !== defaultLocale) {
    
          return navigate(`/${localeCode + pathname}`);
    
        } else if (localeCode === defaultLocale) {
    
          if (pathname.split("/").includes(locale)) {
    
            urlToNavigate = pathname.replace(`/${locale}`, "");
    
            return navigate(urlToNavigate);
    
          }
    
        }
    
      }
    
    
      useEffect(() => {
    
        onEntryChange(() => getHeaderData());
    
      }, [locale]);
    
    
      return (
    
        <header className="header">
    
          <div className="note-div">
    
            {renderValue.notification_bar.show_announcement &&
    
              typeof renderValue.notification_bar.announcement_text === "string" &&
    
              parse(renderValue.notification_bar.announcement_text)}
    
          </div>
    
          <div className="max-width header-div">
    
            <div className="wrapper-logo">
    
              <CustomLink to="/" className="logo-tag" title="Contentstack">
    
                <img
    
                  className="logo"
    
                  src={renderValue.logo?.url}
    
                  alt={renderValue.title}
    
                  title={renderValue.title}
    
                />
    
              </CustomLink>
    
            </div>
    
            <input className="menu-btn" type="checkbox" id="menu-btn" />
    
            <label className="menu-icon" htmlFor="menu-btn">
    
              <span className="navicon"></span>
    
            </label>
    
    
            <nav className="menu">
    
              <ul className="nav-ul header-ul">
    
                {renderValue.navigation_menu.map((menu: Menu, index: number) => {
    
                  return (
    
                    <li className="nav-li" key={index}>
    
                      {menu.label === "Home" ? (
    
                        <CustomLink
    
                          to={`${menu.page_reference[0]?.url}`}
    
                          activeClassName="active"
    
                        >
    
                          {menu.label}
    
                        </CustomLink>
    
                      ) : (
    
                        <CustomLink
    
                          to={`${menu.page_reference[0]?.url}`}
    
                          activeClassName="active"
    
                        >
    
                          {menu.label}
    
                        </CustomLink>
    
                      )}
    
                    </li>
    
                  );
    
                })}
    
              </ul>
    
            </nav>
    
            {locale && (
    
              <Dropdown
    
                options={Object.keys(allLocalesList)}
    
                onChange={option => {
    
                  const { value } = option;
    
                  changeLocale(value);
    
                  sanitizedUrl(value);
    
                }}
    
                value={locale}
    
                placeholder="Select an option"
    
              />
    
            )}
    
            <div className="json-preview">
    
              <Tooltip
    
                content="JSON Preview"
    
                direction="top"
    
                dynamic={false}
    
                delay={200}
    
                status={0}
    
              >
    
                <span data-bs-toggle="modal" data-bs-target="#staticBackdrop">
    
                  <img src={jsonIcon} alt="JSON Preview icon" />
    
                </span>
    
              </Tooltip>
    
            </div>
    
          </div>
    
        </header>
    
      );
    
    };
    
    
    export default connect()(Header);
  9. Navigate to the Footer component and replace it with the below code snippet:
    import { Link, useStaticQuery, graphql } from "gatsby";
    
    import React, { useState, useEffect } from "react";
    
    import { CustomLink } from "./CustomLink";
    
    import parser from "html-react-parser";
    
    import { connect } from "react-redux";
    
    import { actionFooter } from "../store/actions/state.action";
    
    import { onEntryChange } from "../live-preview-sdk";
    
    import { getFooterRes, getAllEntries, jsonToHtmlParse } from "../helper/index";
    
    import {
    
      DispatchData,
    
      Entry,
    
      FooterProps,
    
      Links,
    
      Social,
    
      Menu,
    
    } from "../typescript/layout";
    
    import { useLocale } from "../hooks/useLocale";
    
    
    const queryLayout = () => {
    
      const data = useStaticQuery(graphql`
    
        query {
    
          allContentstackFooter {
    
            nodes {
    
              title
    
              locale
    
              logo {
    
                url
    
              }
    
              navigation {
    
                link {
    
                  href
    
                  title
    
                }
    
              }
    
              social {
    
                social_share {
    
                  link {
    
                    href
    
                    title
    
                  }
    
                  icon {
    
                    url
    
                  }
    
                }
    
              }
    
              copyright
    
            }
    
          }
    
        }
    
      `);
    
      return data;
    
    };
    
    
    const Footer = ({ dispatch }: DispatchData) => {
    
      const { allContentstackFooter } = queryLayout();
    
      const { locale } = useLocale();
    
      let renderValue;
    
      allContentstackFooter?.nodes.map((value, idx) => {
    
        if (value?.locale === locale) {
    
          renderValue = value;
    
          jsonToHtmlParse(value);
    
          dispatch(actionFooter(value));
    
        }
    
      });
    
    
      const [getFooter, setFooter] = useState(allContentstackFooter);
    
    
      function buildNavigation(ent: Entry, footer: FooterProps) {
    
        let newFooter = { ...footer };
    
        if (ent.length !== newFooter.navigation.link.length) {
    
          ent.forEach(entry => {
    
            const fFound = newFooter?.navigation.link.find(
    
              (nlink: Links) => nlink.title === entry.title
    
            );
    
            if (!fFound) {
    
              newFooter.navigation.link?.push({
    
                title: entry.title,
    
                href: entry.url,
    
                $: entry.$,
    
              });
    
            }
    
          });
    
        }
    
        return newFooter;
    
      }
    
    
      async function getFooterData() {
    
        const footerRes = await getFooterRes(locale);
    
        const allEntries = await getAllEntries(locale);
    
        const nFooter = buildNavigation(allEntries, footerRes);
    
        setFooter(nFooter);
    
      }
    
    
      useEffect(() => {
    
        onEntryChange(() => getFooterData());
    
      }, [locale]);
    
    
      return (
    
        <footer>
    
          <div className="max-width footer-div">
    
            <div className="col-quarter">
    
              <CustomLink to="/" className="logo-tag">
    
                <img
    
                  src={renderValue.logo?.url}
    
                  alt={renderValue.title}
    
                  title={renderValue.title}
    
                  className="logo footer-logo"
    
                />
    
              </CustomLink>
    
            </div>
    
            <div className="col-half">
    
              <nav>
    
                <ul className="nav-ul">
    
                  {renderValue.navigation.link.map((menu: Menu, index: number) => {
    
                    return (
    
                      <li className="footer-nav-li" key={index} {...menu.$?.title}>
    
                        <CustomLink to={menu.href}>{menu.title}</CustomLink>
    
                      </li>
    
                    );
    
                  })}
    
                </ul>
    
              </nav>
    
            </div>
    
            <div className="col-quarter social-link">
    
              <div className="social-nav">
    
                {renderValue.social.social_share.map(
    
                  (social: Social, index: number) => {
    
                    return (
    
                      <a
    
                        href={social.link?.href}
    
                        title={social.link.title.toLowerCase()}
    
                        key={index}
    
                        className="footer-social-links"
    
                      >
    
                        <img src={social.icon?.url} alt="social-icon" />
    
                      </a>
    
                    );
    
                  }
    
                )}
    
              </div>
    
            </div>
    
          </div>
    
          <div className="copyright">
    
            {typeof renderValue.copyright === "string" ? (
    
              <div>{parser(renderValue?.copyright)}</div>
    
            ) : (
    
              ""
    
            )}
    
          </div>
    
        </footer>
    
      );
    
    };
    
    
    export default connect()(Footer);
  10. Now replace all the default Gatsby Link components with our CustomLink component in all the components where we have used Link.

    Example

    1. Here is a Gatsby Link component in the Header component:
      <Link to="/" className="logo-tag" title="Contentstack">
      
                  <img className="logo" src={renderValue.logo?.url} alt={renderValue.title} title={renderValue.title} />
      
      </Link>
    2. Replace it by importing the CustomLink component:
      import { CustomLink } from "./CustomLink"
      
      <CustomLink to="/" className="logo-tag" title="Contentstack">
      
          <img
      
          className="logo"
      
          src={renderValue.logo?.url}
      
          alt={renderValue.title}
      
          title={renderValue.title}
      
          />
      
      </CustomLink>

    You can see the Header component code snippet in step no.8.

    To achieve a multi-lingual Gatsby site without any plugins, you must make a few changes in page creation using templates. Previously, we had home and blog page routes within the src/pages folder which were not created dynamically. This created conflicts when our route/URL with a locale-specific page was created. So to mitigate that, we have deleted the home and blog pages from the src/pages folder. After deleting, the pages folder would look like this:

    Click to enlarge

    Now it would just have a 404.tsx route.

  11. Create a file named blog-page.tsx inside the src/templates folder as given below: 
    Click to enlarge
  12. Copy and paste the below code snippet to the blog-page.tsx file:
    import React, { useState, useEffect } from "react";
    
    import { graphql } from "gatsby";
    
    import Layout from "../components/Layout";
    
    import SEO from "../components/SEO";
    
    import RenderComponents from "../components/RenderComponents";
    
    import ArchiveRelative from "../components/ArchiveRelative";
    
    import { onEntryChange } from "../live-preview-sdk/index";
    
    import { getPageRes, getBlogListRes, jsonToHtmlParse } from "../helper/index";
    
    import { PageProps } from "../typescript/template";
    
    import BlogList from "../components/BlogList";
    
    import { useLocale } from "../hooks/useLocale";
    
    
    const Blog = ({
    
      data: { allContentstackBlogPost, contentstackPage },
    
    }: PageProps) => {
    
      jsonToHtmlParse(allContentstackBlogPost.nodes);
    
      const [getEntry, setEntry] = useState({
    
        banner: contentstackPage,
    
        blogList: allContentstackBlogPost.nodes,
    
      });
    
      const { locale }: any = useLocale();
    
    
      async function fetchData() {
    
        try {
    
          const banner = await getPageRes("/blog", locale);
    
          const blogList = await getBlogListRes(locale);
    
          if (!banner || !blogList) throw new Error("Error 404");
    
          setEntry({ banner, blogList });
    
        } catch (error) {
    
          console.error(error);
    
        }
    
      }
    
    
      useEffect(() => {
    
        onEntryChange(() => fetchData());
    
      }, [contentstackPage, locale]);
    
    
      const newBlogList = [] as any;
    
      const newArchivedList = [] as any;
    
      getEntry.blogList?.forEach(post => {
    
        if (locale === post.locale) {
    
          if (post.is_archived) {
    
            newArchivedList.push(post)
    
          } else {
    
            newBlogList.push(post)
    
          }
    
        }
    
      });
    
      return (
    
        <Layout blogPost={getEntry.blogList} banner={getEntry.banner}>
    
          <SEO title={getEntry.banner.title} />
    
          <RenderComponents
    
            components={getEntry.banner.page_components}
    
            blogPage
    
            contentTypeUid="page"
    
            entryUid={getEntry.banner.uid}
    
            locale={getEntry.banner.locale}
    
          />
    
          <div className="blog-container">
    
            <div className="blog-column-left">
    
              {newBlogList?.map((blog: BlogList, index: number) => {
    
                return <BlogList blogList={blog} key={index} />;
    
              })}
    
            </div>
    
            <div className="blog-column-right">
    
              <h2>{contentstackPage?.page_components[1]?.widget?.title_h2}</h2>
    
              <ArchiveRelative data={newArchivedList} />
    
            </div>
    
          </div>
    
        </Layout>
    
      );
    
    };
    
    
    export const postQuery = graphql`
    
      query ($locale: String!) {
    
        contentstackPage(locale: { eq: $locale }) {
    
          title
    
          url
    
          uid
    
          locale
    
          seo {
    
            enable_search_indexing
    
            keywords
    
            meta_description
    
            meta_title
    
          }
    
          page_components {
    
            contact_details {
    
              address
    
              email
    
              phone
    
            }
    
            from_blog {
    
              title_h2
    
              featured_blogs {
    
                title
    
                uid
    
                url
    
                is_archived
    
                featured_image {
    
                  url
    
                  uid
    
                }
    
                body
    
                author {
    
                  title
    
                  uid
    
                  bio
    
                }
    
              }
    
              view_articles {
    
                title
    
                href
    
              }
    
            }
    
            hero_banner {
    
              banner_description
    
              banner_title
    
              bg_color
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            our_team {
    
              title_h2
    
              description
    
              employees {
    
                name
    
                designation
    
                image {
    
                  url
    
                  uid
    
                }
    
              }
    
            }
    
            section {
    
              title_h2
    
              description
    
              image {
    
                url
    
                uid
    
              }
    
              image_alignment
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            section_with_buckets {
    
              title_h2
    
              description
    
              buckets {
    
                title_h3
    
                description
    
                icon {
    
                  url
    
                  uid
    
                }
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            section_with_cards {
    
              cards {
    
                title_h3
    
                description
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            widget {
    
              title_h2
    
              type
    
            }
    
          }
    
        }
    
    
        allContentstackBlogPost {
    
          nodes {
    
            url
    
            title
    
            uid
    
            locale
    
            author {
    
              title
    
              uid
    
            }
    
            related_post {
    
              title
    
              body
    
              uid
    
            }
    
            date
    
            featured_image {
    
              url
    
              uid
    
            }
    
            is_archived
    
            body
    
          }
    
        }
    
      }
    
    `;
    
    
    export default Blog;
  13. Copy and paste the below code snippet to the blog-post.tsx file:
    import React, { useState, useEffect } from "react";
    
    import moment from "moment";
    
    import { graphql } from "gatsby";
    
    import SEO from "../components/SEO";
    
    import parser from "html-react-parser";
    
    import Layout from "../components/Layout";
    
    import { useLocation } from "@reach/router";
    
    import { onEntryChange } from "../live-preview-sdk/index";
    
    import ArchiveRelative from "../components/ArchiveRelative";
    
    import RenderComponents from "../components/RenderComponents";
    
    import { getPageRes, getBlogPostRes, jsonToHtmlParse } from "../helper";
    
    import { PageProps } from "../typescript/template";
    
    import { useLocale } from "../hooks/useLocale";
    
    
    const blogPost = ({
    
      data: { contentstackBlogPost, contentstackPage },
    
      pageContext,
    
    }: PageProps) => {
    
      const { pathname } = useLocation();
    
      const { locale }: any = useLocale();
    
    
      jsonToHtmlParse(contentstackBlogPost);
    
    
      const [getEntry, setEntry] = useState({
    
        banner: contentstackPage,
    
        post: contentstackBlogPost,
    
      });
    
    
      async function fetchData() {
    
        try {
    
          const {
    
            result: { url },
    
          } = pageContext;
    
          const entryRes = await getBlogPostRes(url, locale);
    
          const bannerRes = await getPageRes("/blog", locale);
    
          if (!entryRes || !bannerRes) throw new Error("Error 404");
    
          setEntry({ banner: bannerRes, post: entryRes });
    
        } catch (error) {
    
          console.error(error);
    
        }
    
      }
    
    
      useEffect(() => {
    
        onEntryChange(() => fetchData());
    
      }, [contentstackBlogPost, contentstackPage]);
    
      return (
    
        <Layout blogPost={getEntry.post} banner={getEntry.banner}>
    
          <SEO title={getEntry.post.title} />
    
          <RenderComponents
    
            components={getEntry.banner.page_components}
    
            blogPage
    
            contentTypeUid="blog_post"
    
            entryUid={getEntry.banner.uid}
    
            locale={getEntry.banner.locale}
    
          />
    
          <div className="blog-container">
    
            <div className="blog-detail">
    
              <h2 {...getEntry.post.$?.title}>
    
                {getEntry.post.title ? getEntry.post.title : ""}
    
              </h2>
    
              <span>
    
                <p>
    
                  {moment(getEntry.post.date).format("ddd, MMM D YYYY")},{" "}
    
                  <strong {...getEntry.post.author[0]?.$?.title}>
    
                    {getEntry.post.author[0]?.title}
    
                  </strong>
    
                </p>
    
              </span>
    
              <span {...getEntry.post.$?.body}>{parser(getEntry.post.body)}</span>
    
            </div>
    
            <div className="blog-column-right">
    
              <div className="related-post">
    
                {getEntry.banner.page_components[2].widget && (
    
                  <h2 {...getEntry.banner.page_components[2]?.widget.$?.title_h2}>
    
                    {getEntry.banner.page_components[2].widget.title_h2}
    
                  </h2>
    
                )}
    
                <ArchiveRelative
    
                  data={getEntry.post.related_post && getEntry.post.related_post}
    
                />
    
              </div>
    
            </div>
    
          </div>
    
        </Layout>
    
      );
    
    };
    
    
    export const postQuery = graphql`
    
      query ($title: String!) {
    
        contentstackBlogPost(title: { eq: $title }) {
    
          url
    
          title
    
          body
    
          uid
    
          locale
    
          date
    
          author {
    
            title
    
            bio
    
          }
    
          related_post {
    
            body
    
            url
    
            title
    
            date
    
          }
    
          seo {
    
            enable_search_indexing
    
            keywords
    
            meta_description
    
            meta_title
    
          }
    
        }
    
        contentstackPage(url: { eq: "/blog" }) {
    
          title
    
          url
    
          uid
    
          locale
    
          seo {
    
            enable_search_indexing
    
            keywords
    
            meta_description
    
            meta_title
    
          }
    
          page_components {
    
            contact_details {
    
              address
    
              email
    
              phone
    
            }
    
            from_blog {
    
              title_h2
    
              featured_blogs {
    
                title
    
                uid
    
                url
    
                is_archived
    
                featured_image {
    
                  url
    
                  uid
    
                }
    
                body
    
                author {
    
                  title
    
                  uid
    
                  bio
    
                }
    
              }
    
              view_articles {
    
                title
    
                href
    
              }
    
            }
    
            hero_banner {
    
              banner_description
    
              banner_title
    
              bg_color
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            our_team {
    
              title_h2
    
              description
    
              employees {
    
                name
    
                designation
    
                image {
    
                  url
    
                  uid
    
                }
    
              }
    
            }
    
            section {
    
              title_h2
    
              description
    
              image {
    
                url
    
                uid
    
              }
    
              image_alignment
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            section_with_buckets {
    
              title_h2
    
              description
    
              buckets {
    
                title_h3
    
                description
    
                icon {
    
                  url
    
                  uid
    
                }
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            section_with_cards {
    
              cards {
    
                title_h3
    
                description
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            widget {
    
              title_h2
    
              type
    
            }
    
          }
    
        }
    
      }
    
    `;
    
    export default blogPost;
  14. Copy and paste the below code snippet to the page.tsx file:
    import React, { useState, useEffect } from "react";
    
    import { graphql } from "gatsby";
    
    import SEO from "../components/SEO";
    
    import Layout from "../components/Layout";
    
    import { onEntryChange } from "../live-preview-sdk/index";
    
    import { getPageRes, jsonToHtmlParse } from "../helper";
    
    import RenderComponents from "../components/RenderComponents";
    
    import { PageProps } from "../typescript/template";
    
    import { useLocale } from "../hooks/useLocale";
    
    
    const Page = ({ data: { contentstackPage }, pageContext }: PageProps) => {
    
      jsonToHtmlParse(contentstackPage);
    
      const [getEntry, setEntry] = useState(contentstackPage);
    
      const { locale }: any = useLocale();
    
    
      async function fetchData() {
    
        try {
    
          const entryRes = await getPageRes(pageContext?.url, locale);
    
          if (!entryRes) throw new Error("Error 404");
    
          setEntry(entryRes);
    
        } catch (error) {
    
          console.error(error);
    
        }
    
      }
    
      useEffect(() => {
    
        onEntryChange(() => fetchData());
    
      }, []);
    
    
      return (
    
        <Layout pageComponent={getEntry}>
    
          <SEO title={getEntry.title} />
    
          <div className="about">
    
            {getEntry.page_components && (
    
              <RenderComponents
    
                components={getEntry.page_components}
    
                contentTypeUid="page"
    
                entryUid={getEntry.uid}
    
                locale={getEntry.locale}
    
              />
    
            )}
    
          </div>
    
        </Layout>
    
      );
    
    };
    
    
    export const pageQuery = graphql`
    
      query ($url: String!, $locale: String!) {
    
        contentstackPage(url: { eq: $url }, locale: { eq: $locale }) {
    
          uid
    
          title
    
          url
    
          seo {
    
            meta_title
    
            meta_description
    
            keywords
    
            enable_search_indexing
    
          }
    
          locale
    
          page_components {
    
            contact_details {
    
              address
    
              email
    
              phone
    
            }
    
            from_blog {
    
              title_h2
    
    
              featured_blogs {
    
                uid
    
                title
    
                url
    
                featured_image {
    
                  url
    
                  uid
    
                }
    
                author {
    
                  title
    
                  uid
    
                }
    
                body
    
                date
    
              }
    
              view_articles {
    
                title
    
                href
    
              }
    
            }
    
    
            hero_banner {
    
              banner_description
    
              banner_title
    
              banner_image {
    
                uid
    
                url
    
              }
    
              bg_color
    
              text_color
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            our_team {
    
              title_h2
    
              description
    
              employees {
    
                name
    
                designation
    
                image {
    
                  uid
    
                  title
    
                  url
    
                }
    
              }
    
            }
    
            section {
    
              title_h2
    
              description
    
              image_alignment
    
              image {
    
                uid
    
                title
    
                url
    
              }
    
              call_to_action {
    
                title
    
                href
    
              }
    
            }
    
            section_with_buckets {
    
              title_h2
    
              description
    
              bucket_tabular
    
              buckets {
    
                title_h3
    
                description
    
                icon {
    
                  uid
    
                  title
    
                  url
    
                }
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            section_with_cards {
    
              cards {
    
                title_h3
    
                description
    
                call_to_action {
    
                  title
    
                  href
    
                }
    
              }
    
            }
    
            section_with_html_code {
    
              title
    
              html_code_alignment
    
              html_code
    
              description
    
            }
    
            widget {
    
              type
    
              title_h2
    
            }
    
          }
    
        }
    
      }
    
    `;
    
    
    export default Page;
  15. After making all the necessary changes, save the code and run gatsby develop or gatsby build in Command Prompt.
  16. Run gatsby serve to check the changes.

Ensure you have made the necessary changes to the stack to support Internationalization.

You have now added multi-lingual support for the Gatsby starter app.

More Resources