Localization in GatsbyJS with Kontent by Kentico

By Thanh Nguyen On December 07, 2021

Localization in GatsbyJS with Kontent by Kentico

Localization is an effective tool to extend the reach of messages and content on a website to additional audiences outside of the primary language. In a typical application or site, localization is often considered a difficult or complex feature for developers to implement, but with a static site framework like GatsbyJS coupled with a headless CMS like Kontent by Kentico, localization can be easy to implement.

Assumptions

This article focuses on implementing localization within a GatsbyJS site using Kentico Kontent and will not be delving deeply into configuring localization in Kontent itself. The Kontent Docs provides documentation on setting up localization in the CMS under Set up your project languages.

Another assumption that will be made is the use of the gatsby-source-kontent package as the primary method of retrieving data from Kontent and populating the GraphQL queries used in a site. For the purposes of properly communicating language to the client viewing the site, the gatsby-plugin-react-helmet will be used to modify the page structure outside of the site content. The install process and configuration of these packages can be found on their respective pages on the Gatsby package listing.

This article will focus on a "post" content type, which aligns with articles and blog posts, but the same process can be applied to any content type.

Considerations

Retrieving localized content from Kontent increases the complexity of the static site, as each instance of content will be multiplied by the number of languages configured in Kontent. For example, a site with 50 articles and six languages will have 300 pieces of queried content. The content retrieved for a particular subsection of the site can be reduced by using variables on a normal GraphQL query to filter out content by language, but Gatsby's static queries do not support variables. As using static queries would require additional handling in JavaScript to filter out all content types, static queries will be avoided. As independent components won't be able to query for the data used in the component, the content types used for those components, such as navigation or metadata, should be queried for at the same time as other content. Using a "global" content type that holds all global information typically retrieved using static queries can make modeling localizable content more manageable.

Pages

To reduce the amount of data available to any particular page and to manage these pages across languages, it is expected that all pages will be created using Gatsby's Node API (even the index page). The key important difference between a single language site and a multi-language site at this point is the inclusion of an additional GraphQL field provided by Kontent: preferred_language. The language field in the system corresponds to the actual language of the content instance, which will be the default or primary language if it hasn't been translated.

The query to retrieve articles for page creation might end up looking like the following:

 query ArticleQuery {
  allKontentItemArticle {
    edges {
      node {
        preferred_language
        system {
          codename
          id
        }
        elements {
          slug {
            value
          }
        }
      }
    }
  }
}

slug and preferred_language are important for the next step: creating the localized page. Gatsby's createPage function takes an object containing the page path, the template component, and any context data to pass into the page template. Configured using the query above, the createPages API in Gatsby might end up looking like the following:

// gatsby-node.js
const path = require('path');

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  
  const result = await graphql(
    `
    query ArticleQuery {
      allKontentItemArticle {
        edges {
          node {
            preferred_language
            system {
              codename
              id
            }
            elements {
              slug {
                value
              }
            }
          }
        }
      }
    }
    `
  );
  
  result.data.allKontentItemArticle.edges.forEach(({node}) => {
    createPage({
      path: `${node.preferred_language}/article/${node.elements.slug.value}`,
      component: path.resolve(`./src/templates/article.template.js,
      context: {
        language: node.preferred_language
      }
    });
  });
}

This will create a page using the article.template.js file for each Article content item, assigning the page path based on the language and the slug, and provide the language variable into the page query associated with the template. After the filters are applied, the data forwarded to the template component will be no different from the data used on a single language site.

const ArticleTemplate = ({ data }) => {
  // ...omitted...
}

export default ArticleTemplate;

export const query = graphql`
  query ArticleTemplateQuery($id: String!, $language: String!) {
    kontentItemArticle(
      id { eq: $id }
      preferred_language: { $q: $language }
    ) {
      id
      preferred_language
      elements {
        // ...omitted...
      }
    }
  }
`;

Metadata

In order to communicate with the browser that the particular page is intended to be in a specific language, the lang attribute should be set on the page. One easy way of making the page metadata header available to page generation is through the use of the gatsby-plugin-react-helmet plugin. This is fairly simple to do and can be achieved by using the helmet component with the htmlAttributes prop.

const language = data.kontentItemArticle.preferred_language;

<Helmet
  htmlAttributes={{
    lang: language
  }}
/>

The language code used by the lang attribute is described in RFC 5646: Tags of Identifying Languages. In the event that the language names do not align between Kontent and those considered valid for HTML, mapping those to valid language tags is left as an exercise for the reader. Instances of this may occur when the language code name is the full language name, such as English or Japanese, or when the default language is named default.

Components

One benefit of using Kontent as a headless CMS for a multi-language Gatsby site is that Kontent allows the linking of content types, allowing one content type to contain link to many others, such as a navigation content type containing links, or a banner or featured items content type containing multiple call to action buttons. Intentional structuring of content types can enable reuse of translations across multiple components, such as through a "localization string" content type. In order to make use of these content trees, especially due to the increased overhead of static queries on a multi-language site, one effective approach is to pass the relevant context item from the page template to the components that implement a specific content type. The following will demonstrate how to do this for a hero or featured component with a call to action.

// article.template.js

const CallToAction = ({ label, url, text }) => {
  return (
    // ...omitted...
  )
}

const ArticleTemplate = ({ data }) => {
  // ...omitted...
  const cta = data.kontentItemArticle.elements.cta[0];
  const label = cta.elements.action_label.value[0].elements.text.value;
  const toUrl = cta.elements.url.value;
  const text = cta.elements.text.value;
  
  return (
    <Layout>
      <CallToAction label={label} url={toUrl} text={text} />
    </Layout>
  );
}

export default ArticleTemplate;

export const query = graphql`
  query ArticleTemplateQuery($id: String!, $language: String!) {
    kontentItemArticle(
      id { eq: $id }
      preferred_language: { $q: $language }
    ) {
      id
      preferred_language
      elements {
        // ...omitted...
        cta {
          value {
            ... on kontent_item_cta {
              elements {
                text {
                  value
                }
                url {
                  value
                }
                action_label {
                  value {
                    ... on kontent_item_localization_string {
                      elements {
                        text {
                          value
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
`;

The action_label on the call to action is retrieved from the kontent_item_localization_string, which is an additional component that consists of just a text value that may be used in multiple locations, such as "Subscribe" or "Enroll." A similar process can be used to query for a global component, which can be used to provide access to content types that are used on all pages, such as headers and footers.

Further Reading

GraphQL can be organized into "fragments", which act as subsets of queries, in order to make writing page queries, especially when using many common content types, like the localization string content type as mentioned above. Gatsby has condensed the GraphQL documentation into an easy-to-access form, which can be read here.

Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Thanh Nguyen

About the author

Thanh hails from a process-driven enterprise software development background and a diverse educational profile in art, linguistics, and computer science. Thanh has dabbled in many development fields, from mobile development to systems programming, with everything from Ruby to Go, specializing in web development. Thanh’s leisure activities include building and painting plastic models, DIY electronics, and exploring new knowledge in web development and the Jamstack.

View other posts by Thanh

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.