<?php

namespace EesyPHP;

use Locale;

class I18n {
  // Gettext application text domain
  public const TEXT_DOMAIN = 'DEFAULT';

  // Gettext core text domain
  public const CORE_TEXT_DOMAIN = 'EESYPHP';

  /**
   * The core root directory path of translation files
   * @var string
   */
  protected static $core_root_path = __DIR__.'/../locales';

  /**
   * The root directory path of translation files
   * @var string
   */
  protected static $root_path = 'locales';

  /**
   * The default locale
   * @var string
   */
  protected static $default_locale = 'en_US.UTF8';

  /**
   * Initialize translation system
   *
   * Detect best translation language and configure the translation
   * system.
   *
   * @return void
   */
  public static function init() {
    // Set config default values
    App :: set_default(
      'i18n',
      array(
        'root_directory' => '${root_directory_path}/locales',
        'default_locale' => null,
      )
    );

    self :: $root_path = App::get(
      'i18n.root_directory', null, 'string');
    $default_locale = App::get('i18n.default_locale', null, 'string');

    if (!class_exists('Locale')) {
      Log :: error('Locale PHP class does not exist. May be php-intl is not installed?');
      return;
    }

    $available_langs = self :: get_available_langs();
    if (php_sapi_name() != "cli") {
      if (isset($_REQUEST['lang']) && in_array($_REQUEST['lang'], $available_langs)) {
        $lang = $_REQUEST['lang'];
        Log :: trace("Select lang from request parameter: '$lang'");
      }
      elseif (
        isset($_SESSION['lang'])
        && in_array($_SESSION['lang'], $available_langs)
        && !isset($_REQUEST['reset_lang'])
      ) {
        $lang = $_SESSION['lang'];
        Log :: trace("Restore lang from session: '$lang'");
      }
      else {
        $lang = Locale::lookup(
          self :: get_available_langs(),
          Locale::acceptFromHttp(
            isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])?
            $_SERVER['HTTP_ACCEPT_LANGUAGE']:null
          ),
          true,
          Locale::getPrimaryLanguage(self :: $default_locale)
        );
        Log :: trace("Best lang found is '$lang'");
      }
    }
    else {
      $lang = null;
      $sys_current = getenv('LC_ALL');
      if (!$sys_current)
        $sys_current = getenv('LANG');
      if ($sys_current)
        $lang = Locale::getPrimaryLanguage($sys_current);
      if (is_null($lang)) {
        Log :: trace('No configured lang detected from CLI env, use default.');
        $lang = Locale::getPrimaryLanguage(self :: $default_locale);
      }
      else
        Log :: trace("Lang detected from CLI env : '$lang'");
    }

    // Keep selected lang in session
    $_SESSION['lang'] = $lang;

    $locale = self :: lang2locale($lang);
    Log :: trace("Matching locale found with language '$lang' is '$locale'");

    // Gettext firstly look the LANGUAGE env variable, so set it
    if (!putenv("LANGUAGE=$locale"))
      Log :: error("Fail to set LANGUAGE variable in environnement to '$locale'");

    // Set the locale
    if (setlocale(LC_ALL, $locale) === false)
      Log :: error("An error occured setting locale to '$locale'");

    // Configure and set the text domain
    $fullpath = bindtextdomain(self :: CORE_TEXT_DOMAIN, self :: $core_root_path);
    Log :: trace("Core text domain %s fullpath is '%s'.", self :: CORE_TEXT_DOMAIN, $fullpath);
    Log :: trace("Test: ".self::_('Hello world !'));

    $fullpath = bindtextdomain(self :: TEXT_DOMAIN, self :: $root_path);
    Log :: trace("Default text domain %s fullpath is '%s'.", self :: TEXT_DOMAIN, $fullpath);
    Log :: trace("Default text domain is '".textdomain(self :: TEXT_DOMAIN)."'.");

    // JS translation file
    if (
      php_sapi_name() != "cli"
      && Tpl :: initialized()
    ) {
      Tpl :: register_static_directory(self :: $root_path, null, 'locales/');
      Tpl :: add_js_file("lib/babel.js", "js/translation.js");
      Tpl :: add_js_file("locales/", "$locale.js");
    }

    if (php_sapi_name() == 'cli') {
      Cli :: add_command(
        'extract_messages',
        array('\\EesyPHP\\I18n', 'cli_extract_messages'),
        ___("Extract messages that need to be translated"),
        null,
        ___("This command could be used to generate/update locales/messages.pot file.")
      );

      Cli :: add_command(
        'update_messages',
        array('\\EesyPHP\\I18n', 'cli_update_messages'),
        ___("Update messages in translation PO lang files"),
        null,
        ___("This command could be used to init/update PO files in locales/*/LC_MESSAGES directories.")
      );

      Cli :: add_command(
        'compile_messages',
        array('\\EesyPHP\\I18n', 'cli_compile_messages'),
        ___(
          "Compile messages from existing translation PO lang files to ".
          "corresponding MO files and JS catalogs"
        ),
        null,
        ___(
          "This command could be used to compile PO files in locales/*/LC_MESSAGES ".
          "directories to MO files and as JS catalogs in locales directory."
        )
      );

      Cli :: add_command(
        'init_locale',
        array('\\EesyPHP\\I18n', 'cli_init_locale'),
        ___("Initialize a new locale for translation."),
        "[locale]",
        ___("This command could be used to initialize a new locale for translation.")
      );
    }
  }

  /**
   * List available translation languages
   *
   * @param $as_locales boolean  If true, locale names will be return instead
   *                             of primary languages (optional, default: false)
   *
   * @return array Array of available translation languages (or locales)
   */
  public static function get_available_langs($as_locales=false) {
    if (!is_dir(self :: $root_path))
      Log :: fatal("Root land directory not found (%s)", self :: $root_path);
    $langs = array(($as_locales?'en_US.UTF8':'en'));
    if ($dh = opendir(self :: $root_path)) {
      while (($file = readdir($dh)) !== false) {
        if (!is_dir(self :: $root_path . '/' . $file) || in_array($file, array('.', '..')))
          continue;
        if ($as_locales) {
          $langs[] = $file;
          continue;
        }
        $lang = Locale::getPrimaryLanguage($file);
        if (!in_array($lang, $langs))
          $langs[] = $lang;
      }
      closedir($dh);
    }
    $langs = array_unique($langs);
    Log :: trace('Available '.($as_locales?'locales':'languages').': '.implode(', ', $langs));
    return $langs;
  }

  /**
   * Get locale name corresponding to specified translation language
   *
   * @param  $lang string The translation language
   * @param  $default string|null Default locale name to return if any available translation
   *                              locales matched with the specified language
   *                              (optional, default: self :: $default_locale)
   * @return string Corresponding locale
   */
  public static function lang2locale($lang, $default=null) {
    if (is_null($default))
      $default = self :: $default_locale;
    foreach (self :: get_available_langs(true) as $locale) {
      if (strpos($locale, $lang) === false)
        continue;
      return $locale;
    }
    return $default;
  }

  /*
   ********************************************************************
   *                      Translations helpers                        *
   ********************************************************************
   */

  /**
   * Translate a message using gettext
   * @param string $message The string to translate
   * @param array $extra_args Extra arguments used to compute the message using sprintf
   * @return string The translated and computed message
   */
  public static function _(string $message, ...$extra_args) {
      $return = dgettext(self :: CORE_TEXT_DOMAIN, $message);
      // If extra arguments passed, format message using sprintf
      if ($extra_args) {
        $return = call_user_func_array(
          'sprintf',
          array_merge(array($return), $extra_args)
        );
      }
      return $return;
  }

  /**
   * Translate a message with singular and plural forms using gettext
   * @param string $singular The singular message to translate
   * @param string $plural The plural message to translate
   * @param int $count The item count to determine if the singular or the plural forms must to be
   *                   used
   * @param array $extra_args Extra arguments used to compute the message using sprintf
   * @return string The translated and computed message
   */
  public static function ngettext(string $singular, string $plural, int $count, ...$extra_args) {
      $return = dngettext(self :: CORE_TEXT_DOMAIN, $singular, $plural, $count);
      // If extra arguments passed, format message using sprintf
      if ($extra_args) {
        $return = call_user_func_array(
          'sprintf',
          array_merge(array($return), $extra_args)
        );
      }
      return $return;
  }

  /**
   * Normalize a locale name
   * @param string  $name            The locale name to normalize
   * @param boolean $encoding_lower  Set to true to put encoding in lowercase (optional, default: false)
   * @return string|false  The normalized locale of false in case of invalid provide values
   */
  public static function normalize_locale_name($name, $encoding_lower=False) {
    if (
      !preg_match(
        "/^(?P<lang0>[a-z]{2})_(?P<lang1>[a-z]{2})((@(?P<at>[a-z]+))|(\.(?P<encoding>[a-z0-9\-]+)))?$/i",
        $name, $m
      )
    )
      return false;
    $locale = strtolower($m['lang0'])."_".strtoupper($m['lang1']);
    if ($m['at'])
      $locale .= ".".strtolower($m['at']);
    if ($m['encoding']) {
      $m['encoding'] = strtoupper($m['encoding']);
      if ($m['encoding'] == "UTF-8") $m['encoding'] = "UTF8";
      if ($encoding_lower) $m['encoding'] = strtolower($m['encoding']);
      $locale .= ".".$m['encoding'];
    }
    return $locale;
  }

  /*
   ********************************************************************
   *                   Translations CLI commands                      *
   ********************************************************************
   */


  /**
   * Convert PO file to JSON file
   *
   * @param string $locale                      The locale of the input PO file
   * @param string $po_path                     The path of the main PO input file
   * @param string|null $domain                 The domain specified in the output JSON file
   *                                            (optional, default: self :: CORE_TEXT_DOMAIN)
   * @param array<string>|null $other_po_paths  Optional list of other PO files: if specified,
   *                                            their translated messages will be loaded and
   *                                            included in output JSON file.
   *
   * @return string JSON encoded file content
   */
  public static function po2json($locale, $po_path, $domain=null, $other_po_paths=null) {
    Log :: trace('Load PO file %s', $po_path);
    $fileHandler = new \Sepia\PoParser\SourceHandler\FileSystem($po_path);
    $poparser = new \Sepia\PoParser\Parser($fileHandler);
    $catalog = $poparser->parse();
    $headers = $catalog->getHeader();

    $messages = array();
    foreach ($catalog->getEntries() as $entry) {
      // msg id json format
      $msg = $entry->getMsgStr();
      if ($entry->isPlural())
        $msg = array($msg, $entry->getMsgIdPlural());
      $messages[$entry->getMsgId()] = $msg;
    }

    foreach(ensure_is_array($other_po_paths) as $path) {
      Log :: trace('Load PO file %s (for its translated messages only)', $path);
      $fileHandler = new \Sepia\PoParser\SourceHandler\FileSystem($path);
      $poparser = new \Sepia\PoParser\Parser($fileHandler);
      $catalog = $poparser->parse();
      foreach ($catalog->getEntries() as $entry) {
        // msg id json format
        $msg = $entry->getMsgStr();
        if ($entry->isPlural())
          $msg = array($msg, $entry->getMsgIdPlural());
        $messages[$entry->getMsgId()] = $msg;
      }
    }

    return json_encode(array(
      'messages' => $messages,
      'locale' => $locale,
      'domain' => $domain?$domain:self :: CORE_TEXT_DOMAIN,
      'plural_expr' => '(n > 1)',
    ));
  }

  /**
   * Command to initialize a new locale.
   *
   * @param  array $command_args  The command arguments
   * @return void
   */
  public static function cli_init_locale($command_args) {
    $root_path = Cli::core_mode()?self::$core_root_path:self::$root_path;
    if (!is_dir($root_path))
      Log :: fatal(self::_("Root locales directory does not exists ($root_path)."));
    $domain = Cli::core_mode()?self::CORE_TEXT_DOMAIN:self::TEXT_DOMAIN;
    if (count($command_args) != 1)
      Cli::usage(self::_("You must provide the locale to initialize as unique and first argument."));
    $locale = self::normalize_locale_name($command_args[0], true);
    if (!$locale)
      Log :: fatal(self::_("Invalid locale %s.", $command_args[0]));

    // Validate locale
    $result = run_external_command(["locale", "-a"]);
    if (!is_array($result) || $result[0] != 0)
      Log :: fatal(self::_("Fail to list valid locales."));
    if (!in_array($locale, explode("\n", $result[1])))
      Log :: fatal(self::_("Invalid locale %s.", $locale));

    // Continue with locale with encoding as uppercase
    $locale = self::normalize_locale_name($locale);

    // Check and create locale directory
    $locale_dir = "$root_path/$locale";
    if (!is_dir($locale_dir)) {
      if (mkdir($locale_dir))
        Log::info(I18n::_("Locale %s directory created (%s)."), $locale, $locale_dir);
      else
        Log::fatal(I18n::_("Fail to create locale %s directory (%s)."), $locale, $locale_dir);
    }
    else
      Log::debug(I18n::_("Locale %s directory already exist (%s)."), $locale, $locale_dir);

    // Check and create locale LC_MESSAGES directory
    $locale_lc_dir = "$locale_dir/LC_MESSAGES";
    if (!is_dir($locale_lc_dir)) {
      if (mkdir($locale_lc_dir))
        Log::info(
          I18n::_("Locale %s LC_MESSAGES directory created (%s)."), $locale, $locale_lc_dir
        );
      else
        Log::fatal(
          I18n::_("Fail to create locale %s LC_MESSAGES directory (%s)."),$locale, $locale_lc_dir
        );
    }
    else
      Log::debug(I18n::_("Locale %s LC_MESSAGES directory already exist (%s)."), $locale, $locale_dir);

    $po_file = "$locale_lc_dir/$domain.po";
    if (!is_file($po_file)) {
      $fd = fopen($po_file, 'w');
      $lines = fwrite(
        $fd,
        implode(
          "\n",
          array(
            'msgid ""',
            'msgstr ""',
            '"POT-Creation-Date: '.date('Y-m-d H:iO').'\n"',
            '"PO-Revision-Date: '.date('Y-m-d H:iO').'\n"',
            '"Language: '.substr($locale, 0, 2).'\n"',
            '"MIME-Version: 1.0\n"',
            '"Content-Type: text/plain; charset=utf-8\n"',
            '"Content-Transfer-Encoding: 8bit\n"',
            '"Plural-Forms: nplurals=2; plural=(n > 1);\n"',
          )
        )
      );
      fclose($fd);
      Log::info(
        I18n::_("Locale %s PO file created (%s)."), $locale, $po_file
      );
    }
    else
      Log::debug(I18n::_("Locale %s PO file already exist (%s)."), $locale, $po_file);

    // Extract messages
    self :: cli_extract_messages([]);
    self :: cli_update_messages([]);
    self :: cli_compile_messages([]);
  }

  /**
   * Command to extract messages from PHP/JS & template files and
   * generate the lang/messages.pot file.
   *
   * @param  array $command_args  The command arguments
   * @return void
   */
  public static function cli_extract_messages($command_args) {
    $root_path = Cli::core_mode()?self::$core_root_path:self::$root_path;

    // Store list of generated POT files
    $pot_files = array();

    if (Cli::core_mode()) {
      // List EesyPHP PHP files to parse
      $eesyphp_php_files = run_external_command(
        array(
          'find', '-name', "'*.php'",        // Looking for PHP files
          "-not", "-path", "'.*/vendor/*'",  // Exclude vendor directories
          "-not", "-name", "'*.tpl.php'"     // Exclude Smarty cache template files
        ),
        null,    // no STDIN data
        false,   // do not escape command args (already done)
        __DIR__  // Run from EesyPHP src directory
      );
      if (!is_array($eesyphp_php_files) || $eesyphp_php_files[0] != 0)
        Log :: fatal(self::_("Fail to list EesyPHP PHP files."));

      // Extract messages from EesyPHP PHP files using xgettext
      $pot_file = "$root_path/php-messages.pot";
      $result = run_external_command(
        array(
          "xgettext",
          "--from-code utf-8",
          "--language=PHP",
          "-o", $pot_file,      // Output
          "--omit-header",      // No POT header
          "--keyword=___",      // Handle custom ___() translation function
          "--files=-",          // Read files to parse from STDIN
          "--force-po",         // Write PO file even if empty
        ),
        $eesyphp_php_files[1],  // Pass PHP files list via STDIN
        true,                   // Escape parameters
        __DIR__                 // Run from EesyPHP src directory
      );
      if (!is_array($result) || $result[0] != 0)
        Log :: fatal(self::_("Fail to extract messages from EesyPHP PHP files using xgettext."));
      if (is_file($pot_file))
        $pot_files[] = $pot_file;
    }
    else {
      // List application PHP files to parse
      $php_files = run_external_command(
        array(
          'find', '-name', "'*.php'",        // Looking for PHP files
          "-not", "-path", "'.*/vendor/*'",  // Exclude vendor directories
          "-not", "-name", "'*.tpl.php'"     // Exclude Smarty cache template files
        ),
        null,    // no STDIN data
        false,   // do not escape command args (already done)
        App :: root_directory_path()  // Run from Application root directory
      );
      if (!is_array($php_files) || $php_files[0] != 0)
        Log :: fatal(self::_("Fail to list application PHP files."));

      // Extract messages from PHP files using xgettext
      $pot_file = "$root_path/php-messages.pot";
      $result = run_external_command(
        array(
          "xgettext",
          "--from-code utf-8",
          "--language=PHP",
          "-o", $pot_file,            // Output
          "--omit-header",            // No POT header
          "--keyword=___",            // Handle custom ___() translation function
          "--files=-",                // Read files to parse from STDIN
          "--force-po",               // Write PO file even if empty
        ),
        $php_files[1],                // Pass PHP files list via STDIN
        true,                         // Escape parameters
        App :: root_directory_path()  // Run from EesyPHP src directory
      );
      if (!is_array($result) || $result[0] != 0)
        Log :: fatal(self::_("Fail to extract messages from PHP files using xgettext."));
      $pot_files[] = "$root_path/php-messages.pot";
    }

    // Extract messages from JS files using xgettext in each registered static directories
    foreach(Tpl::static_directories() as $idx => $static_directory) {
      if (Cli::core_mode() && $static_directory != Tpl::$core_static_directory) continue;
      if (!Cli::core_mode() && $static_directory == Tpl::$core_static_directory) continue;
      // List JS files to parse
      $result = run_external_command(
        array('find', escapeshellarg(basename($static_directory)), '-name', "'*.js'"),
        null,   // no STDIN data
        false,   // do not escape command args (already done)
        dirname($static_directory)  // Run from parent directory
      );
      if (!is_array($result) || $result[0] != 0)
        Log :: fatal(
          self::_("Fail to list JS files in the directory of static files '%s'."),
          $static_directory);

      // Extract messages from JS files using xgettext
      $pot_file = "$root_path/js-$idx-messages.pot";
      $result = run_external_command(
        array(
          "xgettext",
          "--from-code utf-8",
          "--language=JavaScript",
          "-o", $pot_file,          // Output
          "--omit-header",          // No POT header
          "--keyword=___",          // Handle custom ___() translation function
          "--files=-",              // Read files to parse from STDIN
          "--force-po",             // Write PO file even if empty
        ),
        $result[1],                 // Pass JS files list via STDIN
        true,                       // Escape arguments
        dirname($static_directory)  // Run from parent directory
      );
      if (!is_array($result) || $result[0] != 0)
        Log :: fatal(
          self::_("Fail to extract messages from JS files in the directory of static files '%s' using xgettext."),
          $static_directory);
      if (is_file($pot_file))
        $pot_files[] = $pot_file;
    }

    if (Tpl :: initialized()) {
      foreach (Tpl :: templates_directories() as $idx => $templates_directory) {
        if (Cli::core_mode() && $templates_directory != Tpl::$core_templates_directory) continue;
        if (!Cli::core_mode() && $templates_directory == Tpl::$core_templates_directory) continue;
        // Extract messages from templates files using tsmarty2c.php
        $result = run_external_command(
          array(
            PHP_BINARY.' '.App :: root_directory_path()."/vendor/smarty-gettext/smarty-gettext/tsmarty2c.php",
            basename($templates_directory),
          ),
          null,                          // Pass nothing on STDIN
          true,                          // Escape arguments
          dirname($templates_directory)  // Run from parent directory
        );
        if (!is_array($result) || $result[0] != 0)
          Log :: fatal(
            self::_("Fail to extract messages from templates directory '%s' using tsmarty2c.php script."),
            $templates_directory
          );
        if (!$result[1]) continue;
        $pot_file = "$root_path/templates-$idx-messages.pot";
        $fd = fopen($pot_file, 'w');
        fwrite($fd, $result[1]);
        fclose($fd);
        $pot_files[] = $pot_file;
      }
    }

    $fd = fopen("$root_path/headers.pot", 'w');
    $headers = array(
      'msgid ""',
      'msgstr ""',
      '"POT-Creation-Date: '.date('Y-m-d H:iO').'\n"',
      '"PO-Revision-Date: '.date('Y-m-d H:iO').'\n"',
      '"MIME-Version: 1.0\n"',
      '"Content-Type: text/plain; charset=utf-8\n"',
      '"Content-Transfer-Encoding: 8bit\n"',
    );
    fwrite($fd, implode("\n", $headers));
    fclose($fd);

    // Merge previous results in messages.pot file using msgcat
    $result = run_external_command(array_merge(
      array(
        'msgcat',
        "-t", "utf-8", "--use-first", "--force-po",
        "-o", "$root_path/messages.pot",
        "$root_path/headers.pot",
      ),
      $pot_files
    ));
    if (!is_array($result) || $result[0] != 0)
      Log :: fatal(self::_("Fail to merge messages using msgcat."));
  }

  /**
   * Command to update messages from lang/messages.pot file to
   * all PO file in lang/[lang]/LC_MESSAGES.
   *
   * @param  array $command_args  The command arguments
   * @return bool
   */
  public static function cli_update_messages($command_args) {
    $compendium_args = array();
    foreach ($command_args as $arg) {
      if (!file_exists($arg))
        Log :: fatal(self::_("Compendium file %s not found."), $arg);
      $compendium_args[] = '-C';
      $compendium_args[] = $arg;
    }
    $domain = Cli::core_mode()?self::CORE_TEXT_DOMAIN:self::TEXT_DOMAIN;
    $root_path = Cli::core_mode()?self::$core_root_path:self::$root_path;

    $pot_file = "$root_path/messages.pot";
    if (!is_file($pot_file))
      Log :: fatal(self::_("POT file not found (%s). Please run extract_messages first."), $pot_file);

    if ($dh = opendir($root_path)) {
      $error = False;
      while (($file = readdir($dh)) !== false) {
        if (
          !is_dir("$root_path/$file") ||
          in_array($file, array('.', '..')) ||
          is_link("$root_path/$file")
        )
          continue;

        Log :: debug(self::_("Lang directory '%s' found"), $file);

        // Check LC_MESSAGES directory exists
        $lang = $file;
        $lang_dir = "$root_path/$file/LC_MESSAGES" ;
        if (!is_dir($lang_dir)) {
          Log :: debug(self::_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
            $lang);
          continue;
        }

        $po_file = "$lang_dir/$domain.po";
        $created = false;
        if (!is_file($po_file)) {
          // Init PO file from POT file using msginit
          $result = run_external_command(
            array("msginit", "-i", "$pot_file", "-l", "$lang", "-o", $po_file)
          );
          if (is_array($result) && $result[0] == 0) {
            $created = true;
          } else {
            Log :: error(self::_("Fail to init messages in %s PO file using msginit (%s)."),
              $lang, $po_file);
            $error = True;
          }
        }

        // Update messages in PO file from POT file using msgmerge
        // Note: msginit does not accept compendium files, so we also run
        // msgmerge on creation with compendium file(s).
        if (is_file($po_file) && (!$created || $compendium_args)) {
          $result = run_external_command(
            array_merge(
              array("msgmerge", "-q", "-U"),
              $compendium_args,
              array($po_file, $pot_file)
            )
          );
          if (!is_array($result) || $result[0] != 0) {
            Log :: error(self::_("Fail to update messages in %s PO file using msgmerge (%s)."),
              $lang, $po_file);
            $error = True;
          }
        }
        elseif (!$created) {
          Log :: debug(self::_("PO file not found in lang '%s' directory, ignore it."), $lang);
        }
      }
      closedir($dh);
      return !$error;
    }

    Log :: fatal(self::_("Fail to open root lang directory (%s)."), App :: root_directory_path());
  }

  /**
   * Command to compile messages from existing translation PO lang files
   * to corresponding MO files and as JS catalog (for translation in JS).
   *
   * @param  array $command_args  The command arguments
   * @return bool
   */
  public static function cli_compile_messages($command_args) {
    $domain = Cli::core_mode()?self::CORE_TEXT_DOMAIN:self::TEXT_DOMAIN;
    $root_path = Cli::core_mode()?self::$core_root_path:self::$root_path;
    if ($dh = opendir($root_path)) {
      $error = False;
      while (($file = readdir($dh)) !== false) {
        if (
          !is_dir("$root_path/$file") ||
          in_array($file, array('.', '..'))
        )
          continue;

        if (is_link("$root_path/$file")) {
          $real_lang_dir = readlink("$root_path/$file");
          if (dirname($real_lang_dir) != '.' || !is_dir("$root_path/$real_lang_dir"))
            continue;
          $lang = $file;
          Log :: debug(self::_("Lang alias symlink found: %s -> %s"), $lang, $real_lang_dir);

          // Create JS catalog symlink (if not exists)
          $js_link = "$root_path/$lang.js";
          $link_target = "$real_lang_dir.js";
          if (!file_exists($js_link)) {
            if (symlink($link_target, $js_link)) {
              Log :: info(self::_("JS catalog symlink for %s -> %s created (%s)"),
                $lang, $real_lang_dir, $js_link);
            }
            else {
              Log :: error(self::_("Fail to create JS catalog symlink for %s -> %s (%s)"),
                $lang, $real_lang_dir, $js_link);
              $error = True;
            }
          }
          elseif (readlink($js_link) == $link_target) {
            Log :: debug(self::_("JS catalog symlink for %s -> %s already exist (%s)"),
              $lang, $real_lang_dir, $js_link);
          }
          else {
            Log :: warning(
              self::_("JS catalog file for %s already exist, but it's not a symlink to %s (%s)"),
              $lang, $real_lang_dir, $js_link
            );
            $error = True;
          }
          continue;
        }

        Log :: debug(self::_("Lang directory '%s' found"), $file);

        // Check LC_MESSAGES directory exists
        $lang = $file;
        $lang_dir = "$root_path/$file/LC_MESSAGES" ;
        if (!is_dir($lang_dir)) {
          Log :: debug(self::_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
            $lang);
          continue;
        }

        // Test .PO file is present
        $po_file = "$lang_dir/$domain.po";
        if (!is_file($po_file)) {
          Log :: debug(self::_("PO file not found in lang '%s' directory, ignore it."),
            $lang);
          continue;
        }

        $mo_file = preg_replace('/\.po$/', '.mo', $po_file);

        // Compile messages from PO file to MO file using msgfmt
        $result = run_external_command(
          array("msgfmt", "-o", $mo_file, $po_file)
        );
        if (!is_array($result) || $result[0] != 0) {
          Log :: error(
            self::_("Fail to compile messages from %s PO file as MO file using msgfmt (%s)."),
            $lang, $po_file
          );
          $error = True;
        }

        // Compile messages from PO file to JS catalog file
        $other_po_paths = array();
        if ($domain != self::CORE_TEXT_DOMAIN) {
          $core_po_file = self::$core_root_path."/$lang/LC_MESSAGES/".self::CORE_TEXT_DOMAIN.".po";
          if (is_file($core_po_file)) {
            Log :: debug(
              self::_('Include core translated messages from %s PO file'),
              $core_po_file
            );
            $other_po_paths[] = $core_po_file;
          }
          else
            Log :: warning(
              self::_('Core PO file %s not found: can not include its translated messages in '.
              'resulting JSON catalog.'), $core_po_file);
        }
        $js_catalog = self :: po2json($lang, $po_file, $domain, $other_po_paths);
        $js_file = "$root_path/$lang.js";
        if(!$fd = fopen($js_file, 'w')) {
          Log :: error(self::_("Fail to open %s JS catalog file in write mode (%s)."),
            $lang, $js_file);
          $error = True;
        }
        elseif (fwrite($fd, sprintf("translations_data = %s;", $js_catalog)) === false) {
          Log :: error(self::_("Fail to write %s JS catalog in file (%s)."),
            $lang, $js_file);
          $error = True;
        }
        else {
          Log :: info(self::_("%s JS catalog writed (%s)."), $lang, $js_file);
        }
      }
      closedir($dh);

      return !$error;
    }
    Log :: fatal(self::_("Fail to open root lang directory (%s)."), App :: root_directory_path());
  }

}

# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
