All stories

How to Swap i18n Content in Storyblok

6 min read by  Former employee

screen-with-code.jpg

Did you create a beautiful website with a lot of content and then someone came along with a request to change the default language? If the answer is yes, then don't worry, it's pretty easy to swap translated content using Management API of Storyblok.

Detailed Case Description

Yes, we also experienced this situation at Wondrous LTD. As we set up the project, we already knew that the page would have to be multilingual and would require at least 2 language versions of content for go live. Storyblok offers you 3 different solutions of internationalization - Single tree and field level translation, Multiple trees and Mixed approach. We choose option no. 1 - Single tree and field level translation. This means that you have one default language version  and you can add as many additional language versions as you want through settings. We added German as a secondary language and set up the whole NuxtJS project with English as the default language in mind.

After some time and “go live” we were kindly asked if it would be possible to make the German translation default and English secondary. After that, an editor would see “Default” and “English” listed in dropdown. Basically, you have two options how to do it: 

  1. You can do it manually and copy&paste all content
  2. You can use the Management API of Storyblok and write a short script, which will do it for you.

I believe, you would choose the second option.

tl;dr

You don't have 6 minutes to spare? Easy, just scroll to the end and download the script through the provided link and use it.

NodeJS Script

I am going to show you how to do it in NodeJS, but you could do it also in Ruby, Python, PHP, Java, Swift or C#. See the storyblok documentation for more info on that.

Pseudo Code

  • Get schemas of all your components and for each component save which fields are translatable
  • Loop through all your stories
  • Recursively find all translatable fields in stories content
  • Move content of fields to default language and create new translation fields for original content
    •  Create new field “headline__i18n__en” and fill in value of “headline”
    • "headline" value replace with value of “headline__i18n__de”
  • Update original story with new content

Setup script

Install storyblok-js-client and get your personal access token from your account settings - this is not a space token, but an account token. Then get the id of the space where you will want to perform a swap of content and finally define the new default language and where you want to move the old default content.

  • fromLang define postfix of i18n field - eg. headline__de__
  • toLang define postfix of new field, where the default content will be save - eg. headline__en__
// Get schemas of all components
Storyblok.get(`spaces/${spaceId}/components`, {})
.then(response => {
  // Find translatable fields on each component
  response.data.components.map(component => addTransKeyFromComponent(component))
  // After get of all translatable fields start content swapping
  swapContentOnStories(1) // start contant swapping
}).catch(error => {
  console.log(error)
})

// Finds translatable fields in components and save them into i18nComponentsFields
function addTransKeyFromComponent(component) {
  const compName = component.name
  const compSchema = component.schema
  const compKeys = Object.keys(compSchema)
  let i18nKeys = []
  compKeys.map( key => {
    if (compSchema[key].translatable) {
      i18nKeys.push(key)
    }
  })
  i18nComponentsFields[compName] = i18nKeys
  console.log(`✅   got all i18n fields of ${compName}`)
}

Get all stories

Now we know which content of the fields needs to be swapped. So we need to get all stories. Tge function Storyblok.get(spaces/${spaceId}/stories, {}) returns a paged response of all stories. We get through all pages and for each story we will request the content of the story using story.id.

function swapContentOnStories(page=1) {
  Storyblok.get(`spaces/${spaceId}/stories`, {
    page // starting with first page
  })
  .then(response => {
    response.data.stories.map(story => getStoryContent(story.id))
    if (response.total - response.perPage * page > 0) {
      // recursively get all stories from Storyblok - response is paged!!
      swapContentOnStories(page + 1) 
    }
  }).catch(error => {
    console.log(error)
  })
}

Get story content

Here we just get content of the story and we will call swapContentOfStory function to swap i18n content. After we swap content we have to update/push the new content of the story to Storyblok using the function updateStory.

function getStoryContent(storyId) {
  Storyblok.get(`spaces/${spaceId}/stories/${storyId}`, {})
  .then(response => {
    swapContentOfStory(response.data.story.content) // swap content
    console.log(`✅   swapped content on ${response.data.story.name}`)
    updateStory(storyId, response.data.story) // update story in Storyblok
  }).catch(error => {
    console.log(error)
  })
}

Swap content

Finally swapping - so first we need to know, which fields of the current content are translatable. For that, we will use the name of the component (content.component) and the previously created object i18mComponentsFields, which contains all translatable fields sorted per component.

 

Now we look at the keys of the current content, if it contains any match with the translatable field of the component. We will perform a swap of content for these fields (lines 6-8).  

 

If the key is not translatable and it is an Array we will recursively call one more time this function swapContentOfStory. For all other cases, we do nothing because these are the fields which are not translatable (usual stuff like _uid etc.).

function swapContentOfStory(content) {
  const i18nContentKeys = i18nComponentsFields[content.component]
  const contentKeys = Object.keys(content)
  contentKeys.map(key => {
    if(i18nContentKeys && i18nContentKeys.includes(key)) {
      content[`${key}__i18n__${toLang}`] = content[key]
      // if no translation leave the default
      content[key] = content[`${key}__i18n__${fromLang}`] ? content[`${key}__i18n__${fromLang}`] : content[key]
    } else if (Array.isArray(content[key])) {
      content[key].map(component => swapContentOfStory(component))
    } else {
      // Do nothing - not i18n field
    }
  })
}

Update story

The last step of the script is an update of the story with new content. For that, you need to have an id of the story and new content and call this function. Easy!

function updateStory(storyId, updatedStory) {
  Storyblok.put(`spaces/${spaceId}/stories/${storyId}`, {
    "story": updatedStory,
    "force_update": 1
  }).then(response => {
    console.log(`⬆️   successfully updated - [${response.data.story.name}] `)
  }).catch(error => {
    console.log(error)
  })
}

DON'T FORGET!

At this moment your story in Storyblok will look like the code below. BUT we are not done yet, you still have to go to the language setting of the space in Storyblok (Space → Settings → Languages) and remove the German language and add the English language. Btw don't worry about adding/removing languages as this action will not remove content from your stories. It will correctly set up your visual editor of Storyblok and delivery API of Storyblok.


"story": {
    "name": "My First Article",
    "slug": "first-post",
    "content": {
      "component": "post",
      "headline": "Das ist toll!",
      "headline__i18n__de": "Das ist toll!",
      "headline__i18n__en": "This is awesome!"
    }
  }

That's it - you can now swap your content as often as you want. And don't worry at all, you will always have backups and the version tree in Storyblok. 

Download Script

I also created a little script, which can be downloaded from this GIST and can be run from your command line.

node ./swapContentTranslations.js --token {oauthToken} {spaceID} {fromLang} {toLang}

This article was written with big support of Storyblok, so share some love with them. And if you liked it, then like me back on twitter - @SamuelSnopko - it will motivate me to write more articles like this one.