892 lines
32 KiB
PHP
892 lines
32 KiB
PHP
<?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,
|
|
'extract_messages_excluded_paths' => ['.*/vendor/*'],
|
|
)
|
|
);
|
|
|
|
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 environment to '$locale'");
|
|
|
|
// Set the locale
|
|
if (setlocale(LC_ALL, $locale) === false)
|
|
Log :: error("An error occurred 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();
|
|
|
|
$root_directory_path = App::root_directory_path();
|
|
$excluded_paths = App::get('i18n.extract_messages_excluded_paths', null, 'array');
|
|
|
|
if (Cli::core_mode()) {
|
|
// List EesyPHP PHP files to parse
|
|
$cmd = array(
|
|
'find', '-name', "'*.php'", '-type', 'f', // Looking for PHP files
|
|
"-not", "-name", "'*.tpl.php'", // Exclude Smarty cache template files
|
|
);
|
|
if ($excluded_paths)
|
|
foreach($excluded_paths as $path)
|
|
array_push($cmd, "-not", "-path", "'$path'");
|
|
$eesyphp_php_files = run_external_command(
|
|
$cmd,
|
|
null, // no STDIN data
|
|
false, // do not escape command args (already done)
|
|
"$root_directory_path/src" // 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
|
|
"$root_directory_path/src" // 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
|
|
$cmd = array(
|
|
'find', '-name', "'*.php'", '-type', 'f', // Looking for PHP files
|
|
"-not", "-name", "'*.tpl.php'", // Exclude Smarty cache template files
|
|
);
|
|
foreach($excluded_paths as $path)
|
|
array_push($cmd, "-not", "-path", "'$path'");
|
|
$php_files = run_external_command(
|
|
$cmd,
|
|
null, // no STDIN data
|
|
false, // do not escape command args (already done)
|
|
$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
|
|
$root_directory_path // Run from application root 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;
|
|
// Make path relative to application root directory
|
|
$relative_static_directory = App::relative_path($static_directory);
|
|
if (!$relative_static_directory) {
|
|
Log::debug("Static directory '%s' does not exist, ignore it.", $static_directory);
|
|
continue;
|
|
}
|
|
// List JS files to parse
|
|
$cmd = array(
|
|
'find', escapeshellarg($relative_static_directory),
|
|
'-name', "'*.tpl'", '-type', 'f'
|
|
);
|
|
foreach($excluded_paths as $path)
|
|
array_push($cmd, "-not", "-path", "'$path'");
|
|
$result = run_external_command(
|
|
$cmd,
|
|
null, // no STDIN data
|
|
false, // do not escape command args (already done)
|
|
$root_directory_path // Run from application root 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
|
|
$root_directory_path // Run from application root 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;
|
|
// Make path relative to application root directory
|
|
$relative_templates_directory = App::relative_path($templates_directory);
|
|
if (!$relative_templates_directory) {
|
|
Log::debug("Templates directory '%s' does not exist, ignore it.", $templates_directory);
|
|
continue;
|
|
}
|
|
|
|
// List templates files to parse
|
|
$cmd = array(
|
|
'find', escapeshellarg($relative_templates_directory),
|
|
'-name', "'*.tpl'", '-type', 'f'
|
|
);
|
|
foreach($excluded_paths as $path)
|
|
array_push($cmd, "-not", "-path", "'$path'");
|
|
$templates_files = run_external_command(
|
|
$cmd,
|
|
null, // no STDIN data
|
|
false, // do not escape command args (already done)
|
|
dirname($templates_directory) // Run from parent directory
|
|
);
|
|
if (!is_array($templates_files) || $templates_files[0] != 0)
|
|
Log :: fatal(
|
|
self::_("Fail to list templates files in directory %s."),
|
|
$templates_directory
|
|
);
|
|
|
|
// Compute template files file from find command output
|
|
$templates_files = explode("\n", $templates_files[1]);
|
|
|
|
// Extract messages from templates files using tsmarty2c.php
|
|
$cmd = array(
|
|
PHP_BINARY.' '.App :: root_directory_path().
|
|
"/vendor/smarty-gettext/smarty-gettext/tsmarty2c.php",
|
|
);
|
|
array_push($cmd, ...$templates_files);
|
|
$result = run_external_command(
|
|
$cmd,
|
|
null, // Pass nothing on STDIN
|
|
true, // Escape arguments
|
|
$root_directory_path // Run from application root 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
|