<?php
namespace Drupal\tmgmt_file\Plugin\tmgmt_file\Format;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\tmgmt\Entity\Job;
use Drupal\tmgmt\Entity\JobItem;
use Drupal\tmgmt\JobInterface;
use Drupal\tmgmt\JobItemInterface;
use Drupal\tmgmt_file\Format\FormatInterface;
use Drupal\tmgmt_file\RecursiveDOMIterator;
class Xliff extends \XMLWriter implements FormatInterface {
use MessengerTrait;
protected $job;
protected $importedXML;
protected $importedTransUnits;
protected $configuration;
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
$this->configuration = $configuration;
}
protected function addItem(JobItemInterface $item) {
$this
->startElement('group');
$this
->writeAttribute('id', $item
->id());
$this
->writeElement('note', $item
->getSourceLabel());
$data = \Drupal::service('tmgmt.data')
->filterTranslatable($item
->getData());
foreach ($data as $key => $element) {
$this
->addTransUnit($item
->id() . '][' . $key, $element, $this->job);
}
$this
->endElement();
}
protected function addTransUnit($key, $element, JobInterface $job) {
$key_array = \Drupal::service('tmgmt.data')
->ensureArrayKey($key);
$this
->startElement('trans-unit');
$this
->writeAttribute('id', $key);
$this
->writeAttribute('resname', $key);
if (isset($element['#max_length'])) {
$this
->writeAttribute('size-unit', 'char');
$this
->writeAttribute('maxwidth', $element['#max_length']);
}
$this
->startElement('source');
$this
->writeAttribute('xml:lang', $this->job
->getRemoteSourceLanguage());
$this
->writeData($element['#text'], $key_array);
$this
->endElement();
$this
->startElement('target');
$this
->writeAttribute('xml:lang', $this->job
->getRemoteTargetLanguage());
if (!empty($element['#translation']['#text'])) {
$this
->writeData($element['#text'], $key_array);
}
elseif (!empty($this->configuration['target']) && $this->configuration['target'] === 'source') {
$this
->writeData($element['#text'], $key_array);
}
$this
->endElement();
if (isset($element['#label'])) {
$this
->writeElement('note', \Drupal::service('tmgmt.data')
->itemLabel($element));
}
$this
->endElement();
}
protected function writeData($text, array $key_array) {
if ($this->job
->getSetting('xliff_cdata')) {
return $this
->writeCdata(trim($text));
}
if ($this->job
->getSetting('xliff_processing')) {
return $this
->writeRaw($this
->processForExport($text, $key_array));
}
return $this
->text($text);
}
public function export(JobInterface $job, $conditions = array()) {
$this->job = $job;
$this
->openMemory();
$this
->setIndent(TRUE);
$this
->setIndentString(' ');
$this
->startDocument('1.0', 'UTF-8');
$this
->startElement('xliff');
$this
->writeAttribute('version', '1.2');
$this
->writeAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
$this
->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$this
->writeAttribute('xsi:schemaLocation', 'urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-strict.xsd');
$this
->startElement('file');
$this
->writeAttribute('original', 'xliff-core-1.2-strict.xsd');
$this
->writeAttribute('source-language', $job
->getRemoteSourceLanguage());
$this
->writeAttribute('target-language', $job
->getRemoteTargetLanguage());
$this
->writeAttribute('datatype', 'plaintext');
$this
->writeAttribute('date', date('Y-m-d\\Th:m:i\\Z'));
$this
->startElement('header');
$this
->startElement('phase-group');
$this
->startElement('phase');
$this
->writeAttribute('tool-id', 'tmgmt');
$this
->writeAttribute('phase-name', 'extraction');
$this
->writeAttribute('process-name', 'extraction');
$this
->writeAttribute('job-id', $job
->id());
$this
->endElement();
$this
->endElement();
$this
->startElement('tool');
$this
->writeAttribute('tool-id', 'tmgmt');
$this
->writeAttribute('tool-name', 'Drupal Translation Management Tools');
$this
->endElement();
$this
->endElement();
$this
->startElement('body');
foreach ($job
->getItems($conditions) as $item) {
$this
->addItem($item);
}
$this
->endElement();
$this
->endElement();
$this
->endElement();
$this
->endDocument();
return $this
->outputMemory();
}
public function import($imported_file, $is_file = TRUE) {
if ($this
->getImportedXML($imported_file, $is_file) === FALSE) {
return FALSE;
}
$phase = $this->importedXML
->xpath("//xliff:phase[@phase-name='extraction']");
$phase = reset($phase);
$job = Job::load((string) $phase['job-id']);
return \Drupal::service('tmgmt.data')
->unflatten($this
->getImportedTargets($job));
}
public function validateImport($imported_file, $is_file = TRUE) {
$xml = $this
->getImportedXML($imported_file, $is_file);
if ($xml === FALSE) {
$this
->messenger()
->addError(t('The imported file is not a valid XML.'));
return FALSE;
}
$phase = $xml
->xpath("//xliff:phase[@phase-name='extraction']");
if ($phase) {
$phase = reset($phase);
}
else {
$this
->messenger()
->addError(t('The imported file is missing required XLIFF phase information.'));
return FALSE;
}
if (!isset($phase['job-id'])) {
$this
->messenger()
->addError(t('The imported file does not contain a job reference.'));
return FALSE;
}
$job = Job::load((int) $phase['job-id']);
if (empty($job)) {
$this
->messenger()
->addError(t('The imported file job id @file_tjid is not available.', array(
'@file_tjid' => $phase['job-id'],
)));
return FALSE;
}
if (!isset($xml->file['source-language']) || $job
->getRemoteSourceLanguage() != $xml->file['source-language']) {
$job
->addMessage('The imported file source language @file_language does not match the job source language @job_language.', array(
'@file_language' => empty($xml->file['source-language']) ? t('none') : $xml->file['source-language'],
'@job_language' => $job
->getRemoteSourceLanguage(),
), 'error');
return FALSE;
}
if (!isset($xml->file['target-language']) || $job
->getRemoteTargetLanguage() != $xml->file['target-language']) {
$job
->addMessage('The imported file target language @file_language does not match the job target language @job_language.', array(
'@file_language' => empty($xml->file['target-language']) ? t('none') : $xml->file['target-language'],
'@job_language' => $job
->getRemoteTargetLanguage(),
), 'error');
return FALSE;
}
$targets = $this
->getImportedTargets($job);
if (empty($targets)) {
$job
->addMessage('The imported file seems to be missing translation.', 'error');
return FALSE;
}
if (!$job
->getSetting('xliff_processing')) {
return $job;
}
$reader = new \XMLReader();
$xliff_validation = $job
->getSetting('xliff_validation');
foreach ($targets as $id => $target) {
$array_key = \Drupal::service('tmgmt.data')
->ensureArrayKey($id);
$job_item = JobItem::load(array_shift($array_key));
$count = 0;
$reader
->XML('<translation>' . $target['#text'] . '</translation>');
while ($reader
->read()) {
if (in_array($reader->name, array(
'translation',
'#text',
))) {
continue;
}
$count++;
}
if (!isset($xliff_validation[$id]) || $xliff_validation[$id] != $count) {
$job_item
->addMessage('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.', array(
'%key' => \Drupal::service('tmgmt.data')
->ensureStringKey($array_key),
));
}
}
return $job;
}
protected function getImportedXML($imported_file, $is_file = TRUE) {
if (empty($this->importedXML)) {
if ($is_file) {
$imported_file = file_get_contents($imported_file);
}
$this->importedXML = simplexml_load_string($imported_file);
if ($this->importedXML === FALSE) {
$this
->messenger()
->addError(t('The imported file is not a valid XML.'));
return FALSE;
}
$this->importedXML
->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
}
return $this->importedXML;
}
protected function getImportedTargets(JobInterface $job) {
if (empty($this->importedXML)) {
return FALSE;
}
if (empty($this->importedTransUnits)) {
$reader = new \XMLReader();
foreach ($this->importedXML
->xpath('//xliff:trans-unit') as $unit) {
if (!$job
->getSetting('xliff_processing')) {
$this->importedTransUnits[(string) $unit['id']]['#text'] = (string) $unit->target;
continue;
}
$reader
->XML($unit->target
->asXML());
$reader
->read();
$this->importedTransUnits[(string) $unit['id']]['#text'] = $this
->processForImport($reader
->readInnerXML(), $job);
}
}
return $this->importedTransUnits;
}
protected function processForImport($translation, JobInterface $job) {
if (!$job
->getSetting('xliff_processing')) {
return $translation;
}
$reader = new \XMLReader();
$reader
->XML('<translation>' . $translation . '</translation>');
$text = '';
while ($reader
->read()) {
if ($reader->name == '#text' || $reader->name == '#cdata-section') {
$text .= $reader->value;
}
elseif ($reader->name == 'x') {
if ($reader
->getAttribute('ctype') == 'lb') {
$text .= '<br />';
}
}
elseif ($reader->name == 'ph') {
if ($reader
->getAttribute('ctype') == 'image') {
$text .= '<img';
while ($reader
->moveToNextAttribute()) {
if ($reader->name != 'ctype' && $reader->name != 'id') {
$text .= " {$reader->name}=\"{$reader->value}\"";
}
}
$text .= ' />';
}
}
}
return $text;
}
protected function processForExport($source, array $key_array) {
$tjiid = $key_array[0];
$key_string = \Drupal::service('tmgmt.data')
->ensureStringKey($key_array);
$dom = new \DOMDocument();
$dom
->loadHTML("<html><head><meta http-equiv='Content-type' content='text/html; charset=UTF-8' /></head><body>" . $source . '</body></html>');
$iterator = new \RecursiveIteratorIterator(new RecursiveDOMIterator($dom), \RecursiveIteratorIterator::SELF_FIRST);
$writer = new \XMLWriter();
$writer
->openMemory();
$writer
->startDocument('1.0', 'UTF-8');
$writer
->startElement('wrapper');
$tray = array();
$non_pair_tags = array(
'br',
'img',
);
$xliff_validation = $this->job
->getSetting('xliff_validation');
foreach ($iterator as $node) {
if (in_array($node->nodeName, array(
'html',
'body',
'head',
'meta',
))) {
continue;
}
if ($node->nodeType === XML_ELEMENT_NODE) {
if (!isset($xliff_validation[$key_string])) {
$xliff_validation[$key_string] = 0;
}
$xliff_validation[$key_string]++;
$id = 'tjiid' . $tjiid . '-' . $xliff_validation[$key_string];
$is_pair_tag = !in_array($node->nodeName, $non_pair_tags);
if ($is_pair_tag) {
$this
->writeBPT($writer, $node, $id);
}
elseif ($node->nodeName == 'img') {
$this
->writeIMG($writer, $node, $id);
}
elseif ($node->nodeName == 'br') {
$this
->writeBR($writer, $node, $id);
}
$tray[$id] = array(
'name' => $node->nodeName,
'id' => $id,
'value' => $node->nodeValue,
'built_text' => '',
'is_pair_tag' => $is_pair_tag,
);
}
elseif ($node->nodeName == '#text') {
$writer
->writeCdata($this
->toEntities($node->nodeValue));
foreach ($tray as &$info) {
$info['built_text'] .= $node->nodeValue;
}
}
$reversed_tray = array_reverse($tray);
foreach ($reversed_tray as $_info) {
if ($_info['value'] == $_info['built_text'] && $_info['is_pair_tag']) {
$xliff_validation[$key_string]++;
$this
->writeEPT($writer, $_info['name'], $_info['id']);
unset($tray[$_info['id']]);
}
}
}
$this->job->settings->xliff_validation = $xliff_validation;
$this->job
->save();
$writer
->endElement();
$reader = new \XMLReader();
$reader
->XML($writer
->outputMemory());
$reader
->read();
return $reader
->readInnerXML();
}
protected function writeBR(\XMLWriter $writer, \DOMElement $node, $id) {
$writer
->startElement('x');
$writer
->writeAttribute('id', $id);
$writer
->writeAttribute('ctype', 'lb');
$writer
->endElement();
}
protected function writeBPT(\XMLWriter $writer, \DOMElement $node, $id) {
$beginning_tag = '<' . $node->nodeName;
if ($node
->hasAttributes()) {
$attributes = array();
foreach ($node->attributes as $attribute) {
$attributes[] = $attribute->name . '="' . $attribute->value . '"';
}
$beginning_tag .= ' ' . implode(' ', $attributes);
}
$beginning_tag .= '>';
$writer
->startElement('bpt');
$writer
->writeAttribute('id', $id);
$writer
->text($beginning_tag);
$writer
->endElement();
}
protected function writeEPT(\XMLWriter $writer, $name, $id) {
$writer
->startElement('ept');
$writer
->writeAttribute('id', $id);
$writer
->text('</' . $name . '>');
$writer
->endElement();
}
protected function writeIMG(\XMLWriter $writer, \DOMElement $node, $id) {
$writer
->startElement('ph');
$writer
->writeAttribute('id', $id);
$writer
->writeAttribute('ctype', 'image');
foreach ($node->attributes as $attribute) {
$writer
->writeAttribute($attribute->name, $attribute->value);
}
$writer
->endElement();
}
protected function toEntities($string) {
return str_replace(array(
'&',
'>',
'<',
), array(
'&',
'>',
'<',
), $string);
}
}