Move translation (i18n) stuff in EesyPHP namespace

This commit is contained in:
Benjamin Renard 2023-01-29 23:35:17 +01:00
parent 15c2acee08
commit 5e8a2b6d1c
18 changed files with 579 additions and 548 deletions

View file

@ -1,6 +1,7 @@
<?php
use EesyPHP\Check;
use EesyPHP\I18n;
use EesyPHP\Log;
$cli_commands=array();
@ -248,13 +249,13 @@ function cli_list($command_args) {
add_cli_command(
'list',
'cli_list',
___("List/search items"),
___("[patterns]"),
I18n :: ___("List/search items"),
I18n :: ___("[patterns]"),
array(
___("-o|--orderby Ordering list criterion. Possible values:"),
I18n :: ___("-o|--orderby Ordering list criterion. Possible values:"),
" - ".implode("\n - ", $orderbys),
___("-r|--reverse Reverse order"),
___("-s|--status Filter on status. Possible values:"),
I18n :: ___("-r|--reverse Reverse order"),
I18n :: ___("-s|--status Filter on status. Possible values:"),
" - ".implode("\n - ", array_keys($status_list)),
)
);
@ -275,8 +276,8 @@ function cli_show($command_args) {
add_cli_command(
'show',
'cli_show',
___("Show item"),
___("[ID]")
I18n :: ___("Show item"),
I18n :: ___("[ID]")
);
function cli_delete($command_args) {
@ -313,8 +314,8 @@ function cli_delete($command_args) {
add_cli_command(
'delete',
'cli_delete',
___("Delete item"),
___("[item ID]")
I18n :: ___("Delete item"),
I18n :: ___("[item ID]")
);
function cli_export($command_args) {
@ -326,8 +327,8 @@ function cli_export($command_args) {
add_cli_command(
'export',
'cli_export',
___("Export items (as CSV)"),
___("[output file path]")
I18n :: ___("Export items (as CSV)"),
I18n :: ___("[output file path]")
);
function cli_restore($command_args) {
@ -342,8 +343,8 @@ function cli_restore($command_args) {
add_cli_command(
'restore',
'cli_restore',
___("Restore items (from CSV)"),
___("[input file path]")
I18n :: ___("Restore items (from CSV)"),
I18n :: ___("[input file path]")
);
function cli_cron($command_args) {
@ -410,11 +411,11 @@ function cli_cron($command_args) {
add_cli_command(
'cron',
'cli_cron',
___("Cron to handle item expiration"),
I18n :: ___("Cron to handle item expiration"),
false,
array (
___("-j/--just-try Just-try mode : do not really removed expired item(s)"),
___("-m/--max-age Item expiration limit (in days, optional)"),
I18n :: ___("-j/--just-try Just-try mode : do not really removed expired item(s)"),
I18n :: ___("-m/--max-age Item expiration limit (in days, optional)"),
)
);

View file

@ -71,7 +71,7 @@ $sentry_traces_sample_rate = 0.2;
$smarty_templates_dir = "$root_dir_path/templates";
$smarty_templates_c_dir = "$tmp_root_dir/templates_c";
// Default locale (see lang directory for available languages list)
// Default locale (see locales directory for available languages list)
$default_locale = 'en_US.UTF8';
// Session

View file

@ -1,6 +1,7 @@
<?php
use EesyPHP\Email;
use EesyPHP\I18n;
use EesyPHP\Log;
use EesyPHP\SentryIntegration;
use EesyPHP\SentrySpan;
@ -31,7 +32,6 @@ set_include_path($root_dir_path.'/includes' . PATH_SEPARATOR . get_include_path(
require("$root_dir_path/vendor/autoload.php");
// Load configuration
require_once('translation.php');
require_once('config.inc.php');
// Load local configuration file is present
@ -67,14 +67,13 @@ Session :: init(
// Nomenclatures
$status_list = array (
'pending' => ___('Pending'),
'validated' => ___('Validated'),
'refused' => ___('Refused'),
'archived' => ___('Archived'),
'pending' => I18n :: ___('Pending'),
'validated' => I18n :: ___('Validated'),
'refused' => I18n :: ___('Refused'),
'archived' => I18n :: ___('Archived'),
);
require_once('cli.php');
require_once('translation-cli.php');
require_once('smarty.php');
Url::init(isset($public_root_url)?$public_root_url:null);
require_once('url-helpers.php');
@ -90,7 +89,10 @@ Email :: init(
);
// Initialize translation
init_translation();
I18n::init(
"$root_dir_path/locales",
isset($default_locale)?$default_locale:null);
foreach($status_list as $key => $value)
$status_list[$key] = _($value);
$sentry_span->finish();

View file

@ -1,373 +0,0 @@
<?php
use EesyPHP\Log;
/*
********************************************************************
* Translations CLI commands *
********************************************************************
*/
if (php_sapi_name() != "cli")
return true;
/**
* Convert PO file to JSON file
*
* @param string $locale The locale of the input PO file
* @param string $path The path of the input PO file
*
* @return string JSON encoded file content
*/
function po2json($locale, $path) {
$fileHandler = new \Sepia\PoParser\SourceHandler\FileSystem($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;
}
return json_encode(array(
'messages' => $messages,
'locale' => $locale,
'domain' => TEXT_DOMAIN,
'plural_expr' => '(n > 1)',
));
}
/**
* 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
*/
function cli_extract_messages($command_args) {
global $root_dir_path, $root_lang_dir, $smarty_templates_dir;
// List PHP files to parse
$php_files = run_external_command(
array('find', escapeshellarg($root_dir_path), '-name', "'*.php'"),
null, // no STDIN data
false // do not escape command args (already done)
);
if (!is_array($php_files) || $php_files[0] != 0) {
Log :: fatal(_("Fail to list PHP files."));
}
// Extract messages from PHP files using xgettext
$result = run_external_command(
array(
"xgettext",
"--from-code utf-8",
"--language=PHP",
"-o", "$root_lang_dir/php-messages.pot", // Output
"--omit-header", // No POT header
"--keyword=___", // Handle custom ___() translation function
"--files=-" // Read files to parse from STDIN
),
$php_files[1] // Pass PHP files list via STDIN
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("Fail to extract messages from PHP files using xgettext."));
// List JS files to parse
$js_files = run_external_command(
array('find', escapeshellarg("$root_dir_path/public_html/js"), '-name', "'*.js'"),
null, // no STDIN data
false // do not escape command args (already done)
);
if (!is_array($js_files) || $js_files[0] != 0) {
Log :: fatal(_("Fail to list JS files."));
}
// Extract messages from JS files using xgettext
$result = run_external_command(
array(
"xgettext",
"--from-code utf-8",
"--language=JavaScript",
"-o", "$root_lang_dir/js-messages.pot", // Output
"--omit-header", // No POT header
"--keyword=___", // Handle custom ___() translation function
"--files=-" // Read files to parse from STDIN
),
$js_files[1] // Pass JS files list via STDIN
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("Fail to extract messages from JS files using xgettext."));
// Extract messages from templates files using tsmarty2c.php
$result = run_external_command(
array (
"$root_dir_path/vendor/bin/tsmarty2c.php",
"-o", "$root_lang_dir/templates-messages.pot",
$smarty_templates_dir,
)
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(
_("Fail to extract messages from template files using tsmarty2c.php script."));
$fd = fopen("$root_lang_dir/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(
'msgcat',
"$root_lang_dir/headers.pot",
"$root_lang_dir/php-messages.pot",
"$root_lang_dir/js-messages.pot",
"$root_lang_dir/templates-messages.pot",
"-t", "utf-8", "--use-first",
"-o", "$root_lang_dir/messages.pot",
));
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("Fail to merge messages using msgcat."));
}
add_cli_command(
'extract_messages',
'cli_extract_messages',
___("Extract messages that need to be translated"),
null,
___("This command could be used to generate/update lang/messages.pot file.")
);
/**
* 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
*/
function cli_update_messages($command_args) {
global $root_dir_path, $root_lang_dir, $smarty_templates_dir;
$compendium_args = array();
foreach ($command_args as $path) {
if (!file_exists($path))
Log :: fatal(_("Compendium file %s not found."), $path);
$compendium_args[] = '-C';
$compendium_args[] = $path;
}
$pot_file = "$root_lang_dir/messages.pot";
if (!is_file($pot_file))
Log :: fatal(_("POT file not found (%s). Please run extract_messages first."), $pot_file);
if ($dh = opendir($root_lang_dir)) {
$error = False;
while (($file = readdir($dh)) !== false) {
if (
!is_dir($root_lang_dir . '/' . $file) ||
in_array($file, array('.', '..')) ||
is_link($root_lang_dir . '/' . $file)
)
continue;
Log :: debug(_("Lang directory '%s' found"), $file);
// Check LC_MESSAGES directory exists
$lang = $file;
$lang_dir = $root_lang_dir . '/' . $file . '/LC_MESSAGES' ;
if (!is_dir($lang_dir)) {
Log :: debug(_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
$lang);
continue;
}
$po_file = $lang_dir . '/' . TEXT_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(_("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(_("Fail to update messages in %s PO file using msgmerge (%s)."),
$lang, $po_file);
$error = True;
}
}
elseif (!$created) {
Log :: debug(_("PO file not found in lang '%s' directory, ignore it."), $lang);
}
}
closedir($dh);
return !$error;
}
Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
add_cli_command(
'update_messages',
'cli_update_messages',
___("Update messages in translation PO lang files"),
null,
___("This command could be used to init/update PO files in lang/*/LC_MESSAGES directories.")
);
/**
* Command to compile messages from existing translation PO lang files
* to corresponding MO files and as JSON catalog (for translation in JS).
*
* @param array $command_args The command arguments
* @return bool
*/
function cli_compile_messages($command_args) {
global $root_dir_path, $root_lang_dir, $smarty_templates_dir;
if ($dh = opendir($root_lang_dir)) {
$error = False;
while (($file = readdir($dh)) !== false) {
if (
!is_dir($root_lang_dir . '/' . $file) ||
in_array($file, array('.', '..'))
)
continue;
if (is_link($root_lang_dir . '/' . $file)) {
$real_lang_dir = readlink($root_lang_dir . '/' . $file);
if (dirname($real_lang_dir) != '.' || !is_dir($root_lang_dir . '/' . $real_lang_dir))
continue;
$lang = $file;
Log :: debug(_("Lang alias symlink found: %s -> %s"), $lang, $real_lang_dir);
// Create JSON catalog symlink (if not exists)
$js_link = "$root_dir_path/public_html/translations/$lang.js";
$link_target = "$real_lang_dir.js";
if (!file_exists($js_link)) {
if (symlink($link_target, $js_link)) {
Log :: info(_("JSON catalog symlink for %s -> %s created (%s)"),
$lang, $real_lang_dir, $js_link);
}
else {
Log :: error(_("Fail to create JSON catalog symlink for %s -> %s (%s)"),
$lang, $real_lang_dir, $js_link);
$error = True;
}
}
elseif (readlink($js_link) == $link_target) {
Log :: debug(_("JSON catalog symlink for %s -> %s already exist (%s)"),
$lang, $real_lang_dir, $js_link);
}
else {
Log :: warning(
_("JSON 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(_("Lang directory '%s' found"), $file);
// Check LC_MESSAGES directory exists
$lang = $file;
$lang_dir = $root_lang_dir . '/' . $file . '/LC_MESSAGES' ;
if (!is_dir($lang_dir)) {
Log :: debug(_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
$lang);
continue;
}
// Test .PO file is present
$po_file = $lang_dir . '/' . TEXT_DOMAIN . '.po';
if (!is_file($po_file)) {
Log :: debug(_("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(
_("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 JSON catalog file
$json_catalog = po2json($lang, $po_file);
$js_file = "$root_dir_path/public_html/translations/$lang.js";
if(!$fd = fopen($js_file, 'w')) {
Log :: error(_("Fail to open %s JSON catalog file in write mode (%s)."),
$lang, $js_file);
$error = True;
}
elseif (fwrite($fd, sprintf("translations_data = %s;", $json_catalog)) === false) {
Log :: error(_("Fail to write %s JSON catalog in file (%s)."),
$lang, $js_file);
$error = True;
}
else {
Log :: info(_("%s JSON catalog writed (%s)."), $lang, $js_file);
}
}
closedir($dh);
return !$error;
}
Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
add_cli_command(
'compile_messages',
'cli_compile_messages',
___(
"Compile messages from existing translation PO lang files to ".
"corresponding MO files and JSON catalogs"
),
null,
___(
"This command could be used to compile PO files in lang/*/LC_MESSAGES ".
"directories to MO files and as JSON catalogs in public_html/translations."
)
);
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

@ -1,151 +0,0 @@
<?php
use EesyPHP\Log;
// Gettext text domain
define('TEXT_DOMAIN', 'DEFAULT');
/**
* 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)
*/
function get_available_langs($as_locales=false) {
global $root_lang_dir;
if (!is_dir($root_lang_dir))
Log :: fatal("Root land directory not found ($root_lang_dir)");
$langs = array(($as_locales?'en_US.UTF8':'en'));
if ($dh = opendir($root_lang_dir)) {
while (($file = readdir($dh)) !== false) {
if (!is_dir($root_lang_dir . '/' . $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: $default_locale)
* @return string Corresponding locale
*/
function lang2locale($lang, $default=null) {
global $default_locale;
if (is_null($default))
$default = $default_locale;
foreach (get_available_langs(true) as $locale) {
if (strpos($locale, $lang) === false)
continue;
return $locale;
}
return $default;
}
/**
* Helper function: just mark message for translation
*
* @param string $msg The message to translate
*
* @return string The message without transformation
*/
function ___($msg) {
return $msg;
}
/**
* Initialize translation system
*
* Detect best translation language and configure the translation
* system.
*
* @return void
*/
function init_translation() {
global $root_dir_path, $root_lang_dir, $default_locale, $smarty;
$root_lang_dir = "$root_dir_path/lang";
if (!class_exists('Locale')) {
Log :: error('Locale PHP class does not exist. May be php-intl is not installed?');
return;
}
$available_langs = 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(
get_available_langs(),
Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']),
true,
Locale::getPrimaryLanguage($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($default_locale);
}
else
Log :: trace("Lang detected from CLI env : '$lang'");
}
// Keep selected lang in session
$_SESSION['lang'] = $lang;
$locale = 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(TEXT_DOMAIN, $root_lang_dir);
Log :: trace("Text domain fullpath is '$fullpath'.");
Log :: trace("Text domain is '".textdomain(TEXT_DOMAIN)."'.");
Log :: trace("Test: "._('Hello world !'));
// JS translation file
$js_translation_file = "translations/$lang.js";
if (php_sapi_name() != "cli" && is_file("$root_dir_path/public_html/$js_translation_file")) {
add_js_file(array("lib/babel.js", "js/translation.js", $js_translation_file));
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab

View file

552
src/I18n.php Normal file
View file

@ -0,0 +1,552 @@
<?php
namespace EesyPHP;
use EesyPHP\Log;
use Locale;
use add_js_file;
class I18n {
// Gettext text domain
public const TEXT_DOMAIN = 'DEFAULT';
/**
* The root directory path of translation files
* @var string
*/
protected static string $root_path = 'locales';
/**
* The default locale
* @var string
*/
protected static string $default_locale = 'en_US.UTF8';
/**
* Initialize translation system
*
* Detect best translation language and configure the translation
* system.
*
* @param string|null $root_path The root directory path of translation files
* (optional, default: ./locales)
* @param string|null $default_locale The default locale
* (optional, default: en_US.UTF8)
* @return void
*/
public static function init($root_path=null, $default_locale=null) {
global $root_dir_path;
if (!is_null($root_path))
self :: $root_path = $root_path;
if (!is_null($default_locale))
self :: $default_locale = $default_locale;
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($_SERVER['HTTP_ACCEPT_LANGUAGE']),
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 :: TEXT_DOMAIN, self :: $root_path);
Log :: trace("Text domain fullpath is '$fullpath'.");
Log :: trace("Text domain is '".textdomain(self :: TEXT_DOMAIN)."'.");
Log :: trace("Test: "._('Hello world !'));
// JS translation file
$js_translation_file = "translations/$lang.js";
if (
php_sapi_name() != "cli"
&& isset($root_dir_path) && $root_dir_path
&& is_file("$root_dir_path/public_html/$js_translation_file")
&& function_exists('add_js_file')
) {
add_js_file(array("lib/babel.js", "js/translation.js", $js_translation_file));
}
if (php_sapi_name() == 'cli') {
add_cli_command(
'extract_messages',
array('\\EesyPHP\\I18n', 'cli_extract_messages'),
self :: ___("Extract messages that need to be translated"),
null,
self :: ___("This command could be used to generate/update lang/messages.pot file.")
);
add_cli_command(
'update_messages',
array('\\EesyPHP\\I18n', 'cli_update_messages'),
self :: ___("Update messages in translation PO lang files"),
null,
self :: ___("This command could be used to init/update PO files in lang/*/LC_MESSAGES directories.")
);
add_cli_command(
'compile_messages',
array('\\EesyPHP\\I18n', 'cli_compile_messages'),
self :: ___(
"Compile messages from existing translation PO lang files to ".
"corresponding MO files and JSON catalogs"
),
null,
self :: ___(
"This command could be used to compile PO files in lang/*/LC_MESSAGES ".
"directories to MO files and as JSON catalogs in public_html/translations."
)
);
}
}
/**
* 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;
}
/**
* Helper function: just mark message for translation
*
* @param string $msg The message to translate
*
* @return string The message without transformation
*/
public static function ___($msg) {
return $msg;
}
/*
********************************************************************
* Translations CLI commands *
********************************************************************
*/
/**
* Convert PO file to JSON file
*
* @param string $locale The locale of the input PO file
* @param string $path The path of the input PO file
*
* @return string JSON encoded file content
*/
public static function po2json($locale, $path) {
$fileHandler = new \Sepia\PoParser\SourceHandler\FileSystem($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;
}
return json_encode(array(
'messages' => $messages,
'locale' => $locale,
'domain' => self :: TEXT_DOMAIN,
'plural_expr' => '(n > 1)',
));
}
/**
* 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) {
global $root_dir_path, $smarty_templates_dir;
// List PHP files to parse
$php_files = run_external_command(
array('find', escapeshellarg($root_dir_path), '-name', "'*.php'"),
null, // no STDIN data
false // do not escape command args (already done)
);
if (!is_array($php_files) || $php_files[0] != 0) {
Log :: fatal(_("Fail to list PHP files."));
}
// Extract messages from PHP files using xgettext
$result = run_external_command(
array(
"xgettext",
"--from-code utf-8",
"--language=PHP",
"-o", self :: $root_path."/php-messages.pot", // Output
"--omit-header", // No POT header
"--keyword=___", // Handle custom ___() translation function
"--files=-" // Read files to parse from STDIN
),
$php_files[1] // Pass PHP files list via STDIN
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("Fail to extract messages from PHP files using xgettext."));
// List JS files to parse
$js_files = run_external_command(
array('find', escapeshellarg("$root_dir_path/public_html/js"), '-name', "'*.js'"),
null, // no STDIN data
false // do not escape command args (already done)
);
if (!is_array($js_files) || $js_files[0] != 0) {
Log :: fatal(_("Fail to list JS files."));
}
// Extract messages from JS files using xgettext
$result = run_external_command(
array(
"xgettext",
"--from-code utf-8",
"--language=JavaScript",
"-o", self :: $root_path."/js-messages.pot", // Output
"--omit-header", // No POT header
"--keyword=___", // Handle custom ___() translation function
"--files=-" // Read files to parse from STDIN
),
$js_files[1] // Pass JS files list via STDIN
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("Fail to extract messages from JS files using xgettext."));
// Extract messages from templates files using tsmarty2c.php
$result = run_external_command(
array (
"$root_dir_path/vendor/bin/tsmarty2c.php",
"-o", self :: $root_path."/templates-messages.pot",
$smarty_templates_dir,
)
);
if (!is_array($result) || $result[0] != 0)
Log :: fatal(
_("Fail to extract messages from template files using tsmarty2c.php script."));
$fd = fopen(self :: $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(
'msgcat',
self :: $root_path."/headers.pot",
self :: $root_path."/php-messages.pot",
self :: $root_path."/js-messages.pot",
self :: $root_path."/templates-messages.pot",
"-t", "utf-8", "--use-first",
"-o", self :: $root_path."/messages.pot",
));
if (!is_array($result) || $result[0] != 0)
Log :: fatal(_("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) {
global $root_dir_path, $smarty_templates_dir;
$compendium_args = array();
foreach ($command_args as $path) {
if (!file_exists($path))
Log :: fatal(_("Compendium file %s not found."), $path);
$compendium_args[] = '-C';
$compendium_args[] = $path;
}
$pot_file = self :: $root_path."/messages.pot";
if (!is_file($pot_file))
Log :: fatal(_("POT file not found (%s). Please run extract_messages first."), $pot_file);
if ($dh = opendir(self :: $root_path)) {
$error = False;
while (($file = readdir($dh)) !== false) {
if (
!is_dir(self :: $root_path . '/' . $file) ||
in_array($file, array('.', '..')) ||
is_link(self :: $root_path . '/' . $file)
)
continue;
Log :: debug(_("Lang directory '%s' found"), $file);
// Check LC_MESSAGES directory exists
$lang = $file;
$lang_dir = self :: $root_path . '/' . $file . '/LC_MESSAGES' ;
if (!is_dir($lang_dir)) {
Log :: debug(_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
$lang);
continue;
}
$po_file = $lang_dir . '/' . self :: TEXT_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(_("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(_("Fail to update messages in %s PO file using msgmerge (%s)."),
$lang, $po_file);
$error = True;
}
}
elseif (!$created) {
Log :: debug(_("PO file not found in lang '%s' directory, ignore it."), $lang);
}
}
closedir($dh);
return !$error;
}
Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
/**
* Command to compile messages from existing translation PO lang files
* to corresponding MO files and as JSON catalog (for translation in JS).
*
* @param array $command_args The command arguments
* @return bool
*/
public static function cli_compile_messages($command_args) {
global $root_dir_path, $smarty_templates_dir;
if ($dh = opendir(self :: $root_path)) {
$error = False;
while (($file = readdir($dh)) !== false) {
if (
!is_dir(self :: $root_path . '/' . $file) ||
in_array($file, array('.', '..'))
)
continue;
if (is_link(self :: $root_path . '/' . $file)) {
$real_lang_dir = readlink(self :: $root_path . '/' . $file);
if (dirname($real_lang_dir) != '.' || !is_dir(self :: $root_path . '/' . $real_lang_dir))
continue;
$lang = $file;
Log :: debug(_("Lang alias symlink found: %s -> %s"), $lang, $real_lang_dir);
// Create JSON catalog symlink (if not exists)
$js_link = "$root_dir_path/public_html/translations/$lang.js";
$link_target = "$real_lang_dir.js";
if (!file_exists($js_link)) {
if (symlink($link_target, $js_link)) {
Log :: info(_("JSON catalog symlink for %s -> %s created (%s)"),
$lang, $real_lang_dir, $js_link);
}
else {
Log :: error(_("Fail to create JSON catalog symlink for %s -> %s (%s)"),
$lang, $real_lang_dir, $js_link);
$error = True;
}
}
elseif (readlink($js_link) == $link_target) {
Log :: debug(_("JSON catalog symlink for %s -> %s already exist (%s)"),
$lang, $real_lang_dir, $js_link);
}
else {
Log :: warning(
_("JSON 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(_("Lang directory '%s' found"), $file);
// Check LC_MESSAGES directory exists
$lang = $file;
$lang_dir = self :: $root_path . '/' . $file . '/LC_MESSAGES' ;
if (!is_dir($lang_dir)) {
Log :: debug(_("LC_MESSAGES directory not found in lang '%s' directory, ignore it."),
$lang);
continue;
}
// Test .PO file is present
$po_file = $lang_dir . '/' . self :: TEXT_DOMAIN . '.po';
if (!is_file($po_file)) {
Log :: debug(_("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(
_("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 JSON catalog file
$json_catalog = self :: po2json($lang, $po_file);
$js_file = "$root_dir_path/public_html/translations/$lang.js";
if(!$fd = fopen($js_file, 'w')) {
Log :: error(_("Fail to open %s JSON catalog file in write mode (%s)."),
$lang, $js_file);
$error = True;
}
elseif (fwrite($fd, sprintf("translations_data = %s;", $json_catalog)) === false) {
Log :: error(_("Fail to write %s JSON catalog in file (%s)."),
$lang, $js_file);
$error = True;
}
else {
Log :: info(_("%s JSON catalog writed (%s)."), $lang, $js_file);
}
}
closedir($dh);
return !$error;
}
Log :: fatal(_("Fail to open root lang directory (%s)."), $root_dir_path);
return false;
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab