Creating Multi-Language Websites

Utilizing the flexibility of the SDK, it is possible to build many and varied multi-language websites. In this guide, we would like to give you an idea of how separate language sites served by a single application can be implemented. We're going to endow editors with the power to structure each individual language site as they see fit.

Extending the website structure

In the CMS, the structure of a website is represented by the path of every individual page. With single websites, the root object (whose path is /) usually represents the homepage, and this is how Scrivito is configured by default. Since one cannot have more than one root object, we'll place the homepages on the level underneath the root object and name them according to their country code. For example:

Copy
/en # English
/es # Spanish
/fr # French

This allows us to maintain any number of homepages in one CMS. Underneath the homepages, the websites will look just like any other Scrivito site. We will use the guide for integrating Scrivito as a starting point for this guide. However, you can turn any other website you developed with Scrivito into a multi-language site.

Preparing the Scrivito application

Assuming that the current content in the CMS should become one of the future sites, we'll move the current root object and all its children from "/" to "/en" first. After that, we'll adapt the place Scrivito looks for the default homepage. 

So, first of all, we need to specify a homepage, which will be the starting page of the application and the parent page of our language-specific sites. Since it must be possible to easily differentiate this top-level page from regular pages, we are going to create a model for it:

Copy
rails g scrivito:page Homepage

The Scrivito “install” migration, that you ran when following the integration tutorial, creates a root page whose type is “Page.” This is the object we want to be our Homepage. On the Rails console we can change the underlying _obj_type like this:

Copy
[1] pry(main)> Scrivito::Workspace.current = Scrivito::Workspace.create(title: 'My Working Copy')
[2] pry(main)> Page.find_by_path('/').update(_obj_class: Homepage)
[3] pry(main)> Scrivito::Workspace.current.publish
If you don't have any other pages so far, let's create two example pages to work with first. Click the list icon next to the Home button and then 'Add page'. You can give the page a title by changing the headline. Do the same thing again for the second page.

Navigation list icon

Now let's create a migration for moving these pages sitting underneath the root Homepage to “/en”:

Copy
rails g scrivito:migration prepare_for_multi_language

Navigate to the new migration file in the “scrivito/migrate” directory, and change its contents to: 

Copy
class PrepareForMultiLanguageMigration < Scrivito::Migration
  def up
    Obj.all.to_a.each do |obj|
      if obj.path.present? && !obj.path.start_with?('/_internal')
        obj.update(_path: "/en#{obj.path}")
      end
    end
  end
end

Run this migration using bundle exec rake scrivito:migrate. The migration is applied to the "Migration Working Copy", which is automatically created on demand.

Now, in your browser, select the “Migration Working Copy” to see the migration results. You will get an error saying that the root CMS object was not found. However, if you take a look at the working copy in the console, you will notice that the paths of all objects start with ”/en“ now:

Copy
[1] pry(main)> Scrivito::Workspace.use('Migration Working Copy')
[2] pry(main)> Obj.all.map(&:path).compact
=> ["/en/0f9af53200fe1ee7", "/en", "/en/59ca7a56e35fc4e4", "/_internal/migration-store"]

To fix this homepage issue, we need to make the new location of our homepage known to Scrivito. So, first open the “config/initializers/scrivito.rb” file and add the following configuration to it:

Copy
# Define the CMS object to deliver if / is requested

Scrivito.configure do |config|  
  # …

  config.choose_homepage do |request|
    Homepage.find_by_path('/en') || Obj.find_by_path('/')
  end

  # …
end

Note that we have added || Obj.find_by_path('/') to the code above and below as a way to prevent errors in the browser while implementing these changes. This will allow you to switch to your working copy and see the changes and functionality before publishing. It is recommended, after publishing your "Migration Working Copy", to remove this code from each section.

Additionally, we require a method to identify the root of a given Obj. Open “app/models/obj.rb” and add the following method to the file: 

