⚠️Draft Content

Storyblok Raises $80M Series C - Read News

What’s the True Total Price of Enterprise CMS? Find out here.

Skip to main content

Manage Multilingual Content in Storyblok and Astro

Try Storyblok

Storyblok is the first headless CMS that works for developers & marketers alike.

Let’s see how to add and manage multiple languages on our website. We'll statically generate all the pages for each language. Additionally, we'll develop a simple language switcher in our header. For the backend, we will learn how to manage multiple languages in our Storyblok space and how to get the translations from our codebase.

HINT:

You can read more about how to Add i18n features in Astro here.

Before starting with the practical part, it is important to know that Storyblok has three different approaches to implementing internationalization. In this tutorial, we will be using Field-Level translation. Depending on your requirements it will make sense to use one instead of the other. You can read all about the different approaches in our Internationalization guide.

Live demo:

If you’re in a hurry, have a look at our live demo in Netlify! Alternatively, you can explore or fork the code from the Astro Ultimate Tutorial GitHub Repository.

Requirements

This tutorial is part 6 of the Ultimate Tutorial Series for Astro. We recommend that you follow the previous tutorials before starting this one.

Hint:

We will use the code from the previous tutorial as a starting point. You can find it here.


Adding a language in Storyblok

First, let's add a new language to our Storyblok space. Go to Settings {1} and click on Internationalization {2}. Here, you will find the configuration for field-level translation.

Hint:

Although you can select any language you want, for the purpose of this tutorial we will use Spanish as the second language.

Let's select the Spanish language from the drop-down {3} and hit the Add button {4}. Once the Spanish language is added, save the changes by clicking on the Save button {5}.

Adding a language in the Storyblok space
1
2
3
4
5

Adding a language in the Storyblok space

If we now go to the Content section and open any Story, we will see a language drop-down in the action bar {1}.

Language drop-down in the Storyblok space action bar
1

Switching the language from the drop-down won't work yet because we haven't translated anything yet.

Since we’re using the field-level translation approach, we need to make the fields translatable in our component schema in order to allow the translation of our content. Let's edit the title field of the article Content Type and mark it as (Translatable) {1}. Hit the Save & Back to Fields button after changing the field {2}.

Mark block fields as translatable
1
2

If we change the language now, we will get a 404 error in Astro. This is because we have not generated these new routes.

Looking now at the translatable field, you will notice a change in the UI. You will see that the field is non-editable and contains the default language's content {1}. If we want to translate it, we must click on the Translate checkbox {2}.

Use of the "Translate" checkbox per field in the content area
1
2

By activating it, we will be able to edit the field and add the content in Spanish. But not only that. As we can see in the screenshot below, when we activate the translate option, an arrow button {1} appears. If we expand it, we will see the default language content {2}, a button to add the default language content in the new language {3}, and the option to go to Google Translate {4}.

Features when field translation is enabled per field
1
2
3
4

Let's hit the Publish button with the translated content and configure our code in the frontend to make it work.

Implementing i18n in Astro

Let's first generate all the URLs for our Astro site so we don't get a 404 error.
Open pages/[...slug].astro and update the getStaticPaths function to generate all new language pages.

pages/[...slug].astro
        
      export async function getStaticPaths() {
  //You can also have this in an utils file so it can be reused.
  let languages = ['en', 'es']
  const storyblokApi = useStoryblokApi()
  const links = await storyblokApi.getAll('cdn/links', {
    version: 'draft',
  })
  let paths = []
  links
    .filter((link) => !link.is_folder)
    .forEach((link: { slug: string }) => {
      languages.forEach((language) => {
        //This slug will be used for fetching data from storyblok
        let slug = link.slug === 'home' ? undefined : link.slug
        //This will be used for generating all the urls for astro
        let full_url = language === 'en' ? slug : `${language}/${slug ?? ''}`
        //This will let us change the url for diffrent versions
        let langSwitch = {}
        languages.forEach((lang) => {
          langSwitch = {
            ...langSwitch,
            [lang]: lang === 'en' ? `/${slug ?? ''}` : `/${lang}/${slug ?? ''}`,
          }
        })
        paths.push({
          props: { language, slug, langSwitch },
          params: {
            slug: full_url,
          },
        })
      })
    })
  return paths
}
    

