PHP Gettext с пользовательским сканированием файлов для ключей сообщений и фильтром Twig для эха
У меня проблема с получением перевода строки из пользовательских сгенерированных.po и.mo файлов.
Прежде всего, я установил все настройки gettext
$config['text_domain'] = 'wc';
$config['languages'] = array(
'hr' => array(
'locale' => 'hr_HR.utf8',
'date_format' => '',
'time_format' => '',
'timezone' => '',
'lang_name' => 'Croatian'
),
'en' => array(
'locale' => 'en_EN',
'date_format' => '',
'time_format' => '',
'timezone' => '',
'lang_name' => 'English'
),
'rs' => array(
'locale' => 'rs_RS',
'date_format' => '',
'time_format' => '',
'timezone' => '',
'lang_name' => 'Serbian'
)
);
$config['current_lang'] = 'hr';
putenv('LC_ALL=' . $config['languages'][$config['current_lang']]['locale']);
setlocale(LC_ALL, $config['languages'][$config['current_lang']]['locale']);
bindtextdomain($config['text_domain'], '/var/www/webcore.lan/public_html/Languages');
bind_textdomain_codeset($config['text_domain'], 'UTF-8');
textdomain($config['text_domain']);
Структура каталогов Gettext:
public_html / Языки / hr_HR.utf8 / LC_MESSAGES /
(файлы mo и po хранятся в формате: domain_locale.po & domain_locale.mo), поэтому в этом случае:
wc_hr_HR.utf8.mo
wc_hr_HR_utf8.po
Для рендеринга шаблона я использую Twig2 с пользовательским фильтром
$gettext = new Twig_SimpleFunction('message', function($message) {
return _($message);
});
$twig->addFunction($gettext);
Итак, мой пример шаблона представления, который использует этот фильтр сообщений:
{{ message("test") }}
И теперь я делаю в CMS функцию, которая сканирует все шаблоны представлений, затем ищет функцию ветки "message" и извлекает ее атрибут (который всегда является ключом сообщения), затем дает пользователю возможность написать свой собственный перевод и затем генерирует файлы.po и.mo,
Это контроллер, который делает это (начало этой операции в методе 'сканирования')
class LanguageController extends Controller
{
protected $_container;
protected $_db;
protected $_twig;
protected $_config;
protected $_data = array();
public function __construct($c)
{
$this->_container = $c;
$this->_db = $c->get('db_mysqli');
$this->_twig = $c->get('Twig');
$this->_config = $c->get('settings');
$this->_data['page_template'] = 'Languages/index.twig';
$this->_data['base_url'] = $this->_config['base_url'];
$this->_data['default_lang'] = $this->_config['current_lang'];
$this->_data['languages'] = $this->_config['languages'];
}
public function index(Request $request, Response $response, $args)
{
$get = $request->getQueryParams();
$this->_twig->display('layout.twig', $this->_data);
}
public function scan(Request $request, Response $response, $args)
{
$base_folder = APP . DIRECTORY_SEPARATOR . APP_DIRECTORY . DIRECTORY_SEPARATOR . APP_VIEW_DIRECTORY;
$scaned = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base_folder));
$messageKeys = array();
foreach($scaned as $file)
{
if($file->isDir()) {
continue;
}
$file_content = file_get_contents($file);
$messageKeys = array_unique(array_merge($messageKeys, $this->get_string_between($file_content, 'message("', '")')));
}
//echo '<pre>' .print_r($messageKeys, true) . '</pre>';
// Sada idi po svakom jeziku i provjeri dali .po file postoji
// ukoliko ne postoji kreiraj ga prvi puta sa message keyevima, a za defaultni jezik
// će nađeni message keyevi ujedno biti i prijevod
foreach($this->_config['languages'] as $langKey => $lang)
{
$poFile = LANG_DIRECTORY . DIRECTORY_SEPARATOR . $lang['locale'] . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . $lang['locale'] . '.po';
$moFile = LANG_DIRECTORY . DIRECTORY_SEPARATOR . $lang['locale'] . DIRECTORY_SEPARATOR . 'LC_MESSAGES' . DIRECTORY_SEPARATOR . $lang['locale'] . '.mo';
if(!file_exists($poFile))
{
$po = fopen($poFile, "w");
$poTemplate =
'msgid ""
msgstr ""
"Language: '.$lang['locale'].'\n"
"Project-Id-Version: '.$this->_config['text_domain'].'\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 1.8.1\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;"
"X-Textdomain-Support: yes\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: *.js\n"
';
foreach($messageKeys as $key)
{
$quotedKey = '"' . $key . '"';
$translation = '"' . $key . '-a-a-a--a-a"';
$poTemplate .= '
msgid '.$quotedKey;
$poTemplate .= '
msgstr '.($this->_config['current_lang'] == $langKey ? $translation : '""').'
';
}
fwrite($po, $poTemplate);
fclose($po);
chmod($poFile, 0777);
$this->phpmo_convert($poFile, $moFile);
}
}
//echo '<pre>' .print_r($messageKeys, true) . '</pre>';
exit;
}
public function get_string_between($string, $start, $end)
{
$last_end = 0;
$matches = array();
while (($ini = strpos($string, $start, $last_end)) !== false)
{
$ini += strlen($start);
$len = strpos($string,$end,$ini) - $ini;
$matches[] = substr($string, $ini, $len);
$last_end = $ini + $len + strlen($end);
}
return $matches;
}
public function phpmo_convert($input, $output = false) {
if ( !$output )
$output = str_replace( '.po', '.mo', $input );
$hash = $this->phpmo_parse_po_file( $input );
if ( $hash === false ) {
return false;
} else {
$this->phpmo_write_mo_file( $hash, $output );
return true;
}
}
public function phpmo_clean_helper($x) {
if (is_array($x)) {
foreach ($x as $k => $v) {
$x[$k] = $this->phpmo_clean_helper($v);
}
} else {
if ($x[0] == '"')
$x = substr($x, 1, -1);
$x = str_replace("\"\n\"", '', $x);
$x = str_replace('$', '\\$', $x);
}
return $x;
}
public function phpmo_parse_po_file($in) {
$fh = fopen($in, 'r');
if ($fh === false) {
return false;
}
$hash = array ();
$temp = array ();
$state = null;
$fuzzy = false;
while(($line = fgets($fh, 65536)) !== false) {
$line = trim($line);
if ($line === '')
continue;
list ($key, $data) = preg_split('/\s/', $line, 2);
switch ($key) {
case '#,' :
$fuzzy = in_array('fuzzy', preg_split('/,\s*/', $data));
case '#' :
case '#.' :
case '#:' :
case '#|' :
if (sizeof($temp) && array_key_exists('msgid', $temp) && array_key_exists('msgstr', $temp)) {
if (!$fuzzy)
$hash[] = $temp;
$temp = array ();
$state = null;
$fuzzy = false;
}
break;
case 'msgctxt' :
case 'msgid' :
case 'msgid_plural' :
$state = $key;
$temp[$state] = $data;
break;
case 'msgstr' :
$state = 'msgstr';
$temp[$state][] = $data;
break;
default :
if (strpos($key, 'msgstr[') !== FALSE) {
$state = 'msgstr';
$temp[$state][] = $data;
} else {
switch ($state) {
case 'msgctxt' :
case 'msgid' :
case 'msgid_plural' :
$temp[$state] .= "\n" . $line;
break;
case 'msgstr' :
$temp[$state][sizeof($temp[$state]) - 1] .= "\n" . $line;
break;
default :
// parse error
fclose($fh);
return FALSE;
}
}
break;
}
}
fclose($fh);
if ($state == 'msgstr')
$hash[] = $temp;
$temp = $hash;
$hash = array ();
foreach ($temp as $entry) {
foreach ($entry as & $v) {
$v = $this->phpmo_clean_helper($v);
if ($v === FALSE) {
return FALSE;
}
}
$hash[$entry['msgid']] = $entry;
}
return $hash;
}
public function phpmo_write_mo_file($hash, $out) {
ksort($hash, SORT_STRING);
$mo = '';
$offsets = array ();
$ids = '';
$strings = '';
foreach ($hash as $entry) {
$id = $entry['msgid'];
if (isset ($entry['msgid_plural']))
$id .= "\x00" . $entry['msgid_plural'];
if (array_key_exists('msgctxt', $entry))
$id = $entry['msgctxt'] . "\x04" . $id;
$str = implode("\x00", $entry['msgstr']);
$offsets[] = array (
strlen($ids
), strlen($id), strlen($strings), strlen($str));
$ids .= $id . "\x00";
$strings .= $str . "\x00";
}
$key_start = 7 * 4 + sizeof($hash) * 4 * 4;
$value_start = $key_start +strlen($ids);
$key_offsets = array ();
$value_offsets = array ();
foreach ($offsets as $v) {
list ($o1, $l1, $o2, $l2) = $v;
$key_offsets[] = $l1;
$key_offsets[] = $o1 + $key_start;
$value_offsets[] = $l2;
$value_offsets[] = $o2 + $value_start;
}
$offsets = array_merge($key_offsets, $value_offsets);
$mo .= pack('Iiiiiii', 0x950412de,
0,
sizeof($hash),
7 * 4,
7 * 4 + sizeof($hash) * 8,
0,
$key_start
);
foreach ($offsets as $offset)
$mo .= pack('i', $offset);
$mo .= $ids;
$mo .= $strings;
file_put_contents($out, $mo);
}
}
Когда мои файлы генерируются, они выглядят примерно так:
Файл wc_hr_HR.utf8.po:
msgid ""
msgstr ""
"Language: hr_HR.utf8\n"
"Project-Id-Version: wc\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 1.8.1\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: __;_e;_n:1,2;_x:1,2c;_ex:1,2c;_nx:4c,1,2;esc_attr__;"
"X-Textdomain-Support: yes\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPathExcluded-0: *.js\n"
msgid "test"
msgstr "test-a-a-a--a-a"
А для файла.mo я использую все эти сложные алгоритмы, но я попытался сгенерировать.mo с помощью командной строки, используя
msgfmt hr_HR.utf8.po -o hr_HR.utf8.mo
И я до сих пор не получаю переведенную строку {{ message("test") }}
ОС: Ubuntu 16.04 LTS PHP Версия: 7.1.12 PHP модули:
[PHP Modules]
bcmath
calendar
Core
ctype
curl
date
dom
exif
fileinfo
filter
ftp
gd
gettext
hash
iconv
intl
json
libxml
mbstring
mcrypt
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
Phar
posix
readline
Reflection
session
shmop
SimpleXML
soap
sockets
SPL
standard
sysvmsg
sysvsem
sysvshm
tokenizer
wddx
xml
xmlreader
xmlwriter
xsl
Zend OPcache
zlib
[Zend Modules]
Zend OPcache
Локали (основанные на 'local -a':
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
hr_HR.utf8
POSIX
Извините за кучу кода и длинный вопрос, я очень устал. Не могу понять проблему. Может кто-нибудь помочь?
Извините за неубедительный английский.