Localizing your site can benefit your business or organization in several ways. By translating your content or site, you expand the markets you target. Adapting your product to the language and cultural preferences of potential customers who were not able to use your product before boosts your conversion rates.
Ultimately, this often leads to a growth in the revenue you generate. With a larger, more widespread customer base, your brand becomes increasingly recognizable and strengthened in newer markets.
A localized website has a higher SEO score which means that users within a specific market can find it easier through a search engine. A recognizable brand and improved SEO score reduce the cost of marketing to users within the markets you target.
We’ve seen that localization has its benefits, but what exactly is it? Localization is the process of revising your website, app, or content that was initially intended for a primary market to suit the needs of a new market you plan on targeting. Localization often involves translating a product into the language used in the market you want to introduce it to. It can also mean adding new things or removing parts of the product, for example, that might offend the market. You may also modify a product by changing its look and feel based on writing systems, color preferences, etc.
Although localization may seem straightforward, it cannot happen if the underlying site or app cannot accommodate these changes. Since it isn’t practical to build the same site for every market you want to enter, it makes sense that your site should switch content, language, UI elements, etc., between markets. That’s where internationalization comes in.
Internationalization is the process of designing and building a site or app to accommodate localization across different markets. For example, an online magazine’s site published in Portugal, Japan, and Ireland needs to accommodate different languages, writing systems, payment processors, and so on.
Before embarking on localization, it is important to pick a backend that will help you manage your site content across different locales. Strapi is one choice that provides this functionality. It’s an open-source headless content management system (CMS) built with Node.js. With it, you can manage and structure content into types using its content types builder on its user-friendly admin panel. For every content type you create, it automatically generates a customizable API for it. You can upload all kinds of media and manage them using its media library.
With its Role-Based Access Control (RBAC) features, you can set custom roles and permissions for content creators, marketers, localizers, and translators. This is especially useful since different people on a team should only be responsible for the content in the locales they manage. In this tutorial, you will learn about its internationalization feature that allows you to manage content in different languages and locales.
Your frontend also needs to handle your content in different languages and present it to multiple locales adequately and efficiently. Hugo is an amazing option for this. It’s a static site generator built with Go. It takes your data and content and applies it to templates. It then converts them to static pages, which are faster to deliver to your site visitors.
Hugo builds sites pretty fast, with average site builds completed in a second or less. It supports several content types, enables theme integration, meticulously organizes your content, allows you to build your site in multiple languages, and write content in markdown. It also supports Google Analytics, comments with Disqus, code highlighting, and RSS. Static sites are faster, have great SEO scores, have better security, and are cheaper and less complicated to make.
Without further ado, let’s dive right in!
Pre-Requisites
Before you can proceed with this tutorial, you will need to have:
Hugo installed.
You can get it through pre-built binaries, which are available for macOS, Windows, Linux, and other operating systems. You can also install it from the command line. These installation guides are available on the Hugo website explaining how to get it in this way. This tutorial was written using v0.68.
Node.js installed.
Strapi requires at minimum Node.js 12 or higher but recommends Node.js 14. Do not install a version higher than 14 as Strapi may not support it. The Node.js downloads page offers pre-built installers for various operating systems on its website.
An Example Site
To illustrate how localization can work using Strapi and Hugo, you’ll build a documentation website for a product used in Canada, Mexico, and America. The top three languages spoken in those regions are English, French, and Spanish. So, the documents on this site need to be displayed in each of them. The site will have three pages: a home page, an about page, and a terms page.
The Strapi CMS provides a platform to create content for those pages in those three languages. It will later serve the markdown versions of the content created through its API. The Hugo site will consume this content and display it depending on the language a user selects.
Step 1: Setting Up the Strapi App
In this step, you will install the Strapi app and set up an administrator account on its admin panel. The app will be called docs-server. To begin, on your terminal, change directories to the location you’d like the Strapi app to reside and run:
npx [email protected] docs-server
When prompted:
Select Quickstart as the installation type.
Pick No when asked to use a template.
This command will create a Strapi quickstart project, install the dependencies it requires, and run the application. It will be available at http://localhost:1337. To register an administrator, head to http://localhost:1337/admin/auth/register-admin. You should see the page below.
Enter your first and last names, an email, and a password. Once you’ve finished signing up, you will be redirected to the admin panel. Here’s what it looks like.
On the admin panel, you can create content types, add content entries, and manage settings for the Strapi app. In this step, you generate the Strapi app and set up an administrator account. In the next one, you will create content types for each of the three pages.
Step 2: Create the Content Types
In this step, you will create content types for each of the three pages. A content type on Strapi, as the name suggests, is a type of content. Strapi supports two categories of content types: collection types and single types. A collection type is for content that takes a single structure and has multiple entries.
For example, a blog post collection type collects multiple blog posts. A single type is for content that is unique and only has one entry. An about content type that models content for an about page, for instance, is a single type because a site typically has only one about page.
To generate these types, you’re going to use the Strapi CLI. You have the option of using the existing Strapi admin panel to create the types if you wish. However, the Strapi CLI can be faster and involves fewer steps.
If the Strapi is running, stop it. Running the commands in this step will cause errors that will crash the app. Once you’ve completed this step, you can run it again with the command below on your terminal within the docs-server directory:
npm run develop
Since you will have three separate pages, you will create three different single types. These will be the home, about, and terms types. Each will have a content and title attribute. These two attributes are just a starting point. You can modify the types later if you’d like to add more attributes or customize them further. To create them, run this command on your terminal within the docs-server directory:
Running the above command will generate the home, about, and terms content types with title and content attributes. It also generates APIs for each of the page types. The APIs are generated within the api/ folder. Here’s what this folder looks like now.
api
├── about
│ ├── config
│ │ └── routes.json
│ ├── controllers
│ │ └── about.js
│ ├── models
│ │ ├── about.js
│ │ └── about.settings.json
│ └── services
│ └── about.js
├── home
│ ├── config
│ │ └── routes.json
│ ├── controllers
│ │ └── home.js
│ ├── models
│ │ ├── home.js
│ │ └── home.settings.json
│ └── services
│ └── home.js
└── terms
├── config
│ └── routes.json
├── controllers
│ └── terms.js
├── models
│ ├── terms.js
│ └── terms.settings.json
└── services
└── terms.js
Each of the content types have models, services, controllers, and configuration created for them. Several API routes are added as well to create, modify, and retrieve content modeled against these types.
In the api/about/models/about.settings.json file, you will change the kind of the about content type from a collection type to a singleType. You will also add a description and enable localization for it and its attributes. Replace the code with the following:
{
“kind”: “singleType”,
“collectionName”: “about”,
“info”: {
“name”: “about”,
“description”: “The about page content”
},
“options”: {
“increments”: true,
“timestamps”: true,
“draftAndPublish”: true
},
“pluginOptions”: {
“i18n”: {
“localized”: true
}
},
“attributes”: {
“title”: {
“pluginOptions”: {
“i18n”: {
“localized”: true
}
},
“type”: “string”
},
“content”: {
“pluginOptions”: {
“i18n”: {
“localized”: true
}
},
“type”: “richtext”
}
}
}
In this file, you are adding detail to the content type that you can’t specify when generating them through the CLI. The kind property changes to a singleType from a collection type. Localization is enabled using the pluginOptions property. By setting localized to true under the i18n internationalization property, localization is enabled for the type as well as the attributes that specify the same property.
Next, you will modify its API routes to only have routes that will update, delete, and retrieve content. When you create a content type using the CLI, it is by default a collection type. A collection type has five routes created for it: routes to find, find one, count, delete, update, and post. A single type doesn’t need count, post and find-one routes since there’s just one entry. So you will be removing these. Replace the contents of api/about/config/routes.json with this code:
{
“routes”: [
{
“method”: “GET”,
“path”: “/about”,
“handler”: “about.find”,
“config”: {
“policies”: []
}
},
{
“method”: “PUT”,
“path”: “/about”,
“handler”: “about.update”,
“config”: {
“policies”: []
}
},
{
“method”: “DELETE”,
“path”: “/about”,
“handler”: “about.delete”,
“config”: {
“policies”: []
}
}
]
}
Since the other content types share the same attributes, you will make similar changes to the model settings for each of the other types. The content types in this tutorial share the same attributes for demonstration purposes but you can modify them to suit the needs of the pages you create. In the api/privacy/models/home.settings.json file, change the code to:
{
“kind”: “singleType”,
“collectionName”: “home”,
“info”: {
“name”: “Home”,
“description”: “The home page content”
},
“options”: {
“increments”: true,
“timestamps”: true,
“draftAndPublish”: true
},
“pluginOptions”: {
“i18n”: {
“localized”: true
}
},
“attributes”: {
“title”: {
“type”: “string”,
“pluginOptions”: {
“i18n”: {
“localized”: true
}
}
},
“content”: {
“type”: “richtext”,
“pluginOptions”: {
“i18n”: {
“localized”: true
}
}
}
}
}
Similar to the about API routes, you will remove the find-one, count, and post routes for the home content type since it’s a single type. Replace the contents of the api/home/config/routes.json file with this code:
{
“routes”: [
{
“method”: “GET”,
“path”: “/home”,
“handler”: “home.find”,
“config”: {
“policies”: []
}
},
{
“method”: “PUT”,
“path”: “/home”,
“handler”: “home.update”,
“config”: {
“policies”: []
}
},
{
“method”: “DELETE”,
“path”: “/home”,
“handler”: “home.delete”,
“config”: {
“policies”: []
}
}
]
}
Lastly, in the api/terms/models/terms.settings.json file, replace the existing code with:
{
“kind”: “singleType”,
“collectionName”: “terms”,
“info”: {
“name”: “Terms”,
“description”: “The terms content”
},
“options”: {
“increments”: true,
“timestamps”: true,
“draftAndPublish”: true
},
“pluginOptions”: {
“i18n”: {
“localized”: true
}
},
“attributes”: {
“title”: {
“type”: “string”,
“pluginOptions”: {
“i18n”: {
“localized”: true
}
}
},
“content”: {
“type”: “richtext”,
“pluginOptions”: {
“i18n”: {
“localized”: true
}
}
}
}
}
To remove the unnecessary find-one, count and post API routes for the terms content type, change the contents of api/terms/config/routes.json to this:
{
“routes”: [
{
“method”: “GET”,
“path”: “/terms”,
“handler”: “terms.find”,
“config”: {
“policies”: []
}
},
{
“method”: “PUT”,
“path”: “/terms”,
“handler”: “terms.update”,
“config”: {
“policies”: []
}
},
{
“method”: “DELETE”,
“path”: “/terms”,
“handler”: “terms.delete”,
“config”: {
“policies”: []
}
}
]
}
Now you have content types set up for all three pages. In the next step, you will add locales for the markets your content is targeted to.
Step 3: Adding the Locales
In this step, you will add the different locales you’d like to support. As explained in the example section, you will add English(America)(en-US), French(Canada)(fr-CA), and Spanish(Mexico)(es-MX). Be sure to run Strapi with npm run develop, then go to the Internationalization settings, under Settings then Global Settings, and add these locales by clicking the blue Add a locale button.
In the popup, select a locale then click Add locale. You should add the three locales listed in the table below. They are all available in the Locales dropdown.
Locale
Local Display Name
en-US
English(America)
es-MX
Spanish(Mexico)
fr-Ca
French(Canada)
When adding these locales, set one as the default locale under Advanced Settings in the Add a locale pop-up. This makes it easier when adding content the first time around. If you do not, the first entry will always default to the en locale. If you do not need the en locale, it’s best to delete it after setting an alternate default locale.
In this step, you added locales on your Strapi app. These will be used when you add content. In the proceeding step, you will add placeholder content for each of the pages.
Step 4: Add Content to Strapi App
In this step, you will add content to the Strapi app for each of the three pages. You will do this using the content manager on the admin panel. Here are links to content entry forms on the admin panel for each of the types:
About page
Home page
Terms page
Here’s what a content entry form looks like.
Add a title and some content. When adding content, always check the locale. Make sure the language of the content matches the locale language.
Once you’re done, click the bright green Save button then the Publish button in the top right of the entry form. When you want to add new content for a locale, select it from the Locales dropdown in the Internationalization section on the right of the form. Remember to save and publish the new content.
Here’s what you’ll add for each of the pages for the title field:
English (America)(en-US)
French (Canada)(fr-CA)
Spanish (Mexico)(ex-MX)
About
À propos
Sobre
Home
Accueil
Hogar
Terms
Conditions
Condiciones
For the content, you can use this lorem ipsum text for all the pages. You can add a flag emoji for the country to identify the change in locale. This is placeholder content only for demonstration purposes.
English (America)(en-US)
# 🇺🇸
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec neque ultrices, tincidunt tellus a, imperdiet nulla. Aliquam erat volutpat. Vestibulum finibus, lectus sit amet sagittis euismod, arcu eros tincidunt augue, non lobortis tortor turpis non elit.
French (Canada)(fr-CA)
# 🇨🇦
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec neque ultrices, tincidunt tellus a, imperdiet nulla. Aliquam erat volutpat. Vestibulum finibus, lectus sit amet sagittis euismod, arcu eros tincidunt augue, non lobortis tortor turpis non elit.
Spanish (Mexico)(ex-MX)
# 🇲🇽
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec neque ultrices, tincidunt tellus a, imperdiet nulla. Aliquam erat volutpat. Vestibulum finibus, lectus sit amet sagittis euismod, arcu eros tincidunt augue, non lobortis tortor turpis non elit.
In this step, you added placeholder content in multiple languages for different locales. In the next step, you will make the API routes for the content types public.
Step 5: Making the API Routes Public
In this step, you will make the routes that return page content public. These are the GET routes for /home, /about, and /terms. Currently, if you try to access them, you will get a 403 Forbidden error. This is because the permissions set do not allow them to be accessed publicly. You’ll change this so that they are publicly accessible.
To do this:
head over to the Public Roles settings under Users & Permissions Plugin using this link;
in the Application settings, under Permissions, select the find checkboxes for Home, About, and Terms;
click the bright green Save button in the top right of the page.
Here’s a screenshot of what checkboxes to select in the Application Permissions section in the Public Roles settings page:
Now the routes at http://localhost:1337/home, http://localhost:1337/about, and http://localhost:1337/terms are all accessible. They return the content you entered for the pages in the previous step. To specify a locale when fetching content, use the _locale query parameter and assign it the locale. For example, http://localhost:1337/home?_locale=fr-CA will return the home page for the Canadian French locale. If you do not specify a locale, content for the default locale will be returned.
In this step, you made the routes that return content public. In the next step, you will generate a Hugo site that will consume the localized content.
Step 6: Generate a New Hugo Site
The Hugo site that will display the localized content will be called docs-app. To generate it, run the following command on your terminal in a separate directory outside the docs-server project:
hugo new site docs-app
Running this command will generate a new Hugo site. It will scaffold the site with different folders that contain site input. Hugo will use this input and generate a whole site. However, no themes nor content have been added. You will have to add the content and theme. The content will come from the Strapi application. You can view the new site by running:
cd docs-app && hugo server
The app is served at http://localhost:1313/. However, the app is blank since there is no content yet.
In this step, you generated a new Hugo site. In the next step, you will add a documentation theme to it.
Step 7: Add a Theme to the Hugo Site
Hugo provides support for themes. You can add pre-configured theme components to your site. For the purpose of this tutorial, you will use the hugo-book theme, which is a theme for documentation sites. You can pick from a wide range of themes available on the Hugo theme showcase site. However, make sure that the theme supports internationalization.
To add the book theme, make sure you are in the docs-app folder, and if not, run:
cd docs-app
The app needs to have a git repository to add a theme as a git submodule. To initialize an empty one, run:
git init
To add the book theme, run:
This command adds the book theme repository as a submodule to the site. It clones the book theme into the themes folder. To view thedocs-app site using the book theme, you can run the app with this command:
hugo server –theme book
Here’s a screenshot of what it looks like:
The site is still pretty bare as it does not contain any content yet. You’ll add content to it from Strapi in the later steps.
In this step, you added a theme to your Hugo site that supports internationalization. In the following step, you will modify the setting of the docs-app site to support internationalization.
Step 8: Modify the Hugo Site Settings
While the book theme supports internationalization, you have to modify the settings of the docs-app to enable it. You will also modify other attributes of the site, like its title and base URL. Additionally, you will include other settings to disable search on the book theme and limit the cache lifespan. In the config.toml file, remove the existing code and add the one below:
# The default site and content language
languageCode = ‘en-us’
defaultContentLanguage = ‘en-us’
# The site title
title = ‘Docs’
# Setting the site theme to hugo-book
theme = ‘book’
[params]
# Disabling search here because it falls out of the scope of this tutorial
BookSearch = false
# The Strapi server URL
StrapiServerURL = ‘http://localhost:1337′
[caches]
[caches.getjson]
# Sets the maximum age of cache to 10s before it is cleared.
maxAge = “10s”
[languages]
# The US English content settings
[languages.en-us]
languageName = “English (US)”
contentDir = “content”
# The Canadian French content settings
[languages.fr-ca]
languageName = “Français (Canada)”
contentDir = “content.fr-ca”
# The Mexican Spanish content settings
[languages.es-mx]
languageName = “Español (Mexico)”
contentDir = “content.es-mx”
The StrapiServerURL is the URL of the Strapi server. Since it’s running locally for now, you will use http://localhost:1337. You’re going to use the getJSON Hugo function to fetch data from the server. It caches request results. During development, you may change the content on the Strapi app often, and because of cache, it may not reflect the changes you make. So, using the maxAge config property, you will set it to 10s; thus, the most recent Strapi content changes appear on the site. When you deploy the site, you will have to change this to an adequate timespan depending on how often the site is rebuilt, and the content is changed.
For the language settings, you will define three language categories. For each language, you will define a name and a directory for its content. Each of the content directories will be at the site root. The language names will be displayed in a dropdown where users can select what content they want. Here’s a table of the settings for each language.
Language Name
Language Code
Content Directory
English (US)
en-us
content/
Español (Mexico)
es-mx
content.es-mx/
Français (Canada)
fr-ca
content.fr-ca/
In this step, you added settings to the Hugo app to make it support internationalization. In the next step, you will modify the theme to accept localized content from an external server.
Step 9: Modify the Theme to Accept Strapi Content
In this step, you will modify the theme to accept data from a Strapi server. Although themes already come with pre-configured templates, you can override them by creating similar files in the layouts folder.
For the hugo-book theme, you will modify the template at themes/book/layouts/partials/docs/inject/content-after.html. This template displays whatever is added in it after the main page content. To do this, you will create this file in the layouts/ folder at the site’s root directory and then add content to it. In this file, you will define a template to fetch markdown content from the server, pass it through the markdown processor, and display it. The logic to fetch the content will be placed in a new partial template that you will call strapi-content. So, to create the content-after file, run these commands on your terminal:
Next, you will create the partial template to fetch content from Strapi:
In the layouts/partials/docs/strapi-content.html file, add this code:
{{ $endpoint := $.Param “endpoint” }}
{{ $data := dict “title” “” “content” “” }}
{{ if and $endpoint .Site.Params.StrapiServerURL }}
{{ $contentURL := printf “%s%s” .Site.Params.StrapiServerURL $endpoint }}
{{ $data = getJSON $contentURL }}
{{ end }}
{{ return $data }}
In this partial file, you fetch the endpoint page variable for a specific page and store it in $endpoint. This variable is added to the front matter of content files, as you will see in the proceeding step. Next, you create a variable called $data that is returned at the end of the partial. It will hold the content returned from the Strapi server. You will then assign it a default structure with a title and content. This is done in case no endpoint is specified, or a request is unsuccessful. Afterward, you check if a content endpoint and a Strapi server URL is set. You need both of these for a request. If set, you create a URL for the content you need and use the getJSON function to make a request. Lastly, you return the data.
In layouts/partials/docs/inject/content-after.html, add the code below to the file:
{{ $strapiData := partial “docs/strapi-content” . }}
<article class=”markdown”>
<h1>{{ $strapiData.title }}</h1>
{{ $strapiData.content | markdownify }}
</article>
Here, you are fetching the data using the strapi-content partial template. Once you get the content, you add the title as a heading within the article tag. Lastly, you take the returned content, pass it through the markdown processor using the markdownify function, and display it within the article tag.
In this step, you modified the theme by overriding one of its templates and adding a new partial template to fetch content from Strapi. In the next step, you will add content pages for each of the languages.
Step 10: Add Content Pages to the Hugo Site
In this step, you will add content pages. Each language has a content folder, as shown in the previous steps. The content folder is for English(US) content, content.es-mx for Español (Mexico) content, and content.fr-ca for Français (Canada) content. Each content file has to have an endpoint front matter variable which is the Strapi endpoint that provides its content in a specific language. You’ll add this variable in two archetypes files, archetypes/default.md and archetypes/docs.md.
Archetype files are templates for content files. They can be used to specify the front matter and other content. The hugo new command uses archetypes to generate new content files. archetypes/default.md will be the template for all the _index.md content files while archetypes/docs.md will be for all the content files in docs/ folders. archetypes/docs.md and docs/ are specific to the hugo-book theme. To create the archetypes/docs.md file on your terminal:
touch archetypes/docs.md
Next, replace the content of both archetypes/default.md and archetypes/docs.md with:
—
title: “{{ replace .Name “-” ” ” | title }}”
endpoint: “/”
—
<br/>
The title will be displayed as the page title and in the table of contents. endpoint, as mentioned earlier, is the Strapi endpoint that provides the content. You add the <br/> tag so that the page is not considered blank during a build.
To create the content folders for the other languages, run this command on your terminal:
mkdir content.es-mx content.fr-ca
Next, add content files for each of the pages:
This command creates an _index.md file, a docs/about.md file, and a docs/terms.md file in each of the content directories. Here’s what the content directories will look like after you run this command:
content
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
content.es-mx
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
content.fr-ca
├── docs
│ ├── about.md
│ └── terms.md
└── index.md
Here’s the front matter and content you should add for each of the files:
Home (index.md)
content
—
title: “Home”
endpoint: “/home?_locale=en-US”
—
<br/>
content.es-mx
—
title: “Hogar”
endpoint: “/home?_locale=es-MX”
—
<br/>
content.fr-ca
—
title: “Accueil”
endpoint: “/home?_locale=fr-CA”
—
<br/>
About (docs/about.md)
content
—
title: “About”
endpoint: “/about?_locale=en-US”
—
<br/>
content.es-mx
—
title: “Sobre”
endpoint: “/about?_locale=es-MX”
—
<br/>
content.fr-ca
—
title: “À propos”
endpoint: “/about?_locale=fr-CA”
—
<br/>
Terms (docs/terms.md)
content
—
title: “Terms”
endpoint: “/terms?_locale=en-US”
—
<br/>
content.es-mx
—
title: “Condiciones”
endpoint: “/terms?_locale=es-MX”
—
<br/>
content.fr-ca
—
title: “Conditions”
endpoint: “/terms?_locale=fr-CA”
—
<br/>
So, all you need to do now is run the Hugo server. Before you do this, make sure that the Strapi app is running with npm run develop in a different terminal within the docs-server folder, so Hugo can fetch content from it when building the site. You can run the Hugo server using this command:
hugo server
Since Hugo creates static sites, the content displayed will not be dynamic. Hugo gets the content from the Strapi server during build time and not on the fly when a page is requested. So, if you’d like content to regularly reflect what is on the Strapi server, make sure to automate rebuilds of your Hugo site regularly or as often as changes to the content are made. For example, if your site is hosted on Netlify, you can schedule regular rebuilds of your site.
Conclusion
Hugo is a static site generator that allows you to build fast and efficient static sites. It offers multilingual support using its internationalization feature. You can specify a range of languages, and Hugo will build a site to support each of them. Strapi is a headless CMS that allows its users to manage content with more flexibility. It provides an admin portal to enter and manage content and a customizable API that different frontends can consume the content through. It also offers an internationalization plugin to manage content in different locales.
In this tutorial, you created a Strapi application. Using this app, you added three single content types to represent data for three pages: a home, an about, and a terms page. You added content for each of the pages for three locales: English (US), Español (Mexico), and Français (Canada). You also generated APIs to access content for these pages and made some of its routes public.
After, you generated a Hugo app. In this app, you added a documentation theme, configuration to support internationalization, and content pages for different languages. Lastly, you modified the theme to consume content from Strapi. If you’d like to build out more of the app, try adding more content page types with complex structures or adding content in a new language.
If you’d like to learn more about Hugo, check out their documentation page. To find out more about what you can do with Strapi and the range of features it offers, head to its website here.