Translating Laws of UX

Reading Time
11 mins
Translating Laws of UX

I’ve been working on an Arabic version of Laws of UX in partnership with my friends at The Valve Studio. It’s been an opportunity for me to learn more about internationalization and paves the way for future translations of the website. In this article, I document the design decisions that were made and the technical details that I encountered in the process.


Let’s begin by outlining the underlying technology in use in the Laws of UX. The website is powered by Hugo and it takes advantage of the built-in asset pipeline called Hugo Pipes. Hosting is handled with Netlify, which connects to a Github repo and deploys when updates are pushed.

I’ve experimented with a few static site generators but Hugo has remained my go-to for a few reasons. Firstly, it’s blazing fast. It clocks in at around <1ms per page, so the average site builds in less than a second. This means no more waiting for your pages to build each time you save. Secondly, it comes with an asset pipeline baked right in. When it comes to simplicity, there’s something nice about not needing to configure a build process for each project that leans heavily on a build tool like Grunt or Gulp. It also comes with the ability to spin up a local server and live-reload that server when changes are detected. This is typically something I’d have to set up with a task runner, but in Hugo, it comes baked in.

If you’re interested in learning more about how I use Hugo’s built-in asset pipeline to do things like transform templates to HTML, process PostCSS to CSS, or generate responsive images, check out this article where I detail my Hugo workflow.

Lastly, Hugo natively supports the creation of websites with multiple languages side-by-side. It is this feature that we’ll talk about at length in this article.


The first step is configuring the website to support multiple languages. Hugo enables you to define each language that you intend to support via the global config file (config.toml). The first thing you’ll need to do here is set the default language, followed by settings for each language. There are plenty of useful settings you can define but the most common are the language name, base URL, weight (how Hugo prioritizes languages), and the description that will show as the <meta name="description" content="..."> of the site. You can learn more about Hugo’s multilingual language configuration here.

# Default language
defaultContentLanguage = 'en'

# Mutli-lang Settings
		languageName = "English"
		baseURL = "/en/"
		weight = 1
		languageName = "Arabic"
		baseURL = "/ar/"
		languagedirection = 'rtl'
		weight = 2
		description = "قوانين تجربة المستخدم، هي عبارة عن مجموعة من أفضل الممارسات التي يمكن للمصممين أخذها بعين الاعتبار عند إنشاء واجهة مستخدم."

Directory Structure

Next up is deciding how you will structure your files to support multiple languages. There are two ways to manage your content translations via Multilingual Mode: translation by filename or translation by content directory. Both methods ensure each page is assigned a language and is linked to its counterpart translations. For the Arabic version of Laws of UX, I took the translation by filename approach so that translated versions of each page could live in the same folder side-by-side, making managing and updating individual files easy.

Laws of UX content directory with language-specific files
Laws of UX content directory with language-specific files

HTML attributes

The HTML attributes necessary for enabling translations are pretty minimal. I first set the lang property on the HTML element for the current language. Secondly, it’s necessary to indicate what writing mode of the website via the dir attribute. In the case of Arabic, the value of this attribute is rtl, which means ‘right to left’.

  lang="{{ .Language.Lang }}" 
  dir="{{ .Language.LanguageDirection | default `ltr` }}">

By default, the direction of text will be left-to-right (ltr) and dynamically insert the language direction (.LanguageDirection) if available based on the setting in the site’s configuration file.


I had the fortune to work with the wonderful team at The Valve Studio to translate all text content from the website into Arabic. English text strings were documented side-by-side with their Arabic equivalent in Google Sheets and separated by page or general (e.g. navigation labels, copyright info, etc).


The next step was to implement the translated content. Content that is specific to a single page can easily be implemented via a language-specific variation of that page (e.g. and index.en.html). On build, Hugo will render any page that has a language abbreviation in a separate language-specific directory. This results in the Arabic version of the page residing at /ar/[name-of-page] while the English version is at /en/[name-of-page].

