src/Parser.php
<?php
namespace PoParser;
use Exception;
class Parser
{
/**
* @var array
*/
protected $headers = array();
/**
* @var Entry[]
*/
protected $entries = array();
/**
* @var array
*/
protected $entriesAsArrays = array();
/**
* @var string
*/
protected $state;
/**
* @var array
*/
protected $rawEntries;
/**
* @var array
*/
protected $currentEntry;
/**
* @var boolean
*/
protected $justNewEntry;
/**
* @return Entry[]
*/
public function getEntries()
{
return $this->entries;
}
/**
* @return array
*/
public function getEntriesAsArrays()
{
return $this->entriesAsArrays;
}
/**
* Reads and parses strings in a .po file.
*
* return An array of entries located in the file:
* Format: array(
* 'msgid' => <string> ID of the message.
* 'msgctxt' => <string> Message context.
* 'msgstr' => <string> Message translation.
* 'tcomment' => <string> Comment from translator.
* 'ccomment' => <string> Extracted comments from code.
* 'references' => <array> Location of string in code.
* 'obsolete' => <bool> Is the message obsolete?
* 'fuzzy' => <bool> Is the message "fuzzy"?
* 'flags' => <array> Flags of the entry. Internal usage.
* )
*
* #~ (old entry)
* # @ default
* #, fuzzy
* #~ msgid "Editar datos"
* #~ msgstr "editar dades"
*
* @param string $filePath
* @throws Exception
* @return array|bool
*/
public function read($filePath)
{
$this->rawEntries = array();
$this->currentEntry = $this->createNewEntryAsArray();
$this->state = null;
$this->justNewEntry = false;
$handle = $this->openFile($filePath);
while (!feof($handle)) {
$line = trim(fgets($handle));
$this->processLine($line);
}
fclose($handle);
$this->addFinalEntry();
$this->prepareResults();
return $this->entriesAsArrays;
}
/**
* @param string $line
*
* @throws Exception
*/
protected function processLine($line)
{
if ($line === '') {
$this->handleBlankLine();
return;
}
$this->justNewEntry = false;
$data = $this->parseLine($line);
if ($data['key'][0] === '#') {
$this->handleComment($data);
return;
}
$this->handleOtherCases($data, $line);
}
/**
*
*/
protected function handleBlankLine()
{
if ($this->justNewEntry) {
// Two consecutive blank lines
return;
}
// A new entry is found
$this->rawEntries[] = $this->currentEntry;
$this->currentEntry = $this->createNewEntryAsArray();
$this->state = null;
$this->justNewEntry = true;
}
/**
* @param $line
*
* @return array
*/
protected function parseLine($line)
{
$split = preg_split('/\s/', $line, 2);
return array(
'key' => $split[0],
'value' => isset($split[1]) ? $split[1] : null
);
}
/**
* @param $data
*
* @return array
*/
protected function parseFlags($data)
{
return preg_split('/,\s*/', $data);
}
/**
* @param $data
*/
protected function handleComment($data)
{
switch ($data['key']) {
case '#:':
$this->currentEntry['references'][] = addslashes($data['value']);
break;
case '#,':
//flag
$this->currentEntry['flags'] = $this->parseFlags($data['value']);
$this->currentEntry['fuzzy'] = in_array('fuzzy', $this->currentEntry['flags'], true);
break;
case '#':
$this->currentEntry['tcomment'] = $data['value'];
break;
case '#.':
$this->currentEntry['ccomment'] = $data['value'];
break;
case '#|':
//msgid previous-untranslated-string
// start a new entry
break;
case '#@':
// ignore #@ default
$this->currentEntry['@'] = $data['value'];
break;
case '#~':
$this->processObsoleteEntry($data['value']);
break;
default:
break;
}
}
/**
* @param $data
* @param $rawLine
*
* @throws Exception
*/
protected function handleOtherCases($data, $rawLine)
{
switch ($data['key']) {
case 'msgctxt':
case 'msgid':
case 'msgid_plural':
case 'msgstr':
$this->state = $data['key'];
$this->addEntryData($data['value']);
break;
default:
if (strpos($data['key'], 'msgstr[') !== false) {
// translated plurals
$this->state = 'msgstr';
$this->addEntryData($data['value']);
} else {
$this->processContinuedLineInSameState($rawLine);
}
break;
}
}
/**
* @param string $line
*
* @throws Exception
*/
protected function processContinuedLineInSameState($line)
{
switch ($this->state) {
case 'msgctxt':
case 'msgid':
case 'msgid_plural':
if (is_string($this->currentEntry[$this->state])) {
// Convert it to array
$this->currentEntry[$this->state] = array($this->currentEntry[$this->state]);
}
$this->currentEntry[$this->state][] = $line;
break;
case 'msgstr':
$this->currentEntry['msgstr'][] = trim($line, '"');
break;
default:
throw new Exception('Parse error!');
}
}
/**
* @param $data
*/
protected function processObsoleteEntry($data)
{
$this->currentEntry['obsolete'] = true;
$tmpParts = explode(' ', $data);
$tmpKey = $tmpParts[0];
$str = implode(' ', array_slice($tmpParts, 1));
switch ($tmpKey) {
case 'msgid':
$this->currentEntry['msgid'] = trim($str, '"');
break;
case 'msgstr':
$this->currentEntry['msgstr'][] = trim($str, '"');
break;
default:
break;
}
}
/**
* @param $value
*/
protected function addEntryData($value)
{
if ($this->state === 'msgstr') {
$this->currentEntry[$this->state][] = $value;
} else {
$this->currentEntry[$this->state] = $value;
}
}
/**
*
*/
protected function addFinalEntry()
{
if ($this->state == 'msgstr' || $this->currentEntry['obsolete']) {
$this->rawEntries[] = $this->currentEntry;
}
}
/**
* Cleanup data, merge multiline entries, reindex hash for ksort
*
* @return bool
*/
protected function prepareResults()
{
$this->entriesAsArrays = array();
$this->entries = array();
$this->headers = array();
$counter = 0;
foreach ($this->rawEntries as $entry) {
$entry = $this->prepareEntry($entry, $counter);
$id = $this->getMsgId($entry);
$this->entriesAsArrays[$id] = $entry;
$this->entries[$id] = new Entry($entry);
$counter++;
}
return true;
}
/**
* @param $entry
*
* @return string
*/
protected function getMsgId($entry)
{
return is_array($entry['msgid']) ? implode('', $entry['msgid']) : $entry['msgid'];
}
/**
* @param $entry
* @param $index
*
* @return array
*/
protected function prepareEntry($entry, $index)
{
foreach ($entry as &$fieldValue) {
$fieldValue = $this->clean($fieldValue);
}
$id = $this->getMsgId($entry);
if ($index === 0 && $id === '') {
//header entry
$entry['header'] = true;
$this->setHeaders($this->parseHeaders($entry));
}
return $entry;
}
/**
* @return array
*/
protected function createNewEntryAsArray()
{
return array(
'msgctxt' => '',
'header' => false,
'obsolete' => false,
'fuzzy' => false,
'flags' => array(),
'references' => array(),
'ccomment' => '',
'tcomment' => '',
);
}
/**
* @param string $filePath
*
* @throws Exception
* @return resource
*/
protected function openFile($filePath)
{
if (empty($filePath)) {
throw new Exception('Input file not defined.');
} elseif (!file_exists($filePath)) {
throw new Exception("File does not exist: {$filePath}");
}
$handle = @fopen($filePath, 'r');
if (false === $handle) {
throw new Exception("Unable to open file for reading: {$filePath}");
}
return $handle;
}
/**
* @param array $entry
*
* @return array
*/
protected function parseHeaders(array $entry)
{
$headers = array();
if (!is_array($entry['msgstr'])) {
return $headers;
}
foreach ($entry['msgstr'] as $headerRaw) {
$parts = explode(':', $headerRaw);
if (count($parts) < 2) {
continue;
}
$parts[1] = ltrim($parts[1]);
$values = array_slice($parts, 1);
$headerValue = rtrim(implode(':', $values));
$headers[$parts[0]] = $headerValue;
}
return $headers;
}
/**
* set all entries at once
*
* @param array $entries
*/
public function setEntries(array $entries)
{
$this->entriesAsArrays = $entries;
}
/**
* Helper for the update-functions by deleting the fuzzy flag
*
* @param $msgid string msgid of entry
*
* @throws Exception
*/
protected function removeFuzzyFlagForMsgId($msgid)
{
if (!isset($this->entriesAsArrays[$msgid])) {
throw new Exception('Entry does not exist');
}
if ($this->entriesAsArrays[$msgid]['fuzzy']) {
$flags = $this->entriesAsArrays[$msgid]['flags'];
unset($flags[array_search('fuzzy', $flags, true)]);
$this->entriesAsArrays[$msgid]['flags'] = $flags;
$this->entriesAsArrays[$msgid]['fuzzy'] = false;
}
}
/**
* Allows modification of all translations of an entry
*
* @param $msgid string msgid of the entry which should be updated
* @param $translation array of strings new Translation for all msgstr by msgid
*
* @throws Exception
*/
public function updateEntries($msgid, $translation)
{
if (
!isset($this->entriesAsArrays[$msgid])
|| !is_array($translation)
|| sizeof($translation) != sizeof($this->entriesAsArrays[$msgid]['msgstr'])
) {
throw new Exception('Cannot update entry translation');
}
$this->removeFuzzyFlagForMsgId($msgid);
$this->entriesAsArrays[$msgid]['msgstr'] = $translation;
}
/**
* Allows modification of a single translation of an entry
*
* @param $msgid string msgid of the entry which should be updated
* @param $translation string new translation for an msgstr by msgid
* @param $positionMsgstr integer spezification which of the msgstr
* should be changed
*
* @throws Exception
*/
public function updateEntry($msgid, $translation, $positionMsgstr = 0)
{
if (
!isset($this->entriesAsArrays[$msgid])
|| !is_string($translation)
|| !isset($this->entriesAsArrays[$msgid]['msgstr'][$positionMsgstr])
) {
throw new Exception('Cannot update entry translation');
}
$this->removeFuzzyFlagForMsgId($msgid);
$this->entriesAsArrays[$msgid]['msgstr'][$positionMsgstr] = $translation;
}
/**
* Write entries into the po file.
*
* @param string $filePath
* @throws Exception
*/
public function write($filePath)
{
$writer = new Writer();
$writer->write($filePath, $this->entriesAsArrays);
}
/**
*
*/
public function clearFuzzy()
{
foreach ($this->entriesAsArrays as &$entry) {
if ($entry['fuzzy'] === true) {
$flags = $entry['flags'];
$entry['flags'] = str_replace('fuzzy', '', $flags);
$entry['fuzzy'] = false;
$entry['msgstr'] = array('');
}
}
}
/**
* @param $value
*
* @return array|string
*/
public function clean($value)
{
if ($value === true || $value === false) {
return $value;
} elseif (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->clean($v);
}
} else {
$value = preg_replace('/^\"|\"$/', '', $value);
$value = stripcslashes($value);
}
return $value;
}
/**
* @return array
*/
public function getHeaders()
{
return $this->headers;
}
/**
* @param array $headers
*/
public function setHeaders($headers)
{
$this->headers = $headers;
}
}