Measurable Success «10 Checkpoints for Future-Proof Enterprise CMS» White Paper
Measurable Success - White Paper

Creating Multi-Language Websites

In this guide, which is based on the Scrivito Example App, we will give you an idea of how separate language sites can be implemented in a single application backed by a single Scrivito CMS. The most obvious advantage of this approach, when compared to a multi-CMS solution, is that one doesn’t have to duplicate content common to several or all sites, e.g. binary content such as images or PDF files. Also, there is only one infrastructure and one editing interface, keeping maintenance and training efforts – to name just these – at the lowest level possible.

Representing “language” in the CMS

Generally speaking, when planning to offer content in different languages on a website, the language of a piece of content becomes the criterion that defines the “realm”, or namespace, the page belongs to. With Scrivito, such namespaces can be created via the page hierarchy.

In a Scrivito CMS, the hierarchical structure of a website is formed by the unique paths of the individual pages. With single websites, the root object usually represents the homepage, and this is how Scrivito is configured by default. Since one cannot have more than one root object – and the root object must be the object whose path is / – we will use an individual homepage object (e.g. en, de, fr) for each site.

Introducing languages by extending the website structure

We will prepend the paths of these objects with /lang to have a namespace under which all language sites can be accessed. This also allows us to maintain language-site-independent content by using path prefixes such as /products or /authors. Like en, de, etc., /lang is a path component, but no CMS object with this path needs to exist since its only purpose is to keep the language-specific sites together, separated from the rest.

So this is the convention the paths of our language-specific homepages will follow, /lang plus a two-letter language identifier:

/lang/en # English
/lang/de # German
/lang/fr # French

This convention allows us to maintain any number of homepages in one CMS. Underneath the homepage objects, the websites will look just like any other Scrivito site with the default root object as the homepage.

Following what the Example App comes with, each Homepage object holds all the site settings including a logo and various API keys for third-party services. This gives you the flexibility to have these settings language-site-specific, but you must also set them individually.

Even though the Scrivito Example App serves as a starting point for this guide, you can turn any other website you developed with Scrivito into a multi-language site.

Preparing the Scrivito application

Introducing language-specific homepages

As we are assuming that the current content in the CMS is to become one of the future sites, we'll first change the paths of all CMS objects (except the ones that don’t have a path, e.g. landing pages) so that they start with /lang/en. After that, we'll adapt how the app finds the homepage belonging to a page with such a language-specific path.

Make sure you are in a working copy and in editing mode, then, in the browser console, select the scrivito_application context and execute:

Browser console
Scrivito.load(() => Scrivito.Obj.where('_path','startsWith','/').toArray()).then(a => {
  for (var obj of a) {
    obj.update({_path: `/lang/en${obj.path()}`});
    console.log(obj.path()); 
  }
})

/lang/en
/lang/en/product
/lang/en/pricing
/lang/en/about/contact
…

As you can see, the above code outputs the paths of all pages that start with /lang/en now.

Browsing the Example App in this state will produce a 404 error page saying that the root CMS object was not found. We know why: The root object represents the page to be rendered for the “/” URL, the standard homepage object with the / path. This object doesn’t exist anymore because we’ve changed its path to /lang/en. Also, the root object is referenced in several places in the Example App (e.g. when the navigation is rendered).

We’re going to take care of both situations, i.e. make the “/” URL point to the new /lang/en homepage, and replace all the Example App’s references to the default root object with references to the proper language-specific homepage.

Changing the default homepage

As indicated, the “/” URL defaults to the root object in the CMS. Luckily, Scrivito lets us change this setting by means of configuration. To make the “/” URL point to the /lang/en homepage, open “src/config/scrivito.js” and add the homepage key to the call to Scrivito.configure, like so:

src/config/scrivito.js
import * as Scrivito from "scrivito";

const config = { 
  homepage: () => Scrivito.Obj.getByPath('/lang/en'),
  tenant: process.env.SCRIVITO_TENANT,
};

if (process.env.SCRIVITO_ORIGIN) {
  config.origin = process.env.SCRIVITO_ORIGIN;
}

Scrivito.configure(config);

Now, if you navigate to localhost:8080/scrivito/, you should see the new /lang/en homepage!

Adding another language-specific homepage

Now that everything required for handling more than one homepage is in place, let’s create another Homepage object and set its path to /lang/de (use the two-letter language identifier you need). Again, make sure the working copy containing the /lang/en objects is selected and in editing mode, and the context in the browser console is scrivito_application. Here we go:

Scrivito.getClass('Homepage').create({ _path: '/lang/de' }).id()

That’s all! We’re outputting the ID of the new homepage because we don’t have a language switch yet but still want to be able to view the page in the browser. Just paste the ID to the address line, so that the URL path looks like “/scrivito/32909cd5c076acfe”. Of course, the empty homepage needs to be configured via its page properties later on. To add a subpage to it, use the blue navigation handle at the top right, or select “Add subpage” from the page menu.