Translation document for Laws of UX
Translation document for Laws of UX

Global strings take advantage of the i18n (an abbreviation of Internationalization) configuration files. This enables specific strings to be represented as tags which are then transformed into their translated value if the language-specific configuration file exists. As long as the language file exists in the i18n directory (represented by the language abbreviation) with the proper ID, the tag gets replaced with the value of that ID when Hugo builds the page.

i18n translation configuration files
i18n translation configuration files


Fonts play a critical role in the readability of any content and this is especially true for multilingual websites. I’ve always used IBM Plex as the primary font family for Laws of UX because it’s well crafted, it’s a great fit for the art direction of the website, and it features a diversity of styles and weights. Fortunately, this type of family also supports a multitude of other languages, including Arabic. This means that the specific glyphs needed to properly render Arabic text are baked right into the fonts, ensuring there are never any weird or missing characters when displaying Arabic text with a font meant for languages such as English.

To load the necessary fonts for each supported language, I first load both English and Arabic versions of fonts that I need using the standard @font-face declaration. Next, I store references to the appropriate fonts for each language in CSS variables that get used throughout the codebase. This ensures that only the necessary fonts are loaded, depending on which version of the website is being viewed.

:root:lang(en) {
  --base-font-family: 'IBM Plex Sans', Arial, Sans-Serif;
  --secondary-font-family: 'IBM Plex Mono', monospace;

:root:lang(ar) {
  --base-font-family: 'IBM Plex Sans Arabic', Arial, Sans-Serif;
  --secondary-font-family: 'IBM Plex Mono', monospace;

html {
  font-family: var(--base-font-family);

Logical Properties

CSS logical properties were used to avoid writing duplicative CSS to make adjustments for elements when switching from a left-to-right to a right-to-left layout. Everywhere margin, padding or position properties were being used to provide spacing, separate elements from one another or position them got refactored to be direction-agnostic.

CSS logical properties being used for banner graphic margin/padding
CSS logical properties being used for banner graphic margin/padding

Take for example the banner graphics for each page. These graphics originally were styled using margin and padding that was specific to ltr languages (margin-left, margin-right, padding-left, padding-right). Leveraging the power of logical properties enabled the website to adapt to whichever direction the text needed to flow simply by defining styles in a way that adapts to the selected language (margin-block, margin-inline, padding-block, padding-inline). This approach not only results in less CSS but paves the way for future translations with no CSS adjustments needed.

Language Selector

Users can manually change the language via a language selector, which is located in the header. The language selector is comprised of a toggle button and a list of available languages which link to the respective directory for the translated content.

Laws of UX language switcher
Laws of UX language switcher


The first step is to construct the language selector structure, which consists of a <button> element to toggle an adjacent <ul> that contains a link to the homepage for each translation. These elements are wrapped in a <nav> element that provides some additional semantic information about what the component does.

<div class="lang-switcher" id="language">
  <!-- Toggle -->
  <button class="lang-switcher__toggle button button--toggle button--min" aria-expanded="false" aria-controls="lang-list" data-toggle>
    <!-- Translation icon -->
    <span class="button__icon button__icon--left">
      {{ partial "components/icon" (dict "name" "language") }}
    <!-- Label -->
    <span class="lang-switcher__toggle-label" aria-label="Select language">{{ .Language.LanguageName }}</span>
    <!-- Arrow -->
    <span class="button__icon button__icon--right button__icon--default" aria-hidden="true">
      {{ partial "components/icon" (dict "name" "arrow-drop-down" "size" "16") }}
    <span class="button__icon button__icon--right button__icon--active" aria-hidden="true">
      {{ partial "components/icon" (dict "name" "arrow-drop-up" "size" "16") }}

  <!-- List -->
  <ul class="lang-switcher__list" id="lang-list">
    {{ range $.Site.Home.AllTranslations }}
    <li class="lang-switcher__list-item" tabindex="-1" {{ if eq $.Language.LanguageName .Language.LanguageName }}aria-checked="true"{{ end }}>
      <a href="{{ .Permalink }}" class="{{ if eq $.Language.LanguageName .Language.LanguageName }}active{{ end }}"
        aria-label="{{ i18n .Language.LanguageName }}">
        {{ .Language.LanguageName }}
    {{ end }}


There are some accessibility considerations here:

  • The toggle is a <button> component that includes ARIA attributes to provide accessibility information: whether the list is expanded (aria-expanded) and the content it controls (aria-controls).
  • The label for the toggle button is provided an aria-label to indicate what the list is.
  • The list is provided an id that associates it with the toggle button.
  • The list has hidden attribute to hide it by default.
  • An ARIA attribute is set on the currently active language to indicate to screen readers (aria-checked="true").
  • Insert translated strings for languages via i18n tags to ensure they read correctly for users.


Next up is to apply the appropriate styling to the language selector. The gist here is to use the <button> element to toggle the visibility of the language list, which gets displayed as a dropdown. The button I’m using to toggle the visibility of the language menu gets reusable styling that’s applied to other button components throughout the website (.button .button--toggle). This ensures more modularity, reduces code repetition, and increases maintainability.

We ensure the language list width is sized according to the longest language name via width: min-content. Additionally, the visibility of the language list is toggled via the class .is-active, which is applied to the list element via Javascript once the toggle button is clicked/tapped.

The following is the CSS required to make this work with unrelated styling omitted:

 * Parent

.lang-switcher {
  position: relative;
  transition: opacity var(--base-duration) var(--base-timing);

 * Elements

.no-js .lang-switcher__toggle {
  display: none;

.lang-switcher__list {
  position: absolute;
  top: 0;
  inset-inline-end: 0;
  width: min-content;
  list-style: none;

.js .lang-switcher__list {
  display: none;
  top: 100%;

 * States

.is-transitioning .lang-switcher {
  opacity: 0;
} {
  display: block;


The last part of the language selector listening for click/tap events on the toggle element. Once this is detected, the aria-expanded attribute is updated on the toggle element and the corresponding language list is revealed or hidden via the hidden attribute. Additionally, an active class is applied to the nextElementSibling to the toggle button, which is the language list.

const activeClass = 'is-active';

const toggleContent = (e) => {
  let expanded ='aria-expanded') === 'true' || false;'aria-expanded', !expanded);
  let content =;

document.addEventListener('click', function() {
  if ('[data-toggle]')) {
}, false);


Hugo creates a separate directory for each language, which lives at the root of the files generated on build. I can define how I want the server to handle redirects within the netlify.toml file, which automatically gets moved to the root directory. Netlify will then use this config file to write redirects and ensure visitors get routed to the correct directory based on the language preference they’ve set in their browser. In the case there is no set preferred language, the server will default to English.

# Redirect users with Arabic language preference
  from = "/*"
  to = "/ar/:splat"
  status = 301
  force = false
  conditions = {Language = ["ar"]}

# Redirect users with English language preference
  from = "/*"
  to = "/en/:splat"
  status = 301
  force = false
  conditions = {Language = ["en"]}

# Redirect users with no language preference (default)
  from = "/*"
  to = "/en/:splat"
  status = 301
  force = false

Netlify’s redirect rules accept several options to customize how the paths are matched and redirected. In addition to language preference, you can redirect users based on their location (country), role, or cookie presence. You can read more about Netlify’s redirect options here.


Building the Arabic version of Laws of UX was quite a fun learning opportunity for me. I’m incredibly grateful to have the team at The Valve Studio collaborate with on this project and make the website available to even more people around the world! I’m looking forward to building upon the foundations established with this project to include even more translations in the future.

See Laws of UX Arabic


Demystifying Internationalization with Hugo (Hugo Conf) ↗

A walk through of the technical details behind the new Arabic edition of Laws of UX which explores Hugo’s Multilingual Mode, how to translate content based on i18n configuration, and using CSS logical properties to control layout.