diff --git a/Dockerfile b/Dockerfile
index 71f0b396..1a69fbff 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,5 +23,21 @@ COPY resources/keyman-site.conf /etc/apache2/conf-available/
 RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
 RUN chown -R www-data:www-data /var/www/html/
 
+# Because the base Docker image doesn't include locales, install these to generate locale files.
+# gettext needed to compile .po files to .mo with msgfmt
+RUN apt-get update && apt-get install -y \
+    locales \ 
+    gettext
+
+# Install PHP-extension gettext for localization at runtime
+RUN docker-php-ext-install gettext
+RUN docker-php-ext-enable gettext
+
+# Only enable en_US locale in /etc/locale.gen
+# PHP will use textdomain() to specify "localization" .mo files
+RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
+    && dpkg-reconfigure --frontend=noninteractive locales \
+    && update-locale
+
 COPY --from=composer-builder /composer/vendor /var/www/vendor
 RUN a2enmod rewrite headers; a2enconf keyman-site
diff --git a/_includes/includes/template.php b/_includes/includes/template.php
index 57e87a81..27dd1361 100644
--- a/_includes/includes/template.php
+++ b/_includes/includes/template.php
@@ -3,6 +3,7 @@
 
   // *Don't* use autoloader here because of potential side-effects in older pages
   require_once(__DIR__ . '/../2020/Util.php');
+  require_once(__DIR__ . '/../locale/Locale.php');
   require_once(__DIR__ . '/../../_common/KeymanVersion.php');
   require_once(__DIR__ . '/../2020/templates/Head.php');
 
diff --git a/_includes/includes/ui/keyboard-details.php b/_includes/includes/ui/keyboard-details.php
index cc4e91b6..e12a2fda 100644
--- a/_includes/includes/ui/keyboard-details.php
+++ b/_includes/includes/ui/keyboard-details.php
@@ -4,6 +4,7 @@
   require_once('includes/template.php');
   require_once('includes/playstore.php');
   require_once('includes/appstore.php');
+  require_once __DIR__ . '/../../../keyboards/session.php';
 
   use \DateTime;
   use \Keyman\Site\com\keyman\KeymanWebHost;
@@ -203,7 +204,7 @@ protected static function LoadData() {
           self::$minVersion = isset(self::$keyboard->minKeymanVersion) ? self::$keyboard->minKeymanVersion : $stable_version;
           self::$license = self::map_license(isset(self::$keyboard->license) ? self::$keyboard->license : 'Unknown');
         } else {
-          self::$error .= "Error returned from ".KeymanHosts::Instance()->api_keyman_com.": $s\n";
+          self::$error .= Locale::_s('Error returned from %1$s: %2$s\n', $KeymanHosts::Instance()->api_keyman_com, $s);
           self::$title = 'Failed to load keyboard package ' . self::$id;
           header('HTTP/1.0 500 Internal Server Error');
         }