Make the new homepages known to the app

Let’s provide the logic for determining the right language-specific homepage as a replacement for the no longer existing root object, so that the navigation and other features based on the former root object become functional again.

In a standard Scrivito-based app, the homepage is determined by means of Scrivito.Obj.root(). To change this, we’ll provide a helper method for finding the language-specific homepage to use instead, getRoot(), and replace all occurrences of Scrivito.Obj.root() with it.

Create a file, “getRoot.js”, in the “src/utils” subfolder of the app’s project directory and add the following to it:

src/utils/getRoot.js
import * as Scrivito from 'scrivito';

function getRoot() {
  const currentPage = Scrivito.currentPage();
  if (!currentPage) { return; }

  const path = currentPage.path();
  if (!path) { return; }
  
  let language = '/lang/en';
  if (path.startsWith('/lang/')) {
    language = path.substr(0, 8);
  }
  return Scrivito.Obj.getByPath(language);
}

export default getRoot;

As you can see, the homepage object associated with the currently displayed page is determined via the first path components of Scrivito.currentPage(). If, for example, the path of the current page reads /lang/en/product, the homepage path can be derived by stripping everything after the first eight characters, resulting in /lang/en in this case. Of course, this is a rather rudimentary approach that would have to be refined to work with variable-length path components.

Next, in your app, wherever Scrivito.Obj.root() is used, import “/src/utils/getRoot” (using a relative path) and replace Scrivito.Obj.root() with getRoot(), for example in:

src/Components/Navigation/Nav.js
...
import getRoot from '../../utils/getRoot.js';
...
//      parent={ Scrivito.Obj.root() } becomes
        parent={ getRoot() }

There’s about a dozen occurrences in the files in the “src” subfolders to which this change needs to be applied. Most of these reference the homepage for their respective attribute values, i.e. navigation, footer, cookie consent banner, etc. If you extended the Example App, make sure to replace Scrivito.Obj.root() in your own code as well.

Providing a language switch

Having a language switch is a must with multi-language websites. How else – if not with such a switch – could visitors select their preferred language? So let’s create a simple React component for this purpose. Since the switch is going to be part of the navigation, we’ll place the component file in “src/Components/Navigation”.

The component is a function that first finds the homepages by searching for pages based on the Homepage object class. Then it iterates over the result and renders for each homepage the substring of the path that indicates the language (en, de) and links it to the homepage object. Again, this is very basic to keep it simple; no fancy styling is applied, no flag icon shown, etc.

src/Components/Navigation/LanguageSwitch.js
import * as React from 'react';
import * as Scrivito from 'scrivito';
import getRoot from '../../utils/getRoot';

function LanguageSwitch() {
  const homepages = [...Scrivito.getClass('Homepage').all()];
  return (
    <ul className="nav navbar-nav">
      { homepages.map(homepage =>
        <li key={homepage.id()}>
          <Scrivito.LinkTag to={ homepage }>
            { homepage.path().substr(6, 2) }
          </Scrivito.LinkTag>
        </li>
      ) }
    </ul>
  );
}

export default Scrivito.connect(LanguageSwitch);

Now put the LanguageSwitch component into action by adding it to the navigation, i.e. rendering it in the FullNavigation component:

src/Components/Navigation/FullNavigation.js
// Other imports
import LanguageSwitch from './LanguageSwitch';
// …

render() {
// …
  <Collapse isOpen={ this.state.expanded } navbar={ true }>
    <div className="navbar-collapse">
      <LanguageSwitch />
      <Nav closeExpanded={ this.closeExpanded } expanded={ this.state.expanded } />
    </div>
  </Collapse>
// …
}

All of a sudden, we’re done, at least with the basic functionality! :) Clicking the Scrivito logo at the top left (or where the logo is supposed to be) should now open the homepage of the language-specific site you’re on. Add a couple of subpages to the second site you created, and see for yourself. Keep in mind that each homepage lets you provide individual site settings (e.g. for the logo).

Determining the initial homepage dynamically

The first page a visitor sees when entering your site at the “/” URL is determined by the homepage callback that can be provided via Scrivito.configure. Above, in the “Changing the default homepage” section, we’ve simply set the homepage to the CMS object whose path is /lang/en. As a consequence, in our case, all visitors are initially served the English-language homepage. Also, the top node of the hierarchy browser in Scrivito’s sidebar always starts at the /lang/en CMS object. To improve this, we require a criterion by which we can set the homepage dynamically, for example:

  • The user’s preferred language. You could determine this language using an algorithm around navigator.languages and navigator.language.
  • A URL path component, e.g. “/en…”, “/de…” and so on. For this, the routing must have been customized accordingly. See Customizing the Routing for details.
  • A subdomain, e.g. “en.example.com” and “de.example.com”.