Okay, we did quite a bit of refactoring, so let's understand what's happening. We are using the Storyblok's Links API to generate all the pages like before but now we are adding all the language paths, too. It's controlled via the languages array at the top. Now in the future, if we want to add a new language in Storyblok we just have to add that language to this array and it will generate all the pages for that language, too.

Moreover, this time we are passing multiple values via the props as well. This will help us to fetch the correct data for each language and add a language switcher in the header.

pages/[...slug].astro
        
      const { slug, language, langSwitch } = Astro.props
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(
  `cdn/stories/${slug === undefined ? 'home' : slug}`,
  {
    version: 'draft',
    resolve_relations: ['popular-articles.articles'],
    language,
  }
)
const story = data.story
    

Next, we are going to get the slug from Astro.props along with all the other information we passed in the getStaticPaths function.

learn:

Previously we used Astro.params to get the slug but that will not work here, as the Storyblok API takes the language as a parameter, not as the part of URL.

We also have to pass the language as a parameter in the storyblokApi.get function. This will now fetch the proper data for each page.

Language drop-down in the Storyblok space action bar
1

Just by doing this, we can see our article pages now showing correct data based on the language and we are not seeing 404 pages.

hint:

For the home page, as it has a real path set to it, it won't change the path when the language is switched from the dropdown. It will only work fine in the browser. You can use the Advanced paths app to configure the preview url programmatically for the visual editor.

Translating the AllArticles Component

If we take a look at our Blog home story, it does not work properly.

Language drop-down in the Storyblok space action bar

We can see the title in Spanish but all the article cards are still showing in English. Let's fix this issue by passing the language as a prop to all the components.

pages/[...slug].astro
        
      <BaseLayout langSwitch={langSwitch} language={language}>
  <StoryblokComponent language={language} blok={story.content} />
</BaseLayout>
    

We are also passing a few props to the BaseLayout component which will help create the language toggle in the future.

Next, we also have to pass this language prop in the storyblok/Page.astro component.

storyblok/Page.astro
        
      ---
import { storyblokEditable } from '@storyblok/astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'

const { language, blok } = Astro.props
---

<main {...storyblokEditable(blok)}>
  {
    blok.body?.map((blok) => {
      return <StoryblokComponent language={language} blok={blok} />
    })
  }
</main>
    

Now, we can get this language prop from our storyblok/AllArticles.astro component. Let's, pass the language as a parameter in the storyblokApi.get function and as a prop in ArticleCard component.

storyblok/AllArticles.astro
        
      ---
import { storyblokEditable, useStoryblokApi } from '@storyblok/astro'
import ArticleCard from '../components/ArticleCard.astro'

const { blok, language } = Astro.props
const storyblokApi = useStoryblokApi()
const { data } = await storyblokApi.get(`cdn/stories`, {
  version: 'draft', // or 'published'
  starts_with: 'blog/',
  is_startpage: false,
  language,
})

const articles = data.stories
---

<section class="mx-6 my-12" {...storyblokEditable(blok)}>
  <h1 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">
    {blok.title}
  </h1>
  <ul class="grid grid-cols-1 gap-6 md:grid-cols-3">
    {
      articles?.length &&
        articles.map((article) => {
          article.content.slug = article.slug
          return (
            <li>
              <ArticleCard language={language} article={article.content} />
            </li>
          )
        })
    }
  </ul>
</section>
    

Looking at the Blog home story again, we can see all the article cards showing the correct data.

Language drop-down in the Storyblok space action bar

Now that we can see our pages translated, we must refactor our internal links. This way, the links of our page will have the corresponding locale automatically added to each URL.

First, we have to pass the language prop in a few components. Let’s start with the storyblok/PopularArticle.astro component:

storyblok/PopularArticles.astro
        
      ---
import ArticleCard from '../components/ArticleCard.astro'
import { storyblokEditable } from '@storyblok/astro'

const { language, blok } = Astro.props
let articles = blok.articles
---

<section class="mx-6 my-12" {...storyblokEditable(blok)}>
  <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">
    {blok.headline}
  </h2>
  <ul class="grid grid-cols-1 gap-6 md:grid-cols-3">
    {
      articles?.length &&
        articles.map((article) => {
          article.content.slug = article.slug
          return (
            <li>
              <ArticleCard language={language} article={article.content} />
            </li>
          )
        })
    }
  </ul>
</section>

    

Here we are also using the shared ArticleCard component. We have to pass the language as a prop in the ArticleCard component.