Copy
class Obj < Scrivito::BasicObj
  # …
  
  def self.root
    Homepage.find_by_path('/en') || Obj.find_by_path('/')
  end

  def root
    ancestors.reverse.find { |obj| obj.is_a?(Homepage) } || self.class.root
  end
  
  # …
 end

The root class method lets Scrivito determine the default homepage CMS object. This is the method that reenables visiting root. The second one, the root instance method, returns the homepage of a given CMS object. We use the ancestors method to determine the CMS objects nearer to the root and locate the one whose class is “Homepage.” If none exists, the default homepage is returned. All that's left to do now is to override the root instance method of “Homepage” objects to make them identify themselves as homepages. Add the following method to "app/models/homepage.rb":

Copy
class Homepage < Obj
  # …
  
  def root
    self
  end
  
  # …
 end

Restart your Rails server as changes to an initializer only take effect after restarting. Then reload the page displayed in your browser to check whether the error has disappeared. Then publish the migration working copy, either via the UI, or using bundle exec rake scrivito:migrate:publish in the console.

Finally, we'll utilize our new homepage method in the CmsController. So, open “app/controllers/cms_controller.rb” and change its contents to:

Copy
class CmsController < ApplicationController
  include Scrivito::ControllerActions

  private

  def root
    @obj.root
  end
  helper_method :root
end

Adding another language

To utilize the new capabilities of the application, we are going to add a second language to the website, "German" in this case. Feel free to choose any other language, the process is largely the same for all languages.

To begin with, let's add the homepage using a migration:

Copy
rails g scrivito:migration add_next_language

Change the contents of the new migration file to:

Copy
class AddNextLanguageMigration < Scrivito::Migration
  def up
    Homepage.create(_path: '/de')
  end
end

Run bundle exec rake scrivito:migrate to add the new homepage and its search result page to the website. Currently, the only way to visit the new homepage is to determine its id and paste it to the browser address line. So, open the console and execute the following two commands:

Copy
[1] pry(main)> Scrivito::Workspace.use('Migration Working Copy')
[2] pry(main)> Homepage.find_by_path('/de').id
=> "f8c57dd4c443e046"

Now, go to the working copy in your browser, copy/paste the id, and visit the corresponding page in your browser. If your Rails app is running on the default port 3000, this would be http://localhost:3000/f8c57dd4c443e046 (your id will be different, of course). As expected, you will see a blank page for your new language. However, searching the ID of a homepage using the console is not the most comfortable way to switch languages. Let's build a simple language switcher.

Providing a language switcher

The first thing needed is a name to display in the language switcher. For now, we can just use the language code contained in the path. Edit the homepage model located at “app/models/homepage.rb” and add the new method, “code,” that determines the language code:

Copy
class Homepage < Obj
  # …

  def code
    path.split('/').last
  end
  
  # …
end

This simply extracts the last path component, in our case "en" for "/en" and "de" for "/de." Using this method, we can have links displayed that point to the two pages in our navigation. Insert the following code snippet right above the search form in the “app/views/layout/_navigation.html.erb” file. Note that this example uses bootstrap classes; follow our directions for adding bootstrap to see the active button:

Copy
<!-- … -->

<div class='navbar-right'>
  <ul class="nav navbar-nav">
    <%= Homepage.all.each do |homepage| %>
      <%= content_tag :li, class: @obj.root == homepage ? 'active' : nil do %>
        <%= link_to homepage.code, scrivito_path(homepage)  %>
      <% end %>
    <% end %>
  </ul>
</div>

<!-- … -->

This renders the country codes of all homepages in the navigation and marks the homepage of the current CMS object as active. 

Setting up I18n

Rails comes with its own internationalization framework, I18n. We will configure it here and, as an example, have the texts on the search form translated into the required language. The first step is to configure the current locale. Edit the CmsController once again and add the following code to it: 

Copy
class CmsController < ApplicationController
  # …

  before_action :set_i18n_locale

  def set_i18n_locale
    I18n.locale = @obj.root.code
  end

  # …
end