@@ -608,7 +609,9 @@ protected static function WriteKeyboardDetails() {
                     $s = @file_get_contents(KeymanHosts::Instance()->SERVER_api_keyman_com.'/keyboard/' . rawurlencode($name));
                     if ($s === FALSE) {
                       echo "<span class='keyboard-unavailable' title='This keyboard is not available on ".
-                        KeymanHosts::Instance()->keyman_com_host."'>$hname</span> ";
+		        Locale::_s('This keyboard is not available on %1$s',
+                        KeymanHosts::Instance()->keyman_com_host) .
+                        "'>$hname</span> ";
                     } else {
                       echo "<a href='/keyboards/$hname$session_query_q'>$hname</a> ";
                     }
diff --git a/_includes/locale/Locale.php b/_includes/locale/Locale.php
new file mode 100644
index 00000000..eaf6cade
--- /dev/null
+++ b/_includes/locale/Locale.php
@@ -0,0 +1,98 @@
+<?php
+  declare(strict_types=1);
+
+  namespace Keyman\Site\com\keyman;
+
+  class Locale {
+    public const DEFAULT_LOCALE = 'en';
+
+    public const CROWDIN_LOCALES = array(
+      'en',
+      'es-ES',
+      'fr-FR'
+    );
+
+    // xx-YY locale as specified in crowdin %locale%
+    private static $currentLocale = Locale::DEFAULT_LOCALE;
+
+    /**
+     * Return the current locale. Fallback to 'en'
+     * @return $currentLocale
+     */
+    public static function currentLocale() {
+      return Locale::$currentLocale;
+    }
+
+    /**
+     * Validate and override the current locale
+     * @param $locale - the new current locale (xx-YY as specified in crowdin %locale%)
+     */
+    public static function overrideCurrentLocale($locale) {
+      if (Locale::validateLocale($locale)) {
+        Locale::$currentLocale = $locale;
+      }
+    }
+
+    /**
+     * Validate $locale is an acceptable locale.
+     * Using xx-YY as specified in crowdin %locale%
+     * @param $locale - the locale to validate
+     * @return true if valid locale
+     */
+    public static function validateLocale($locale) {
+      return in_array($locale, Locale::CROWDIN_LOCALES);
+    }
+
+    /**
+     * Use textdomain to specify the localization file for "localization".
+     * Ignore if locale is "en" or the filename doesn't exist
+     * Filename expected to be "$basename-$locale.mo"
+     * @param $basename - base name of the .mo file to use
+     * @return current message domain
+     */
+    public static function setTextDomain($basename) {
+      // Container uses English locale, and then we use textdomain to change "localization" files
+      setLocale(LC_ALL, 'en_US.UTF-8');
+
+      if (Locale::$currentLocale == Locale::DEFAULT_LOCALE) {
+        return;
+      }
+
+      $filename = sprintf("%s-%s", $basename, Locale::$currentLocale);
+      $fullPath = sprintf("%s/en/LC_MESSAGES/%s.mo", __DIR__, $filename);
+      if(file_exists($fullPath)) {
+        return textdomain($filename);
+      } else {
+        //echo "textdomain $fullPath doesn't exist";
+        return;
+      }
+    }
+
+    /**
+     * Reads localized strings from the specified $domain-locale.po file
+     * for the current locale.
+     * @param $domain - base filename of the .po files (not including -xx-YY locale)
+     */
+    public static function localize($domain) {
+      foreach(Locale::CROWDIN_LOCALES as $l) {
+        if ($l == Locale::DEFAULT_LOCALE) {
+          // Skip English
+          continue;
+        }
+
+        bindtextdomain("$domain-$l", __DIR__);
+      }
+
+      $previousTextDomain = textdomain(NULL);
+      Locale::setTextDomain($domain); 
+    }
+
+    /**
+     * Wrapper to format string with gettext '_(' alias and variable args
+     * @param $s - the format string
+     * @param $args - optional remaining args to the format string
+     */ 
+    public static function _s($s, ...$args) {
+      return vsprintf(_($s), $args);
+    }
+  }
diff --git a/_includes/locale/README.md b/_includes/locale/README.md
new file mode 100644
index 00000000..8fabb137
--- /dev/null
+++ b/_includes/locale/README.md
@@ -0,0 +1,45 @@
+### Setup for Localization
+
+[init-container.sh](../../resources/init-container.sh) contains steps for the Docker container to compile .po files to .mo files which PHP uses for `gettext()`.
+
+If you want to compile the files on your host machine, install `gettext`.
+
+```bash
+sudo apt-get install gettext
+```
+
+### Adding locales
+
+The Docker image has the "en_US.UTF-8" locale enabled in `/etc/locale.gen`
+We'll use `textdomain` to specify filenames for "switching" localization. 
+The filenames will include the `%locale%` as defined in the [crowdin.com project](https://crowdin.com/project/keymancom).
+
+Note: the details below will get refactored to use a Locale.php class
+
+In the example below, the English file `keyboards-en.po` is copied to `keyboards-fr-FR.po` for French.
+
+1. In `/_includes/locale/en/LC_MESSAGES/`
+    * Copy `keyboards-en.po` file and rename to the `keyboards-fr-FR.po`.
+    * Translate/upload the new .po file to crowdin
+    * Convert .po file to .mo with the following
+
+```bash
+msgfmt keyboards-fr-FR.po --output-file=keyboards-fr-FR.mo
+```
+
+(The container handles the msgfmt step in init-container.sh)
+
+2. Add the file to the PHP (path is relative the PHP file)
+
+```php
+bindtextdomain("keyboards-fr-FR", "../_includes/locale");
+```
+
+3. To use French,
+```php
+textdomain('keyboards-fr-FR');
+```
+
+----
+
+For formatted string, use the PHP wrapper [`Locale::_s(msgstr, $args)`](./Locale.php).
diff --git a/_includes/locale/en/LC_MESSAGES/.gitignore b/_includes/locale/en/LC_MESSAGES/.gitignore
new file mode 100644
index 00000000..e1b0b2c1
--- /dev/null
+++ b/_includes/locale/en/LC_MESSAGES/.gitignore
@@ -0,0 +1,2 @@
+# Ignore generated files from msgfmt
+*.mo
diff --git a/_includes/locale/en/LC_MESSAGES/keyboards-en.po b/_includes/locale/en/LC_MESSAGES/keyboards-en.po
new file mode 100644
index 00000000..d2465b03
--- /dev/null
+++ b/_includes/locale/en/LC_MESSAGES/keyboards-en.po
@@ -0,0 +1,85 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+
+# Default English strings for keyboards/index.php
+
+# Page Title
+msgid "Keyboard Search"
+msgstr "Keyboard Search"
+
+# Page Description
+msgid "Keyman Keyboard Search"
+msgstr "Keyman Keyboard Search"
+
+# Keyboard search bar
+msgid "Keyboard search:"
+msgstr "Keyboard search:"
+
+# Search bar placeholder
+msgid "Enter language or keyboard"
+msgstr "Enter language or keyboard"
+
+# Search Button Value
+msgid "Search"
+msgstr "Search"
+
+# Link to start a new keyboard search
+msgid "New search"
+msgstr "New search"
+
+# Search box instruction (Popular keyboards | All keyboards)
+msgid "Enter the name of a keyboard or language to search for"
+msgstr "Enter the name of a keyboard or language to search for"
+
+# Search box link for popular keyboards
+msgid "Popular keyboards"
+msgstr "Popular keyboards"
+
+# Search box link for all Keyman keyboards
+msgid "All keyboards"
+msgstr "All keyboards"
+
+# Search box hint: List header
+msgid "Hints"
+msgstr "Hints"
+
+# Search box hint: Description
+msgid "The search always returns a list of keyboards. It searches for keyboard names and details, language names, country names and script names."
+msgstr "The search always returns a list of keyboards. It searches for keyboard names and details, language names, country names and script names."
+
+# Search box hint: available prefixes to use in the search
+msgid "You can apply prefixes"
+msgstr "You can apply prefixes"
+
+# (keyboards)
+msgid "(keyboards)"
+msgstr "(keyboards)"
+
+# (languages)
+msgid "(languages)"
+msgstr "(languages)"
+
+# (scripts, writing systems) or...
+msgid "(scripts, writing systems) or"
+msgstr "(scripts, writing systems) or"
+
+# (countries) to filter your search results...
+msgid "(countries) to filter your search results. For example"
+msgstr "(countries) to filter your search results. For example"
+
+# Search box hint: example of country search
+msgid "searches for keyboards for languages used in Thailand."
+msgstr "searches for keyboards for languages used in Thailand."
+
+# Search box hint: BCP 47 prefix
+msgid "Use prefix"
+msgstr "Use prefix"
+
+# Seach box hint: BCP 47 language example
+msgid "to search for a BCP 47 language tag, for example"
+msgstr "to search for a BCP 47 language tag, for example"
+
+# Search box hint: BCP 47 language example
+msgid "searches for Tigrigna (Ethiopia)"
+msgstr "searches for Tigrigna (Ethiopia)"
diff --git a/_includes/locale/en/LC_MESSAGES/keyboards-es-ES.po b/_includes/locale/en/LC_MESSAGES/keyboards-es-ES.po
new file mode 100644
index 00000000..ef6416a2
--- /dev/null
+++ b/_includes/locale/en/LC_MESSAGES/keyboards-es-ES.po
@@ -0,0 +1,94 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Crowdin-Project: keymancom\n"
+"X-Crowdin-Project-ID: 740839\n"
+"X-Crowdin-Language: es-ES\n"
+"X-Crowdin-File: /master/keyboards/keyboards.po\n"
+"X-Crowdin-File-ID: 2\n"
+"Project-Id-Version: keymancom\n"
+"Language-Team: Spanish\n"
+"Language: es_ES\n"
+"PO-Revision-Date: 2024-11-11 08:20\n"
+
+# Page Title
+msgid "Keyboard Search"
+msgstr "Búsqueda por Teclado"
+
+# Page Description
+msgid "Keyman Keyboard Search"
+msgstr "Keyman Búsqueda por Teclado"
+
+# Keyboard search bar
+msgid "Keyboard search:"
+msgstr "Búsqueda por teclado:"
+
+# Search bar placeholder
+msgid "Enter language or keyboard"
+msgstr "Ingresar idioma o teclado"
+
+# Search Button Value
+msgid "Search"
+msgstr "Buscar"
+
+# Link to start a new keyboard search
+msgid "New search"
+msgstr "Nueva buscar"
+
+# Search box instruction (Popular keyboards | All keyboards)
+msgid "Enter the name of a keyboard or language to search for"
+msgstr "Introduzca el nombre de un teclado o idioma para buscar"
+
+# Search box link for popular keyboards
+msgid "Popular keyboards"
+msgstr "Teclados populares"
+
+# Search box link for all Keyman keyboards
+msgid "All keyboards"
+msgstr "Todos los teclados"
+
+# Search box hint: List header
+msgid "Hints"
+msgstr "Consejos"
+
+# Search box hint: Description
+msgid "The search always returns a list of keyboards. It searches for keyboard names and details, language names, country names and script names."
+msgstr "La búsqueda siempre devuelve una lista de teclados. Busca nombres de teclados y detalles, nombres de idiomas, nombres de países y nombres de alfabetos."
+
+# Search box hint: available prefixes to use in the search
+msgid "You can apply prefixes"
+msgstr "Puedes aplicar prefijos"
+
+# (keyboards)
+msgid "(keyboards)"
+msgstr "(tescados)"
+
+# (languages)
+msgid "(languages)"
+msgstr "(idiomas)"
+
+# (scripts, writing systems) or...
+msgid "(scripts, writing systems) or"
+msgstr "(guiones, sistemas de escritura) o"
+
+# (countries) to filter your search results...
+msgid "(countries) to filter your search results. For example"
+msgstr "(países) para filtrar los resultados de búsqueda. Por ejemplo"
+
+# Search box hint: example of country search
+msgid "searches for keyboards for languages used in Thailand."
+msgstr "busca teclados para los idiomas utilizados en Tailandia."
+
+# Search box hint: BCP 47 prefix
+msgid "Use prefix"
+msgstr "Utilice prefijo"
+
+# Seach box hint: BCP 47 language example
+msgid "to search for a BCP 47 language tag, for example"
+msgstr "para buscar una etiqueta de idioma BCP 47, por ejemplo"
+
+# Search box hint: BCP 47 language example
+msgid "searches for Tigrigna (Ethiopia)"
+msgstr "busca Tigrigna (Etiopía)"
+
diff --git a/_includes/locale/en/LC_MESSAGES/keyboards-fr-FR.po b/_includes/locale/en/LC_MESSAGES/keyboards-fr-FR.po
new file mode 100644
index 00000000..ce36e072
--- /dev/null
+++ b/_includes/locale/en/LC_MESSAGES/keyboards-fr-FR.po
@@ -0,0 +1,94 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Crowdin-Project: keymancom\n"
+"X-Crowdin-Project-ID: 740839\n"
+"X-Crowdin-Language: fr\n"
+"X-Crowdin-File: /master/keyboards/keyboards.po\n"
+"X-Crowdin-File-ID: 2\n"
+"Project-Id-Version: keymancom\n"
+"Language-Team: French\n"
+"Language: fr_FR\n"
+"PO-Revision-Date: 2024-11-11 08:15\n"
+
+# Page Title
+msgid "Keyboard Search"
+msgstr "Recherche au clavier"
+
+# Page Description
+msgid "Keyman Keyboard Search"
+msgstr "Recherche de clavier Keyman"
+
+# Keyboard search bar
+msgid "Keyboard search:"
+msgstr "Recherche au clavier:"
+
+# Search bar placeholder
+msgid "Enter language or keyboard"
+msgstr "Entrez la langue ou le clavier"
+
+# Search Button Value
+msgid "Search"
+msgstr "Recherche"
+
+# Link to start a new keyboard search
+msgid "New search"
+msgstr "Nouvelle recherche"
+
+# Search box instruction (Popular keyboards | All keyboards)
+msgid "Enter the name of a keyboard or language to search for"
+msgstr "Saisissez le nom d'un clavier ou d'une langue à rechercher"
+
+# Search box link for popular keyboards
+msgid "Popular keyboards"
+msgstr "Claviers populaires"
+
+# Search box link for all Keyman keyboards
+msgid "All keyboards"
+msgstr "Tous les claviers"
+
+# Search box hint: List header
+msgid "Hints"
+msgstr "Conseils"
+
+# Search box hint: Description
+msgid "The search always returns a list of keyboards. It searches for keyboard names and details, language names, country names and script names."
+msgstr "La recherche renvoie toujours une liste de claviers. Elle recherche les noms et les détails des claviers, les noms de langues, les noms de pays et les noms d'écritures."
+
+# Search box hint: available prefixes to use in the search
+msgid "You can apply prefixes"
+msgstr "Vous pouvez appliquer des préfixes"
+
+# (keyboards)
+msgid "(keyboards)"
+msgstr "(claviers)"
+
+# (languages)
+msgid "(languages)"
+msgstr "(langues)"
+
+# (scripts, writing systems) or...
+msgid "(scripts, writing systems) or"
+msgstr "(scripts, systèmes d'écriture) ou"
+
+# (countries) to filter your search results...
+msgid "(countries) to filter your search results. For example"
+msgstr "(pays) pour filtrer vos résultats de recherche. Par exemple"
+
+# Search box hint: example of country search
+msgid "searches for keyboards for languages used in Thailand."
+msgstr "recherche des claviers pour les langues utilisées en Thaïlande."
+
+# Search box hint: BCP 47 prefix
+msgid "Use prefix"
+msgstr "Utiliser le préfixe"
+
+# Seach box hint: BCP 47 language example
+msgid "to search for a BCP 47 language tag, for example"
+msgstr "pour rechercher une balise de langue BCP 47, par exemple"
+
+# Search box hint: BCP 47 language example
+msgid "searches for Tigrigna (Ethiopia)"
+msgstr "busca Tigrigna (Etiopía)"
+
diff --git a/keyboards/index.php b/keyboards/index.php
index c188fa85..41e61918 100644
--- a/keyboards/index.php
+++ b/keyboards/index.php
@@ -7,10 +7,14 @@
   use Keyman\Site\com\keyman\templates\Menu;
   use Keyman\Site\com\keyman\templates\Body;
   use Keyman\Site\com\keyman\templates\Foot;
+  use Keyman\Site\com\keyman\Locale;
+
+  Locale::localize('keyboards');
 
   $head_options = [
-    'title' =>'Keyboard Search',
-    'description' => 'Keyman Keyboard Search',
+    'title' => _('Keyboard Search'),
+    'description' => _('Keyman Keyboard Search'),
+    'language' => Locale::currentLocale(),
     'css' => [Util::cdn('css/template.css'), Util::cdn('keyboard-search/search.css')],
     'js' => [Util::cdn('keyboard-search/jquery.mark.js'), Util::cdn('keyboard-search/dedicated-landing-pages.js'),
       Util::cdn('keyboard-search/search.js')]
@@ -46,14 +50,14 @@
 
 <div class='<?= $embed == 'none' ? '' : 'embed embed-'.$embed ?>'>
 
-  <h2 class="red underline"><a href='/keyboards'>Keyboard Search</a></h2>
+  <h2 class="red underline"><a href='/keyboards'><?= _('Keyboard Search') ?></a></h2>
 
   <div id='search-box'>
     <form method='get' action='/keyboards' name='f'>
-      <label for="search-q">Keyboard search:</label><input id="search-q" type="text" placeholder="Enter language or keyboard" name="q"
+      <label for="search-q"><?= _('Keyboard search:') ?></label><input id="search-q" type="text" placeholder="<?= _('Enter language or keyboard') ?>" name="q"
       <?php if($embed == 'none') echo 'autofocus'; ?>>
-      <input id="search-f" type="image" src="<?= cdn('img/search-button.png"') ?>" value="Search" onclick="return do_search()">
-      <label id="search-new"><a href='/keyboards<?=$session_query_q?>'>New search</a></label>
+      <input id="search-f" type="image" src="<?= cdn('img/search-button.png"') ?>" value="<?= _('Search') ?>" onclick="return do_search()">
+      <label id="search-new"><a href='/keyboards<?=$session_query_q?>'><?= _('New search') ?></a></label>
       <input id="search-obsolete" type="hidden" name="obsolete" value="0">
       <input id="search-page" type="hidden" name="page" value="1">
     </form>
@@ -62,14 +66,31 @@
   <div id='search-results-container' class=''>
   <div id='search-results'></div>
   <div id='search-results-empty'>
-    <p>Enter the name of a keyboard or language to search for. (<a href="?q=p:popular">Popular keyboards</a> | <a href="?q=p:alphabetical">All keyboards</a>)</p>
+    <p>
+      <?= _('Enter the name of a keyboard or language to search for') ?> (
+      <a href="?q=p:popular"><?= _('Popular keyboards') ?></a> | 
+      <a href="?q=p:alphabetical"><?= _('All keyboards') ?></a>)
+    </p>
     <br />
-    <p>Hints</p>
+    <p><?= _('Hints') ?></p>
     <ul>
-      <li>The search always returns a list of keyboards. It searches for keyboard names and details, language names, country names and script names.</li>
-      <li>You can apply prefixes <code>k:</code> (keyboards), <code>l:</code> (languages), <code>s:</code> (scripts, writing systems)
-      or <code>c:</code> (countries) to filter your search results. For example <code>c:thailand</code> searches for keyboards for languages used in Thailand.</li>
-      <li>Use prefix <code>l:id:</code> to search for a BCP 47 language tag, for example <code>l:id:ti-et</code> searches for Tigrigna (Ethiopia).</li>
+      <li>
+        <?= _('The search always returns a list of keyboards. ' .
+          'It searches for keyboard names and details, language names, country names and script names.') ?>
+      </li>
+      <li>
+        <?= _('You can apply prefixes') ?> 
+        <code>k:</code> <?= _('(keyboards)') ?>
+        <code>l:</code> <?= _('(languages)') ?>
+        <code>s:</code> <?= _('(scripts, writing systems) or') ?>
+        <code>c:</code> <?= _('(countries) to filter your search results. For example') ?> 
+        <code>c:thailand</code> <?= _('searches for keyboards for languages used in Thailand.') ?>
+      </li>
+      <li>
+        <?= _('Use prefix') ?> 
+        <code>l:id:</code> <?= _('to search for a BCP 47 language tag, for example') ?> 
+        <code>l:id:ti-et</code> <?= _('searches for Tigrigna (Ethiopia)') ?>
+      </li>
     </ul>
   </div>
 </div>
diff --git a/keyboards/session.php b/keyboards/session.php
index b2153876..0a01f2f8 100644
--- a/keyboards/session.php
+++ b/keyboards/session.php
@@ -32,6 +32,13 @@
   $embed_ios = $embed == 'ios';
   $embed_developer = $embed == 'developer';
 
+  if(isset($_REQUEST['lang'])) {
+    \Keyman\Site\com\keyman\Locale::overrideCurrentLocale($_REQUEST['lang']);
+  } else if (isset($_SESSION['lang'])) {
+    \Keyman\Site\com\keyman\Locale::overrideCurrentLocale($_SESSION['lang']);
+  }
+  $_SESSION['lang'] = \Keyman\Site\com\keyman\Locale::currentLocale();
+
   if($embed != 'none') {
     // Poor man's session control because IE embedded in downlevel Windows destroys cookie support by
     // default, including in existing versions of Keyman.
diff --git a/resources/init-container.sh b/resources/init-container.sh
index f1cb5c18..efcd94d7 100755
--- a/resources/init-container.sh
+++ b/resources/init-container.sh
@@ -13,3 +13,22 @@ else
   echo "Skip Generating CDN and clean CDN cache"
   rm -rf "$THIS_SCRIPT_PATH/../cdn/deploy"
 fi
+
+echo "---- Generating .mo localization files ----"
+cd _includes/locale/en/LC_MESSAGES/
+
+# cleanup previous .mo files
+find . -type f -name '*.mo' -delete
+
+# Compile .po to .mo localization files
+for filename in `find . -type f -name "*.po"`; do
+  # Remove the .po extension
+  base_name="${filename%.po}"
+  msgfmt "${base_name}.po" --output-file="${base_name}".mo
+  retVal=$?
+  if [[ ${retVal} -ne 0 ]]; then
+    exit 1
+  fi
+done
+
+cd ../../../../