Reads a line from the PO stream and stores data internally.
Expands $this->_current_item based on new data for the current item. If this line ends the current item, it is saved with setItemFromArray() with data from $this->_current_item.
An internal state machine is maintained in this reader using $this->_context as the reading state. PO items are inbetween COMMENT states (when items have at least one line or comment inbetween them or indicated by MSGSTR or MSGSTR_ARR followed immediately by an MSGID or MSGCTXT (when items closely follow each other).
FALSE if an error was logged, NULL otherwise. The errors are considered non-blocking, so reading can continue, while the errors are collected for later presentation.
private function readLine() {
// Read a line and set the stream finished indicator if it was not
// possible anymore.
$line = fgets($this->_fd);
$this->_finished = $line === FALSE;
if (!$this->_finished) {
if ($this->_line_number == 0) {
// The first line might come with a UTF-8 BOM, which should be removed.
$line = str_replace("", '', $line);
// Current plurality for 'msgstr[]'.
$this->_current_plural_index = 0;
}
// Track the line number for error reporting.
$this->_line_number++;
// Initialize common values for error logging.
$log_vars = array(
'%uri' => $this
->getURI(),
'%line' => $this->_line_number,
);
$t = get_t();
// Trim away the linefeed. \\n might appear at the end of the string if
// another line continuing the same string follows. We can remove that.
$line = trim(strtr($line, array(
"\\\n" => "",
)));
if (!strncmp('#', $line, 1)) {
// Lines starting with '#' are comments.
if ($this->_context == 'COMMENT') {
// Already in comment context, add to current comment.
$this->_current_item['#'][] = substr($line, 1);
}
elseif ($this->_context == 'MSGSTR' || $this->_context == 'MSGSTR_ARR') {
// We are currently in string context, save current item.
$this
->setItemFromArray($this->_current_item);
// Start a new entry for the comment.
$this->_current_item = array();
$this->_current_item['#'][] = substr($line, 1);
$this->_context = 'COMMENT';
return;
}
else {
// A comment following any other context is a syntax error.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" was expected but not found on line %line.', $log_vars);
return FALSE;
}
return;
}
elseif (!strncmp('msgid_plural', $line, 12)) {
// A plural form for the current source string.
if ($this->_context != 'MSGID') {
// A plural form can only be added to an msgid directly.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgid_plural" was expected but not found on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid_plural' and trim away whitespace.
$line = trim(substr($line, 12));
// Only the plural source string is left, parse it.
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// The plural form must be wrapped in quotes.
$this->_errors[] = $t('The translation stream %uri contains a syntax error on line %line.', $log_vars);
return FALSE;
}
// Append the plural source to the current entry.
if (is_string($this->_current_item['msgid'])) {
// The first value was stored as string. Now we know the context is
// plural, it is converted to array.
$this->_current_item['msgid'] = array(
$this->_current_item['msgid'],
);
}
$this->_current_item['msgid'][] = $quoted;
$this->_context = 'MSGID_PLURAL';
return;
}
elseif (!strncmp('msgid', $line, 5)) {
// Starting a new message.
if ($this->_context == 'MSGSTR' || $this->_context == 'MSGSTR_ARR') {
// We are currently in string context, save current item.
$this
->setItemFromArray($this->_current_item);
// Start a new context for the msgid.
$this->_current_item = array();
}
elseif ($this->_context == 'MSGID') {
// We are currently already in the context, meaning we passed an id with no data.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgid" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgid' and trim away whitespace.
$line = trim(substr($line, 5));
// Only the message id string is left, parse it.
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// The message id must be wrapped in quotes.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgid" on line %line.', $log_vars, $log_vars);
return FALSE;
}
$this->_current_item['msgid'] = $quoted;
$this->_context = 'MSGID';
return;
}
elseif (!strncmp('msgctxt', $line, 7)) {
// Starting a new context.
if ($this->_context == 'MSGSTR' || $this->_context == 'MSGSTR_ARR') {
// We are currently in string context, save current item.
$this
->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif (!empty($this->_current_item['msgctxt'])) {
// A context cannot apply to another context.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgctxt" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgctxt' and trim away whitespaces.
$line = trim(substr($line, 7));
// Only the msgctxt string is left, parse it.
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// The context string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgctxt" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgctxt'] = $quoted;
$this->_context = 'MSGCTXT';
return;
}
elseif (!strncmp('msgstr[', $line, 7)) {
// A message string for a specific plurality.
if ($this->_context != 'MSGID' && $this->_context != 'MSGCTXT' && $this->_context != 'MSGID_PLURAL' && $this->_context != 'MSGSTR_ARR') {
// Plural message strings must come after msgid, msgxtxt,
// msgid_plural, or other msgstr[] entries.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr[]" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Ensure the plurality is terminated.
if (strpos($line, ']') === FALSE) {
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
// Extract the plurality.
$frombracket = strstr($line, '[');
$this->_current_plural_index = substr($frombracket, 1, strpos($frombracket, ']') - 1);
// Skip to the next whitespace and trim away any further whitespace,
// bringing $line to the message text only.
$line = trim(strstr($line, " "));
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr[]" on line %line.', $log_vars);
return FALSE;
}
if (!isset($this->_current_item['msgstr']) || !is_array($this->_current_item['msgstr'])) {
$this->_current_item['msgstr'] = array();
}
$this->_current_item['msgstr'][$this->_current_plural_index] = $quoted;
$this->_context = 'MSGSTR_ARR';
return;
}
elseif (!strncmp("msgstr", $line, 6)) {
// A string pair for an msgidid (with optional context).
if ($this->_context != 'MSGID' && $this->_context != 'MSGCTXT') {
// Strings are only valid within an id or context scope.
$this->_errors[] = $t('The translation stream %uri contains an error: "msgstr" is unexpected on line %line.', $log_vars);
return FALSE;
}
// Remove 'msgstr' and trim away away whitespaces.
$line = trim(substr($line, 6));
// Only the msgstr string is left, parse it.
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// The string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: invalid format for "msgstr" on line %line.', $log_vars);
return FALSE;
}
$this->_current_item['msgstr'] = $quoted;
$this->_context = 'MSGSTR';
return;
}
elseif ($line != '') {
// Anything that is not a token may be a continuation of a previous token.
$quoted = $this
->parseQuoted($line);
if ($quoted === FALSE) {
// This string must be quoted.
$this->_errors[] = $t('The translation stream %uri contains an error: string continuation expected on line %line.', $log_vars);
return FALSE;
}
// Append the string to the current item.
if ($this->_context == 'MSGID' || $this->_context == 'MSGID_PLURAL') {
if (is_array($this->_current_item['msgid'])) {
// Add string to last array element for plural sources.
$last_index = count($this->_current_item['msgid']) - 1;
$this->_current_item['msgid'][$last_index] .= $quoted;
}
else {
// Singular source, just append the string.
$this->_current_item['msgid'] .= $quoted;
}
}
elseif ($this->_context == 'MSGCTXT') {
// Multiline context name.
$this->_current_item['msgctxt'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR') {
// Multiline translation string.
$this->_current_item['msgstr'] .= $quoted;
}
elseif ($this->_context == 'MSGSTR_ARR') {
// Multiline plural translation string.
$this->_current_item['msgstr'][$this->_current_plural_index] .= $quoted;
}
else {
// No valid context to append to.
$this->_errors[] = $t('The translation stream %uri contains an error: unexpected string on line %line.', $log_vars);
return FALSE;
}
return;
}
}
// Empty line read or EOF of PO stream, close out the last entry.
if ($this->_context == 'MSGSTR' || $this->_context == 'MSGSTR_ARR') {
$this
->setItemFromArray($this->_current_item);
$this->_current_item = array();
}
elseif ($this->_context != 'COMMENT') {
$this->_errors[] = $t('The translation stream %uri ended unexpectedly at line %line.', $log_vars);
return FALSE;
}
}