This changes the locale for each request based on the homepage code we've fortunately chosen to match the ISO language code. Now, let's add a submit button without any functionality, just to see the translation process in action. Integrate the translations for the submit button into “app/views/layouts/application.html.erb.” Please add the following at the end of the navbar:

Copy
<button type="submit" class="btn btn-default"><%= t('nav.search.submit') %></button>

This breaks our layout as the translations are still missing. Let's add them right away by changing “config/locales/en.yml” so that it contains the following:

Copy
en:
  nav:
    search:
      submit: "Submit"

Provide the translation file for our new language, “config/locales/de.yml” in the case of German:

Copy
de:
  nav:
    search:
      submit: "Abschicken"

For a language other than German, please change the first line, de:, to the corresponding language code and adapt the localizers accordingly. You will need to restart the Rails server afterwards so that it picks up the new file. The English-language search form should then look the same as before, the form for the new language, however, should now be translated.

Introducing a language attribute

We're going to add an attribute named language to all our page and resource models and set the default attribute values to the country code of the current homepage. This enables us to filter the CMS objects by their language.

We'll accomplish this for all future CMS objects by providing a module, “app/models/concerns/obj_attributes.rb,” and including it in the “Obj” class, which is the parent class of all CMS object classes. Here's the contents of the module:

Copy
module ObjAttributes
  extend ActiveSupport::Concern

  included do
    attribute :language, :enum, values: ["de", "en"]

    default_for :language do |attr|
      if path = attr[:_path]
        homepage_path = path.split('/')[0..1].join('/')
        Homepage.find_by_path(homepage_path).code rescue nil
      end
    end
  end
end

Alternatively, you can include the module in every model class file to equip the CMS objects based on one of these classes with the language attribute. In our case, these classes are "app/models/homepage.rb", "app/models/image.rb", and "app/models/page.rb."

For CMS objects that have a path, the code above automatically sets their language to the language of the corresponding homepage on creation. However, some CMS objects such as images or PDF files are not meant to be part of the visible website structure and therefore don't have a path. For those objects, the language attribute needs to be set by other means. A simple solution is to enable editors to set the language of images by extending the details view so that the language attribute becomes editable. For this, add the following piece of code at the end of the details template for images:

Copy
<%= scrivito_details_for 'Language' do %>
  <%= scrivito_tag :div, @obj, :language %>
<% end %>

If your application is equipped with other resource types, just extract the code to a partial and render it in their details templates.

Now, everything has been set up to work in the future. However, the language of existing content has not been set yet. We can change that by writing a script that sets the language of all the CMS objects that have a path. Run rails g scrivito:migration fill_language_attribute to generate a migration for this and change its contents to:

Copy
class FillLanguageAttributeMigration < Scrivito::Migration
  def up
    Homepage.all.each do |homepage|
      Obj.where(:_path, :starts_with, homepage.path).each do |obj|
        obj.update(language: homepage.code)
      end
    end
  end
end

This code iterates over all the homepages, finds all their CMS objects, and sets their language attribute to the language code of their homepage.

Restricting search results

If you have added a search box to your application following our guide, you might want to improve the search so that it only considers content of a given language. Change the “SearchResultPageController” code to:

Copy
class SearchPageController < CmsController
  def index
    @query = params[:q] || ''
    if @query.present?
      @hits = Obj.where(:*, :contains_prefix, @query).and(:language, :equals, @obj.root.code).take(10)
    else
      @hits = []
    end
  end
end

The line .and(:language, :equals, @obj.root.code) restricts the search results to the pages of the currently selected language.

That's it!

You've got a basic multi-language Scrivito website now! Needless to say that you can make it even better by improving your editors’ editing experience. You might, for example:

  • Use the Content Browser and its ability to apply presets to automatically set the language of uploaded resources.
  • Enable editors to specify how a language is displayed in the switcher by adding a corresponding attribute to “Homepage” objects.
  • Write a custom page menu entry that lets editors copy individual pages to another language.