Almost EVERYONE who tried headless systems said they saw benefits. Download the state of CMS now!

Storyblok now on AWS Marketplace: Read more

O’Reilly Report: Decoupled Applications and Composable Web Architectures - Download Now

Empower your teams & get a 582% ROI: See Storyblok's CMS in action

Skip to main content

Manage Multilingual Content in Storyblok and Nuxt

Try Storyblok

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

  • Home
  • Tutorials
  • Manage Multilingual Content in Storyblok and Nuxt

Let’s see how to add and manage multiple languages on our website. For internationalization on the front end, we will use the @nuxtjs/i18n module and develop a simple language switcher in our header. For the backend, we will see how to manage multiple languages in our Storyblok space and how to get the translations from our codebase.

HINT:

You can read more about the @nuxtjs/i18n@next module 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 Stackblitz! Alternatively, you can explore or fork the code from the Nuxt Ultimate Tutorial GitHub Repository.

Section titled Requirements Requirements

This tutorial is part 6 of the Ultimate Tutorial Series for Nuxt. 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.


Section titled Adding a language in Storyblok 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 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 500 error in Nuxt. This is because when we make the API calls we are not specifying the language yet. We will configure it in a while by adding the @nuxtjs/i18n module and the Spanish (es) locale in the nuxt.config.js file.

Looking now at the translatable field, you will notice a change in the UI. You will see that the field is non-editable and has the default language's content in it {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 Nuxt 3 app.

Section titled Configuring @nuxtjs/i18n module in the Nuxt project Configuring @nuxtjs/i18n module in the Nuxt project

Let's start with the installation of version 8 of the package @nuxtjs/i18n, which is compatible with Nuxt 3, by running the command below:

bash
        
      npm install @nuxtjs/i18n@next --save-dev
    

The next step is to include the module and the i18n configuration in the nuxt.config.js file at the root of our project:

nuxt.config.js
        
      export default defineNuxtConfig({
      // other config
      modules: [
        // other modules
        '@nuxtjs/i18n',
      ],
      i18n: {
        strategy: 'prefix_except_default',
        locales: ['en', 'es'],
        defaultLocale: 'en', // default locale
      }
})
    

With this code, we are adding two locales in the project - English and Spanish (line 9), and setting our default locale as English (line 10). Once you add this code to your project, you will need to restart the server to see the changes.

hint:

The 'prefix_expept_default' strategy causes all your routes to have a locale prefix added except the default language. See the official documentation of @nuxtjs/i18n@next module to learn more about the available options.

Now we will be able to access the locale information in the whole project. However, to see the translations coming from Storyblok we will need to change a couple of components first, starting with our main slug view.

Open pages/[...slug].vue and update its script setup specifying the locale and creating a custom useAsyncData call using the Storyblok composable useAsyncStoryblok:

pages/[...slug].vue
        
      <script setup>
    let { slug } = useRoute().params
    
    const { locale } = useI18n()
    const resolveRelations = ['popular-articles.articles']
    
    const url = slug && slug.length > 0 ? slug.join('/') : 'home'
    
    const story = await useAsyncStoryblok(url.replace(/\/$/, ''),
      {
        version: 'draft',
        language: locale.value,
        resolve_relations: resolveRelations,
      },
      {
        resolveRelations,
      }
    )
</script>
    

As you can see in the code above, we are using the locale (line 4) selected by the user, or coming from the slug, to make the API request (line 12). Now, if you refresh the page, you will see that we don't get a 500 anymore. Instead, we get the translated version page.

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.

Translated version of an article page

When changing the language from the dropdown in Storyblok, or try to modify the title, we will see that the content of the translations is also modified in real-time. Moreover, text-type fields are not the only ones that can be translated, the translation also works in several types of fields, even in the assets. For example, we can translate the image we have on the article page, but for that, we have to update the schema of the Article Content Type and mark it as translatable in a similar way as we did with the title.

Once done with the block schema update, we can now upload another image for the other language. This will allow us to use different images for different languages/regions.

Change image per article page example

Now that we can see our pages translated, we must refactor our internal links, NuxtLink, using the useLocalePath() method the i18n module provides us. This way, the links of our page will have the corresponding locale automatically added to each URL.

Let’s start with the shared component ArticleCard.vue:

components/ArticleCard.vue
        
      <template>
      <NuxtLink
        :to="localePath(`/${slug}`)"
        v-editable="article"
        class="w-full h-full bg-[#f7f6fd] rounded-[5px] text-center overflow-hidden"
      >
        <!-- ArticleCard HTML -->
      </NuxtLink>
</template>
    
<script setup>
defineProps({ article: Object, slug: String })

const localePath = useLocalePath()
</script>
    

Followed by the Header.vue:

