Files
oai-symfony/typo3conf/ext/lfeditor/Classes/Service/FileBaseXLFService.php
2026-07-03 17:53:31 +02:00

430 lines
13 KiB
PHP

<?php
namespace SGalinski\Lfeditor\Service;
/***************************************************************
* Copyright notice
*
* (c) sgalinski Internet Services (https://www.sgalinski.de)
*
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use SGalinski\Lfeditor\Exceptions\LFException;
use SGalinski\Lfeditor\Utility\ExtensionUtility;
use SGalinski\Lfeditor\Utility\SgLib;
use SGalinski\Lfeditor\Utility\Typo3Lib;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* base workspace class (xlf)
*/
class FileBaseXLFService extends FileBaseService {
/**
* extended init
*
* @param string $file name of the file (can be a path, if you need this (no check))
* @param string $path path to the file
* @param string $metaFile
* @return void
* @throws LFException
*/
public function init($file, $path, $metaFile) {
$this->setFileType('xlf');
parent::init($file, $path, $metaFile);
}
/**
* calls the parent function and convert all values from utf-8 to the original charset
*
* @return void
* @throws LFException raised if the parent read file method fails
*/
public function readFile() {
$this->readXlfFile();
foreach ($this->localLang['default'] as $constant => $value) {
if (empty($value)) {
$this->localLang['default'][$constant] = '';
}
}
}
/**
* reads a language file
*
* @param string $file language file
* @param string $langKey language shortcut
* @return array language content
* @throws LFException thrown if no data can be found
*/
protected function readLLFile($file, $langKey) {
if (!\is_file($file)) {
throw new LFException('failure.select.noLangfile');
}
// LIBXML_NOCDATA treat CDATA node as text node.
// htmlentities in CDATA are not modified,
// and those - normaly encoded if XML is valid - outside CDATA will be decoded.
$xmlContent = \simplexml_load_string(\file_get_contents($file), \SimpleXMLElement::class, LIBXML_NOCDATA);
// Cast XML to associative array with json_decode/encode.
// This way, value is XML are casted to string.
$xmlContent = \json_decode(\json_encode($xmlContent), TRUE);
// check data
if (!\is_array($xmlContent['file']['body'])) {
throw new LFException('failure.search.noFileContent', 0, '(' . $file . ')');
}
if ($langKey === 'default') {
$this->meta = $xmlContent['file']['header'] ?? [];
}
$this->meta['@attributes'] = $xmlContent['file']['@attributes'];
return $xmlContent['file']['body'];
}
/**
* Resolves the trans-unit parts of the xlf language files into flat
* source arrays with the target language or the source if the target is en
*
* @param array $sourceData
* @return array
*/
public function resolveTranslationUnitsArrayIntoFlatArray(array $sourceData) {
$flatData = [];
if (isset($sourceData['trans-unit']['@attributes'])) {
$sourceData['trans-unit'] = [$sourceData['trans-unit']];
}
if (!array_key_exists('trans-unit', $sourceData)) {
return $flatData;
}
foreach ((array) $sourceData['trans-unit'] as $data) {
if (isset($data['@attributes']['id'])) {
$constant = $data['@attributes']['id'];
if (array_key_exists('target', $data)) {
$flatData[$constant] = $data['target'];
} else {
$flatData[$constant] = $data['source'];
}
if (empty($flatData[$constant])) {
$flatData[$constant] = '';
}
}
}
return $flatData;
}
/**
* Reads the absolute language file with all localized sub files
*
* @return void
* @throws LFException
*/
public function readXlfFile() {
$localLang = [];
// read absolute file
$localLang['default'] = $this->readLLFile($this->absFile, 'default');
$localLang['default'] = $this->resolveTranslationUnitsArrayIntoFlatArray($localLang['default']);
// loop all languages
$originLang = [];
$languages = SgLib::getSystemLanguages();
foreach ($languages as $lang) {
$originLang[$lang] = $this->absFile;
if ($lang === 'default') {
continue;
}
// get localized file
$content = array_key_exists($lang, $localLang) ? $localLang[$lang] : '';
$lFile = $this->getLocalizedFile($content, $lang);
if ($this->checkLocalizedFile(\basename($lFile), $lang)) {
$originLang[$lang] = $lFile;
$localLang[$lang] = [];
if (!\is_file($lFile)) {
continue;
}
// read the content
$localSubLanguage = $this->readLLFile($lFile, $lang);
$localLang[$lang] = $this->resolveTranslationUnitsArrayIntoFlatArray($localSubLanguage);
// merge arrays and save origin of current language
ArrayUtility::mergeRecursiveWithOverrule($localLang, $localSubLanguage);
}
}
// copy all to object variables, if everything was ok
$this->localLang = $localLang;
$this->originLang = $originLang;
}
/**
* checks the localLang array to find localized version of the language
* (checks l10n directory too)
*
* @param string $content language content (only one language)
* @param string $langKey language shortcut
* @return string localized file (absolute)
*/
protected function getLocalizedFile($content, $langKey) {
$file = '';
if ($this->session->getDataByKey('editingMode') !== 'extension') {
$file = $this->getLocalizedFileName($this->absFile, $langKey);
}
if (!\is_file($file)) {
$file = \dirname($this->absFile) . '/' . $this->nameLocalizedFile($langKey);
}
return Typo3Lib::fixFilePath($file);
}
/**
* checks a filename if its a localized file reference
*
* @param string $filename
* @param string $langKey language shortcut
* @return boolean true(localized) or false
*/
public function checkLocalizedFile($filename, $langKey) {
if (!\preg_match('/^(' . $langKey . ')\..*\.xlf$/', $filename)) {
return FALSE;
}
return TRUE;
}
/**
* get the name of a localized file
*
* @param string $langKey language shortcut
* @return string localized file (only filename)
*/
public function nameLocalizedFile($langKey) {
return $langKey . '.' . \basename($this->relFile);
}
/**
* generates the xml header
*
* @return string xml header
*/
private function getXMLHeader() {
return '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . "\n";
}
/**
* converts the array to a xml string
*
* @param array $phpArray
* @param string $targetLanguage
* @param array $enLanguage
* @return string xml content
*/
private function array2xml($phpArray, $targetLanguage, $enLanguage) {
$targetLanguage = \htmlspecialchars($targetLanguage);
$targetLanguageAttribute = ($targetLanguage !== 'default' ? ' target-language="' . $targetLanguage . '"' : '');
$lfeditorExtConf = ExtensionUtility::getExtensionConfiguration();
$date = ($lfeditorExtConf['changeXlfDate'] ? \gmdate('Y-m-d\TH:i:s\Z') : $this->meta['@attributes']['date']);
$xmlString = $this->getXMLHeader() . '<xliff version="1.0">
<file source-language="en"' . $targetLanguageAttribute . ' datatype="plaintext" original="messages" date="' . $date . '">
###HEADER###
###BODY###
</file>
</xliff>';
$header = '<header/>';
if (\is_array($phpArray['header']) && \count($phpArray['header'])) {
$header = '<header>' . "\n";
foreach ($phpArray['header'] as $tagName => $tagValue) {
$tagName = \htmlspecialchars($tagName);
$header .= "\t\t\t" . '<' . $tagName . '>' . \htmlspecialchars(implode(' ', $tagValue)) .
'</' . $tagName . '>' . "\n";
}
$header .= "\t\t" . '</header>';
}
$xmlString = \str_replace('###HEADER###', $header, $xmlString);
$body = '<body/>';
if (\is_array($phpArray['data']) && \count($phpArray['data'])) {
$body = '<body>' . "\n";
$startCDATA = "<![CDATA[";
$endCDATA = "]]>";
$approved = ($targetLanguage !== 'default' ? ' approved="yes"' : '');
foreach ($phpArray['data'] as $constant => $value) {
if (!array_key_exists($constant, $enLanguage)) {
continue;
}
$enValue = $enLanguage[$constant];
// CDATA markers are stripped when reading the XML file from the disk,
// and html entities are not decoded.
// So, for simplicity and safety, when writing back the data to the file,
// we will always enclose value in CDATA, so html entities are preserved
// and XML remains valid.
$value = $startCDATA . $value . $endCDATA;
$enValue = $startCDATA . $enValue . $endCDATA;
$body .= "\t\t\t" . '<trans-unit id="' . \htmlspecialchars(
$constant
) . '"' . $approved . $this->addPreserveSpaceAttribute(
$value,
$enValue,
$targetLanguage
) . '>' . "\n";
if ($targetLanguage !== 'default') {
$body .= "\t\t\t\t" . '<source>' . $enValue . '</source>' . "\n";
$body .= "\t\t\t\t" . '<target>' . $value . '</target>' . "\n";
} else {
$body .= "\t\t\t\t" . '<source>' . $value . '</source>' . "\n";
}
$body .= "\t\t\t" . '</trans-unit>' . "\n";
}
$body .= "\t\t" . '</body>';
}
$xmlString = \str_replace('###BODY###', $body, $xmlString);
return $xmlString;
}
/**
* prepares the content of a language file
*
* @param array $localLang
* @return array new xml array
*/
private function getLangContent($localLang) {
$content = [];
if (!\is_array($localLang) || !\count($localLang)) {
return $content;
}
\ksort($localLang);
foreach ($localLang as $const => $value) {
$content[$const] = \str_replace("\r", '', $value);
}
return $content;
}
/**
* prepares the meta array for nicer saving
*
* @return array meta content
*/
private function prepareMeta() {
if (\is_array($this->meta) && \count($this->meta)) {
foreach ($this->meta as $label => $value) {
$this->meta[$label] = \str_replace("\r", '', $value);
}
}
$this->addGeneratorString();
$metadata = $this->meta;
unset($metadata['@attributes']);
return $metadata;
}
/**
* prepares the final content
*
* @param array | NULL $editedLanguages
* @return array language files as key and content as value
*/
protected function prepareFileContents($editedLanguages = NULL) {
// prepare Content
$metaData = $this->prepareMeta();
$languages = SgLib::getSystemLanguages();
$languageFiles = [];
$enLanguage = $this->getLangContent($this->localLang['default']);
foreach ($languages as $lang) {
if ($lang === 'default') {
continue;
}
// If default language content and $lang language content are not edited, skip this file.
if ($editedLanguages !== NULL &&
!\in_array('default', $editedLanguages, TRUE) && !\in_array($lang, $editedLanguages, TRUE)
) {
continue;
}
if (\array_key_exists($lang, $this->localLang) && \is_array($this->localLang[$lang]) && \count(
$this->localLang[$lang]
)) {
$file = $this->originLang[$lang];
$data = [
'header' => $metaData,
'data' => $this->getLangContent($this->localLang[$lang]),
];
if (array_key_exists($file, $languageFiles)) {
$languageFiles[$file] .= $this->array2xml($data, $lang, $enLanguage);
} else {
$languageFiles[$file] = $this->array2xml($data, $lang, $enLanguage);
}
}
}
// only a localized file?
if ($this->checkLocalizedFile(\basename($this->absFile), \implode('|', SgLib::getSystemLanguages()))) {
return $languageFiles;
}
if ($editedLanguages !== NULL && !\in_array('default', $editedLanguages, TRUE)) {
return $languageFiles;
}
// prepare content for the main file
$data = [
'header' => $metaData,
'data' => $enLanguage,
];
$languageFiles[$this->absFile] = $this->array2xml($data, 'default', $enLanguage);
return $languageFiles;
}
/**
* If constant's source (en) or target value contains multiple spaces or LF character,
* returns string ' xml:space="preserve"'.
*
* @param string $value
* @param string $enValue
* @param string $targetLanguage
* @return string
*/
protected function addPreserveSpaceAttribute($value, $enValue, $targetLanguage) {
$valueContainsSpacesOrLF = \strpos($value, ' ') !== FALSE || \strpos($value, "\n") !== FALSE;
$enValueContainsSpacesOrLF = FALSE;
if ($targetLanguage !== 'default') {
$enValueContainsSpacesOrLF = \strpos($enValue, ' ') !== FALSE || \strpos($enValue, "\n") !== FALSE;
}
return ($valueContainsSpacesOrLF || $enValueContainsSpacesOrLF ? ' xml:space="preserve"' : '');
}
}