Now let's create a small utility function that will help us manage translated links.

utils/langs.js
        
      //You can have the languages in an utils file so it can be reused.
let languages = ['en', 'es']
function getTransLink(language, slug) {
  return language === 'en' ? slug : `/${language}${slug}`
}
export { getTransLink, languages }
    

We can now use this function in our ArticleCard component.

component/ArticleCard.astro
        
      ---
import { getTransLink } from '../utils/langs'
const { language, article } = Astro.props
---

<article class="column feature bg-gray-100 rounded">
  <!-- Rest of the ArticleCard html -->
  <a
    href={getTransLink(language, `/blog/${article.slug}`)}
    class="font-semibold text-blue-600 hover:underline"
    title="read more"
  >
    Read More »
  </a>
</article>
    

Next, Let's pass this language prop in the Header.astro component. Also, we can add the language as a dynamic variable in the HTML lang attribute. <html lang={language}>

layouts/BaseLayout.astro
        
      ---
import Header from '../components/Header.astro'
const { language, langSwitch } = Astro.props
---

<html lang={language}>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link rel="icon" type="image/x-icon" href="/favicon.ico" />
    <title>The Storyblok Astro Ultimate Tutorial: Part 2</title>
  </head>
  <body class="mx-auto">
    <Header langSwitch={langSwitch} language={language} />
    <slot />
  </body>
</html>
    

If you remember, we have passed language and langSwitch props in the BaseLayout component from pages/[...slug].astro

We are simply forwarding it to the Header component.

component/Header.astro
        
      ---
import { languages, getTransLink } from '../utils/langs'
const { language, langSwitch } = Astro.props
---

<header class="w-full h-24 bg-[#f7f6fd]">
  <div class="container h-full mx-auto flex items-center justify-between">
    <a href={getTransLink(language, '/')}>
      <h1 class="text-[#50b0ae] text-3xl font-bold">Storyblok Astro</h1>
    </a>
    <nav class="flex space-x-8 text-lg font-bold">
      <a href={getTransLink(language, '/blog')} class="hover:text-[#50b0ae]">Blog</a>
      <a href={getTransLink(language, '/about')} class="hover:text-[#50b0ae]">About</a>
    </nav>
  </div>
</header>
    

That's it, this is how easily you can translate your content and manage multiple languages with Storyblok and Astro. Similarly, you can translate all the blogs and any other content you want. Let's also do one more thing, add a language switcher in the navigation.

Adding a Language Switcher

This will be pretty straightforward, we already have the langSwitch data in our Header component.

components/Header.astro
        
      ---
import { languages, getTransLink } from '../utils/langs'
const { language, langSwitch } = Astro.props
---

<header class="w-full h-24 bg-[#f7f6fd]">
  <div class="container h-full mx-auto flex items-center justify-between">
    <a href={getTransLink(language, '/')}>
      <h1 class="text-[#50b0ae] text-3xl font-bold">Storyblok Astro</h1>
    </a>
    <nav class="flex space-x-8 text-lg font-bold">
      <a href={getTransLink(language, '/blog')} class="hover:text-[#50b0ae]">Blog</a>
      <a href={getTransLink(language, '/about')} class="hover:text-[#50b0ae]">About</a>
      {
        languages.map((lang) => (
          <a
            class={`lang-toggle hover:bg-gray-200 px-2 ${lang === language ? 'bg-gray-200' : ''}`}
            href={langSwitch[lang]}
          >
            {lang}
          </a>
        ))
      }
    </nav>
  </div>
</header>
    

Wrapping Up

Congratulations, you are now able to build a full-blown multilingual Astro website using Storyblok. In this tutorial, you saw how to add and manage multiple languages in your Astro and Storyblok website with Storyblok's field-level translation. We also added a basic language switcher on the navigation bar of the website, so that your project is ready to go to production and serve as a base for future projects.

Next part:

In the next part, We will learn how to create a preview environment for your Astro application.

Author

Dipankar Maikap

Dipankar Maikap

Dipankar is a seasoned Developer Relations Engineer at Storyblok, with a specialization in frontend development. His expertise spans across various JavaScript frameworks such as Astro, Next.js, and Remix. Passionate about web development and JavaScript, he remains at the forefront of the ever-evolving tech landscape, continually exploring new technologies and sharing insights with the community.