cs-icon.svg

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:

    pages.png

    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: 
    templates.png
  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

Was this article helpful?
^