components/Header.vue
        
      <template>
  <header class="w-full h-24 bg-[#f7f6fd]">
    <div class="container h-full mx-auto flex items-center justify-between">
      <NuxtLink :to="localePath('/')">
        <h1 class="text-[#50b0ae] text-3xl font-bold">Storyblok Nuxt</h1>
      </NuxtLink>
      <nav>
        <ul class="flex space-x-8 text-lg font-bold">
          <li>
            <NuxtLink :to="localePath('/blog')" class="hover:text-[#50b0ae]">Blog</NuxtLink>
          </li>
          <li>
            <NuxtLink :to="localePath('/about')" class="hover:text-[#50b0ae]">About</NuxtLink>
          </li>
        </ul>
      </nav>
    </div>
  </header>
</template>

<script setup>
const localePath = useLocalePath()
</script>
    

That's it; this is how easily you can translate your content and manage multiple languages with Storyblok and Nuxt 3. Similarly, you can translate all the blogs and any other content you want. Let's also do a couple more things, such as adding a language switcher in the navigation and translating the Blog homepage as it is currently configured to receive the articles' data in the default language.

Section titled Adding a Language Switcher Adding a Language Switcher

We will use a couple of variables and methods from the i18n module to make the language switcher, making its creation super simple. Just replace the Header.vue with the code below:

components/Header.vue
        
      <template>
  <header class="w-full h-24 bg-[#f7f6fd]">
    <div class="container h-full mx-auto flex items-center justify-between">
      <NuxtLink :to="localePath('/')">
        <h1 class="text-[#50b0ae] text-3xl font-bold">Storyblok Nuxt</h1>
      </NuxtLink>
      <nav>
        <ul class="flex space-x-8 text-lg font-bold">
          <li>
            <NuxtLink :to="localePath('/blog')" class="hover:text-[#50b0ae]">Blog</NuxtLink>
          </li>
          <li>
            <NuxtLink :to="localePath('/about')" class="hover:text-[#50b0ae]">About</NuxtLink>
          </li>
          <li v-for="lang in availableLocales" :key="lang">
            <NuxtLink :to="switchLocalePath(lang)" class="uppercase">
              {{ lang }}
            </NuxtLink>
          </li>
        </ul>
      </nav>
    </div>
  </header>
</template>

<script setup>
const localePath = useLocalePath()
const { locale, locales } = useI18n()
const switchLocalePath = useSwitchLocalePath()

const availableLocales = computed(() => {
  return (locales.value).filter(i => i.code !== locale.value)
})
</script>

<style scoped>
nav a.router-link-active {
  @apply underline underline-offset-4 decoration-4 decoration-[#50b0ae];
}
</style>
    

First, we get all locales available in our project. Next, we loop through all the locales using v-for and we use the switchLocalePath() method to switch to the selected locale. After updating the main navigation, the website should now have a working language switcher {1}:

Language switcher example
1

There’s only one thing left. Right now, if we go to the Blog homepage by clicking Blog from the navigation when Spanish is selected, the blog teasers are in English. This is because we are not fetching the locale specific data inside the AllArticles.vue component.

Section titled Translating AllArticles Component Translating AllArticles Component

Now, inside the storyblok/AllArticles.vue component, let's fetch the specific data according to the locale by adding the language parameter to the API url. The updated code should look like this:

storyblok/AllArticles.vue
        
      <template>
  <div class="py-24">
    <h2 class="text-6xl text-[#50b0ae] font-bold text-center mb-12">{{ blok.title }}</h2>
    <div class="container mx-auto grid md:grid-cols-3 gap-12 my-12 place-items-start">
      <ArticleCard
        v-for="article in articles"
        :key="article.uuid"
        :article="article.content"
        :slug="article.full_slug"
      />
    </div>
  </div>
</template>

<script setup>
defineProps({ blok: Object })

const { locale } = useI18n()
const storyblokApi = useStoryblokApi()

const { data } = await storyblokApi.get('cdn/stories', {
  version: 'draft',
  language: locale.value,
  starts_with: 'blog',
  is_startpage: false,
})
const articles = ref(data.stories)
</script>
    

After implementing these changes you will see that all articles within the Blog story are translated.

Translate AllArticles component inside the blog listing page

Section titled Wrapping Up Wrapping Up

Congratulations, you are now able to build a full-blown multilingual Nuxt 3 website using Storyblok and i18n! In this tutorial, you saw how to add and manage multiple languages in your Nuxt 3 and Storyblok website by the Storyblok's Field-Level translation approach using the internationalization module @nuxtjs/i18n. We also added a basic language switcher on the navigation bar of the website, so that your project is ready to go to the market and serves as a base for future projects.

Next part:

The next part, exploring how to Create a preview environment for your Nuxt application, is already available.

Author

Alba Silvente

Alba Silvente

Alba, aka Dawntraoz, is a DevRel Engineer at Storyblok. She writes about frontend development on her personal blog, dawntraoz.com, and she is working hard on open-source projects to create more value for the web community.