Whatever method you choose, it’s the homepage callback that makes it possible to define the CMS object behind the “/” URL, so let’s take a closer look at the subdomain approach. For the purpose of this tutorial, we’ve placed the callback function in a dedicated file and called it getHomepage:

src/utils/getHomepage.js
import * as Scrivito from "scrivito";

function getHomepage() {
  if (window.location.hostname.startsWith("de.")) {
    return Scrivito.Obj.getByPath("/lang/de");
  }
  return Scrivito.Obj.getByPath("/lang/en");
}

export default getHomepage;

As you can see, the homepage is the /lang/en object unless the hostname starts with “de.”, which can be extended as needed. Next, make the homepage callback call getHomepage:

src/config/scrivito.js
import * as Scrivito from "scrivito";
import getHomepage from "../utils/getHomepage.js";

const config = {
  homepage: () => getHomepage(),
  tenant: process.env.SCRIVITO_TENANT,
};

if (process.env.SCRIVITO_ORIGIN) {
  config.origin = process.env.SCRIVITO_ORIGIN;
}

Scrivito.configure(config);

Afterwards, you will see that changing the subdomain causes the homepage to be switched accordingly, and the hierarchy browser’s root reflects this change, too.

Using subdomains in a local environment

For testing our above homepage callback, we needed a means to use subdomains in the URLs in our local environment. We went for xip.io, a custom DNS that resolves hostnames like en.127.0.0.1.xip.io to the given IP address. Several such services exist next to this one, e.g. nip.io and sslip.io.

To use such a service, the hostnames need be added to the devServer entry in your project’s webpack configuration:

webpack.config.js
…
    devServer: {
      allowedHosts: [".127.0.0.1.xip.io"],
      host: "127.0.0.1.xip.io",
    …
    },
…

The leading dot in hostnames specified in allowedHosts means that any valid subdomain name will be accepted.

These hostnames also need to be made known to your Scrivito dashboard. Here, too, you can have any subdomain accepted; simply use the commonly known wildcard, *.

Adding a language filter to the Content Browser

Wouldn’t it be practical if one could restrict the pages seen in Scrivito’s Content Browser to a specific language so that editors can focus on the content they are actually working with?

Fortunately, the filter list on the left-hand side of the Content Browser can be configured to allow just that, so let’s extend the default filter list by a language filter.

The Content Browser can be configured by calling Scrivito.configureContentBrowser and passing in a configuration object. In case of the Example App, this is done in “src/config/scrivitoContentBrowser.js”. We can provide our filter there by adding its definition to the return value of the defaultFilters function:

src/config/scrivitoContentBrowser.js
…
function defaultFilters() {
  return {
    lang: {
      title: "Language",
      type: "radioButton",
      expanded: true,
      field: "_path",
      operator: "startsWith",
      options: {
        de: {
          title: "German",
          value: "/lang/de",
        },
        en: {
          title: "English",
          value: "/lang/en",
        },
      },
    },
    _objClass: {
…

Note that the above definition, lang, represents only one of the many filtering options that are available. For details, see the API documentation.

For the purpose of this tutorial, FOV (field-operator-value) constraints are used to restrict the items displayed in the Content Browser to one of the two language-specific path hierarchies, /lang/de (“German”) and /lang/en (“English”). As you can see on the screenshot, a third option has been added by Scrivito, “All”, which is selected as a default when the Content Browser is opened causing the filter to be switched off.

To preselect one of the language filters, you can add the selected key to the radio button options and even use the same language detection mechanism as in the getHomepage function above, like so:

src/config/scrivitoContentBrowser.js
…
      options: {
        de: {
          title: "German",
          value: "/lang/de",
          selected: window.location.hostname.startsWith("de."),
        },
…

You will notice that, as expected, the selected filters are combined with “and”, meaning that it is possible, for example, to have only “German” “Standard pages” displayed. By the way, if you have a larger amount of language-specific sites and want to enable editors to select more than one language filter, you can use the checkbox filter type.

For making assets (e.g. images) language specific, provide them with a corresponding attribute and extend the filters accordingly.

What’s next?

Currently, a new homepage can only be created via the console. To let editors create language-specific homepages underneath the /lang node, you could build a custom component for adding a homepage either in place or via the properties of an existing homepage.

Other aspects worth thinking about:

  • To fine tune the language switch and make language-specific settings accessible to editors, add attributes for the language name, the image to be used, and similar properties to the Homepage class definition and the editing configuration. You could also highlight the currently active language.
  • Consider assigning permalinks to the homepages to make their URL speaking and more SEO friendly (e.g. “example.com/de” instead of “example.com/32909cd5c076acfe”).
  • A language-aware site search can easily be implemented by restricting the search results to the active language path.