792 lines
29 KiB
PHP
792 lines
29 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Kimai time-tracking app.
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace App\Command;
|
|
|
|
use App\Configuration\LocaleService;
|
|
use App\Kernel;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Helper\ProgressBar;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
use Symfony\Component\HttpClient\HttpClient;
|
|
|
|
/**
|
|
* Command used to execute all the basic application bootstrapping AFTER "composer install" was executed.
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
#[AsCommand(name: 'kimai:translations', description: 'Automated translation adjustments')]
|
|
final class TranslationCommand extends Command
|
|
{
|
|
public function __construct(
|
|
private readonly string $projectDirectory,
|
|
private readonly string $kernelEnvironment,
|
|
private readonly LocaleService $localeService
|
|
)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this
|
|
->addOption('resname', null, InputOption::VALUE_NONE, 'Fix the resname vs. id attribute')
|
|
->addOption('duplicates', null, InputOption::VALUE_NONE, 'Find duplicate translation keys')
|
|
->addOption('delete-resname', null, InputOption::VALUE_REQUIRED, 'Deletes the translation by resname')
|
|
->addOption('extension', null, InputOption::VALUE_NONE, 'Find translation files with wrong extensions')
|
|
->addOption('fill-empty', null, InputOption::VALUE_NONE, 'Pre-fills empty translations with the english version')
|
|
->addOption('delete-empty', null, InputOption::VALUE_NONE, 'Delete all empty keys and files which have no translated key at all')
|
|
->addOption('move-resname', null, InputOption::VALUE_REQUIRED, 'Move a resname from one file to another (needs "source" and "target" options)')
|
|
->addOption('copy-resname', null, InputOption::VALUE_REQUIRED, 'Copy a resname within one file (needs "source" option)')
|
|
->addOption('target-resname', null, InputOption::VALUE_REQUIRED, 'Target resname when copying (needs "source" option)')
|
|
->addOption('move-all', null, InputOption::VALUE_NONE, 'Move all keys from one file to another (needs "source" and "target" options)')
|
|
->addOption('source', null, InputOption::VALUE_REQUIRED, 'Single source file to use')
|
|
->addOption('only-core', null, InputOption::VALUE_NONE, 'Do not include plugin and theme directories')
|
|
->addOption('target', null, InputOption::VALUE_REQUIRED, 'Single target file to use')
|
|
// DEEPL TRANSLATION FEATURE - UNTESTED
|
|
->addOption('translate-locale', null, InputOption::VALUE_REQUIRED, 'Translate into the given locale with Deepl')
|
|
// @see https://www.deepl.com/de/pro#developer
|
|
->addOption('translate-deepl', null, InputOption::VALUE_REQUIRED, 'Translate using the "DeepL API Free" auth-key')
|
|
;
|
|
}
|
|
|
|
public function isEnabled(): bool
|
|
{
|
|
return $this->kernelEnvironment !== 'prod';
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
|
|
$bases = [
|
|
'core' => $this->projectDirectory . '/translations/*.xlf',
|
|
];
|
|
|
|
if (!$input->getOption('only-core')) {
|
|
$bases['plugins'] = $this->projectDirectory . Kernel::PLUGIN_DIRECTORY . '/*/Resources/translations/*.xlf';
|
|
$bases['theme'] = $this->projectDirectory . '/vendor/kevinpapst/tabler-bundle/translations/*.xlf';
|
|
}
|
|
|
|
$sources = [];
|
|
if ($input->getOption('source') !== null) {
|
|
/** @var string $tmp */
|
|
$tmp = $input->getOption('source');
|
|
foreach ($bases as $directory) {
|
|
$files = glob($directory);
|
|
foreach ($files as $file) {
|
|
if (explode('.', basename($file))[0] === $tmp) {
|
|
$sources[] = $file;
|
|
}
|
|
}
|
|
}
|
|
if (\count($sources) === 0) {
|
|
$io->error('Could not find translation "source" named: ' . $tmp);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
} else {
|
|
foreach ($bases as $directory) {
|
|
$files = glob($directory);
|
|
foreach ($files as $file) {
|
|
$sources[] = $file;
|
|
}
|
|
}
|
|
}
|
|
|
|
$targets = [];
|
|
if ($input->getOption('target') !== null) {
|
|
/** @var string $tmp */
|
|
$tmp = $input->getOption('target');
|
|
foreach ($bases as $directory) {
|
|
$files = glob($directory);
|
|
foreach ($files as $file) {
|
|
if (explode('.', basename($file))[0] === $tmp) {
|
|
$targets[] = $file;
|
|
}
|
|
}
|
|
}
|
|
if (\count($targets) === 0) {
|
|
$io->error('Could not find translation "target" named: ' . $tmp);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
}
|
|
|
|
if ($input->getOption('delete-resname')) {
|
|
foreach ($sources as $file) {
|
|
$this->removeKey($file, $input->getOption('delete-resname'));
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Fix resname vs. id
|
|
// ==========================================================================
|
|
if ($input->getOption('resname')) {
|
|
foreach ($sources as $file) {
|
|
$this->fixXlfFile($file);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Move resname from source to target
|
|
// ==========================================================================
|
|
$moveResname = $input->getOption('move-resname');
|
|
if (\is_string($moveResname)) {
|
|
if (\count($targets) === 0) {
|
|
$io->error('Moving a resname needs a target file');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
return $this->moveResname($io, $moveResname, $sources, $targets);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Move resname from source to target
|
|
// ==========================================================================
|
|
$copyResname = $input->getOption('copy-resname');
|
|
$targetResname = $input->getOption('target-resname');
|
|
if (\is_string($copyResname)) {
|
|
if (!\is_string($targetResname)) {
|
|
$io->error('To copy a resname, we need a target-resname');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
return $this->copyResname($io, $copyResname, $targetResname, $sources);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Move all keys from source to target
|
|
// ==========================================================================
|
|
if ($input->getOption('move-all')) {
|
|
if (\count($sources) === 0 || \count($targets) === 0) {
|
|
$io->error('Moving all keys only works with one source and one target file');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
return $this->moveAll($io, $sources, $targets);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Fill empty translations with english version
|
|
// ==========================================================================
|
|
if ($input->getOption('fill-empty')) {
|
|
$translateFrom = ['de-CH' => 'de', 'de_CH' => 'de', 'pt_BR' => 'pt', 'pt-BR' => 'pt', 'pt' => 'pt_BR'];
|
|
$translations = [];
|
|
foreach ($sources as $file) {
|
|
$base = basename($file);
|
|
$parts = explode('.', $base);
|
|
$name = $parts[0];
|
|
$fileLocale = $parts[1];
|
|
$fromLocale = 'en';
|
|
if (\array_key_exists($fileLocale, $translateFrom)) {
|
|
$fromLocale = $translateFrom[$fileLocale];
|
|
}
|
|
|
|
if (!\array_key_exists($fromLocale, $translations)) {
|
|
$translations[$fromLocale] = [];
|
|
}
|
|
|
|
if (!\array_key_exists($name, $translations[$fromLocale])) {
|
|
$fromLocaleName = str_replace('.' . $fileLocale . '.', '.' . $fromLocale . '.', $file);
|
|
if (!file_exists($fromLocaleName)) {
|
|
$io->error('Could not find translation file: ' . $fromLocaleName);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
$translations[$fromLocale][$name] = $this->getTranslations($fromLocaleName);
|
|
}
|
|
|
|
if (stripos($base, '.' . $fromLocale . '.xlf') !== false || stripos($base, '.' . $fromLocale . '.xliff') !== false) {
|
|
continue;
|
|
}
|
|
|
|
$this->fillEmptyTranslations($file, $translations[$fromLocale][$name]);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Find wrong file extensions
|
|
// ==========================================================================
|
|
if ($input->getOption('extension')) {
|
|
foreach ($sources as $file) {
|
|
$file = str_replace($this->projectDirectory, '', $file);
|
|
$io->warning($file);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Find duplicate translation keys
|
|
// ==========================================================================
|
|
if ($input->getOption('duplicates')) {
|
|
$duplicates = [];
|
|
|
|
foreach ($sources as $file) {
|
|
$xml = simplexml_load_file($file);
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
$n = (string) $unit['resname'];
|
|
if (!\array_key_exists($n, $duplicates)) {
|
|
$duplicates[$n] = [];
|
|
}
|
|
$b = explode('.', basename($file))[0];
|
|
if (!\in_array($b, $duplicates[$n])) {
|
|
$duplicates[$n][] = $b;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($duplicates as $id => $duplicateFiles) {
|
|
if (\count($duplicateFiles) > 1) {
|
|
$io->text($id . ' => ' . implode(', ', $duplicateFiles));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Delete empty translation keys and files without any translation
|
|
// ==========================================================================
|
|
if ($input->getOption('delete-empty')) {
|
|
foreach ($sources as $file) {
|
|
$this->removeEmptyTranslations($io, $file);
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// DEEPL
|
|
// ==========================================================================
|
|
$locale = $input->getOption('translate-locale');
|
|
$deepl = $input->getOption('translate-deepl');
|
|
|
|
if ($locale !== null && $deepl === null) {
|
|
$io->error('Missing "DeepL API Free" auth-key');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if ($locale === null && $deepl !== null) {
|
|
$io->error('Missing translation locale');
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if ($locale !== null && $deepl !== null) {
|
|
// see https://github.com/octfx/DeepLy/blob/master/src/DeepLy.php
|
|
$deeplySupportedLanguages = [
|
|
'de' => 'DE',
|
|
'en' => 'EN-US',
|
|
'fr' => 'FR',
|
|
'it' => 'IT',
|
|
'ja' => 'JA',
|
|
'es' => 'ES',
|
|
'nl' => 'NL',
|
|
'pl' => 'PL',
|
|
'pt' => 'PT-PT', // ???
|
|
'pt_BR' => 'PT-BR', // ???
|
|
'ru' => 'RU',
|
|
'zh_CN' => 'ZH',
|
|
];
|
|
|
|
$locale = strtolower($locale);
|
|
if (!$this->localeService->isKnownLocale($locale)) {
|
|
$io->error('Unknown locale given: ' . $locale);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
if (!\array_key_exists($locale, $deeplySupportedLanguages)) {
|
|
$io->error('Locale not supported by Deeply: ' . $locale);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$allKeys = 0;
|
|
$enFiles = glob($bases['core'] . '/*.en.xlf');
|
|
|
|
$baseUrl = 'https://api-free.deepl.com/v2/translate';
|
|
$client = HttpClient::create([]);
|
|
|
|
foreach ($enFiles as $file) {
|
|
$enTrans = [];
|
|
$domain = explode('.', basename($file))[0];
|
|
|
|
$xml = simplexml_load_file($file);
|
|
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
$id = (string) $unit['id'];
|
|
$enTrans[$id] = [
|
|
'resname' => (string) $unit['resname'],
|
|
'source' => (string) $unit->source,
|
|
'target' => (string) $unit->target
|
|
];
|
|
$allKeys++;
|
|
}
|
|
|
|
$localeFile = $bases['core'] . '/' . $domain . '.' . $locale . '.xlf';
|
|
|
|
$translated = [];
|
|
|
|
if (file_exists($localeFile)) {
|
|
$xml2 = simplexml_load_file($localeFile);
|
|
foreach ($xml2->file->body->{'trans-unit'} as $unit) {
|
|
$id = (string) $unit['id'];
|
|
$translated[$id] = [
|
|
'resname' => (string) $unit['resname'],
|
|
'source' => (string) $unit->source,
|
|
'target' => (string) $unit->target
|
|
];
|
|
}
|
|
}
|
|
|
|
$missingIds = array_diff(array_keys($enTrans), array_keys($translated));
|
|
if (\count($missingIds) === 0) {
|
|
continue;
|
|
}
|
|
|
|
$io->title('Translating ' . $domain);
|
|
$progress = new ProgressBar($output, \count($missingIds));
|
|
|
|
foreach ($missingIds as $id) {
|
|
$progress->advance();
|
|
|
|
$values = $enTrans[$id];
|
|
$translated[$id] = $values;
|
|
|
|
$params = [
|
|
'auth_key' => $deepl,
|
|
//'split_sentences' => '1',
|
|
//'preserve_formatting' => '0',
|
|
'formality' => 'default',
|
|
'text' => $values['target'],
|
|
'source_lang' => 'en',
|
|
'target_lang' => $deeplySupportedLanguages[$locale],
|
|
];
|
|
|
|
$rawResponseData = null;
|
|
try {
|
|
$rawResponseData = $client->request('POST', $baseUrl, ['body' => $params]);
|
|
} catch (\Exception $exception) {
|
|
$io->error($exception->getMessage());
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$json = json_decode($rawResponseData->getContent(), true);
|
|
$translation = $json['translations'][0]['text'];
|
|
|
|
$translated[$id]['target'] = $translation;
|
|
}
|
|
|
|
$progress->finish();
|
|
$io->writeln(PHP_EOL);
|
|
|
|
$this->writeXliffFile($bases['core'], $domain, $locale, $translated);
|
|
}
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
private function getTranslations(string $file): array
|
|
{
|
|
$translations = [];
|
|
|
|
$xml = simplexml_load_file($file);
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
if (!isset($unit['resname'])) {
|
|
throw new \Exception('Missing "resname" attribute in file: ' . $file);
|
|
}
|
|
|
|
$source = (string) $unit['resname'];
|
|
$translations[$source] = (string) $unit->target;
|
|
}
|
|
|
|
return $translations;
|
|
}
|
|
|
|
private function writeXliffFile(string $base, string $domain, string $locale, array $translations = []): void
|
|
{
|
|
$from = $base . '/' . $domain . '.en.xlf';
|
|
$to = $base . '/' . $domain . '.' . $locale . '.xlf';
|
|
|
|
copy($from, $to);
|
|
|
|
$xml = simplexml_load_file($to);
|
|
|
|
/** @var \SimpleXMLElement $fileNode */
|
|
$fileNode = $xml->file;
|
|
$fileNode->attributes()->{'target-language'} = $locale;
|
|
$fileNode->attributes()->{'original'} = $domain . '.en.xlf';
|
|
|
|
unset($xml->file->body);
|
|
|
|
$xmlDocument = new \DOMDocument('1.0', 'UTF-8');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($xml->asXML());
|
|
|
|
$xpath = new \DOMXPath($xmlDocument);
|
|
$xpath->registerNamespace('ns', $xmlDocument->documentElement->namespaceURI);
|
|
|
|
$xmlContent = '';
|
|
foreach ($translations as $id => $values) {
|
|
$xmlContent .= \sprintf(
|
|
'<trans-unit id="%s" resname="%s"><source>%s</source><target>%s</target></trans-unit>',
|
|
$id,
|
|
$values['resname'],
|
|
$values['source'],
|
|
$values['target']
|
|
);
|
|
}
|
|
|
|
$fragment = $xmlDocument->createDocumentFragment();
|
|
$fragment->appendXML('<body>' . $xmlContent . '</body>');
|
|
|
|
/** @var \DOMElement $element */
|
|
$element = $xpath->evaluate('/ns:xliff/ns:file')->item(0);
|
|
$element->appendChild($fragment);
|
|
|
|
file_put_contents($to, $xmlDocument->saveXML());
|
|
}
|
|
|
|
private function fixXlfFile(string $file): void
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
if (isset($xml->file->header)) {
|
|
unset($xml->file->header);
|
|
}
|
|
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
$source = $unit->source;
|
|
if (!isset($unit['resname']) && $source !== null) {
|
|
$unit['resname'] = $source;
|
|
}
|
|
if ($unit['resname'] !== null) {
|
|
$unit['id'] = $this->generateId($unit['resname']);
|
|
}
|
|
}
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($xml->asXML());
|
|
|
|
file_put_contents($file, $xmlDocument->saveXML());
|
|
}
|
|
|
|
private function fillEmptyTranslations(string $file, array $translations): void
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
$foundEmpty = false;
|
|
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
if (!isset($unit['resname'])) {
|
|
continue;
|
|
}
|
|
|
|
$key = (string) $unit['resname'];
|
|
|
|
$translation = (string) $unit->target;
|
|
if (\strlen($translation) > 0) {
|
|
continue;
|
|
}
|
|
|
|
if (!\array_key_exists($key, $translations)) {
|
|
throw new \Exception(
|
|
\sprintf('Missing english translation for key: %s in file %s', $key, $file)
|
|
);
|
|
}
|
|
$unit->target[0] = $translations[$key];
|
|
$unit->target['state'] = 'needs-translation';
|
|
$foundEmpty = true;
|
|
}
|
|
|
|
if (!$foundEmpty) {
|
|
return;
|
|
}
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($xml->asXML());
|
|
|
|
file_put_contents($file, $xmlDocument->saveXML());
|
|
}
|
|
|
|
private function removeKey(string $file, string $key): void
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
|
|
/** @var \SimpleXMLElement $unit */
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
if (!isset($unit['resname'])) {
|
|
continue;
|
|
}
|
|
|
|
if ((string) $unit['resname'] === $key) {
|
|
$dom = dom_import_simplexml($unit);
|
|
$dom->parentNode->removeChild($dom);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($xml->asXML());
|
|
|
|
file_put_contents($file, $xmlDocument->saveXML());
|
|
}
|
|
|
|
private function removeEmptyTranslations(SymfonyStyle $io, string $file): void
|
|
{
|
|
$xml = simplexml_load_file($file);
|
|
$hasTranslation = false;
|
|
|
|
/** @var \SimpleXMLElement $unit */
|
|
foreach ($xml->file->body->{'trans-unit'} as $unit) {
|
|
$translation = (string) $unit->target;
|
|
if (\strlen($translation) > 0) {
|
|
$hasTranslation = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasTranslation) {
|
|
unlink($file);
|
|
$io->warning('Removed empty translation file: ' . basename($file));
|
|
|
|
return;
|
|
}
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->loadXML($xml->asXML());
|
|
|
|
$removedTranslation = false;
|
|
$elements = $xmlDocument->getElementsByTagName('target');
|
|
foreach ($elements as $element) {
|
|
if ($element->nodeValue === '' || $element->nodeValue === null) {
|
|
/** @var \DOMElement $parent */
|
|
$parent = $element->parentNode;
|
|
$io->text('Remove empty translation: ' . $parent->getAttribute('resname'));
|
|
$parent->parentNode->removeChild($parent);
|
|
$removedTranslation = true;
|
|
}
|
|
}
|
|
|
|
if ($removedTranslation) {
|
|
$xmlDocument->formatOutput = true;
|
|
|
|
file_put_contents($file, $xmlDocument->saveXML());
|
|
$io->warning('Removed empty translations from file: ' . basename($file));
|
|
}
|
|
}
|
|
|
|
private function generateId(string $source): string
|
|
{
|
|
return strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._');
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $sources
|
|
* @param array<string> $targets
|
|
*/
|
|
private function moveResname(SymfonyStyle $io, string $resname, array $sources, array $targets): int
|
|
{
|
|
foreach ($sources as $source) {
|
|
$tmp = basename($source);
|
|
$pos = strpos($tmp, '.');
|
|
if ($pos === false) {
|
|
$io->error('Unexpected filename: ' . $source);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
$suffix = substr($tmp, $pos);
|
|
$target = null;
|
|
foreach ($targets as $t) {
|
|
if (str_ends_with($t, $suffix)) {
|
|
$target = $t;
|
|
}
|
|
}
|
|
|
|
if ($target === null) {
|
|
$io->error('Cannot find translation target file for source: ' . $source);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$sourceDocument = new \DOMDocument('1.0');
|
|
$sourceDocument->load($source);
|
|
|
|
$targetDocument = new \DOMDocument('1.0');
|
|
$targetDocument->load($target);
|
|
|
|
$movedNode = false;
|
|
|
|
/** @var \DOMElement $element */
|
|
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
|
|
if (!$element->hasAttribute('resname')) {
|
|
continue;
|
|
}
|
|
|
|
$key = $element->getAttribute('resname');
|
|
|
|
if ($key === $resname) {
|
|
$newNode = $targetDocument->importNode($element, true);
|
|
$targetDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
|
|
$element->parentNode->removeChild($element); // @phpstan-ignore-line
|
|
$movedNode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($movedNode) {
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
|
|
file_put_contents($source, $xmlDocument->saveXML());
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($targetDocument->saveXML()); // @phpstan-ignore-line
|
|
file_put_contents($target, $xmlDocument->saveXML());
|
|
}
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $sources
|
|
*/
|
|
private function copyResname(SymfonyStyle $io, string $resname, string $target, array $sources): int
|
|
{
|
|
foreach ($sources as $source) {
|
|
$tmp = basename($source);
|
|
$pos = strpos($tmp, '.');
|
|
if ($pos === false) {
|
|
$io->error('Unexpected filename: ' . $source);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$sourceDocument = new \DOMDocument('1.0');
|
|
$sourceDocument->load($source);
|
|
|
|
$copiedNode = false;
|
|
|
|
/** @var \DOMElement $element */
|
|
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
|
|
if (!$element->hasAttribute('resname')) {
|
|
continue;
|
|
}
|
|
|
|
$key = $element->getAttribute('resname');
|
|
|
|
if ($key === $resname) {
|
|
$newElement = clone $element;
|
|
$newElement->setAttribute('resname', $target);
|
|
foreach ($newElement->childNodes->getIterator() as $child) {
|
|
if ($child->nodeName === 'source') {
|
|
$child->textContent = $target;
|
|
}
|
|
}
|
|
$newElement->setAttribute('id', $this->generateId($target));
|
|
|
|
$newNode = $sourceDocument->importNode($newElement, true);
|
|
$sourceDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
|
|
$copiedNode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($copiedNode) {
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
|
|
file_put_contents($source, $xmlDocument->saveXML());
|
|
}
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $sources
|
|
* @param array<string> $targets
|
|
*/
|
|
private function moveAll(SymfonyStyle $io, array $sources, array $targets): int
|
|
{
|
|
foreach ($sources as $source) {
|
|
$tmp = basename($source);
|
|
$pos = strpos($tmp, '.');
|
|
if ($pos === false) {
|
|
$io->error('Unexpected filename: ' . $source);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
$suffix = substr($tmp, $pos);
|
|
$target = null;
|
|
foreach ($targets as $t) {
|
|
if (str_ends_with($t, $suffix)) {
|
|
$target = $t;
|
|
}
|
|
}
|
|
|
|
if ($target === null) {
|
|
$io->error('Cannot find translation target file for source: ' . $source);
|
|
|
|
return Command::FAILURE;
|
|
}
|
|
|
|
$sourceDocument = new \DOMDocument('1.0');
|
|
$sourceDocument->load($source);
|
|
|
|
$targetDocument = new \DOMDocument('1.0');
|
|
$targetDocument->load($target);
|
|
$removeNodes = [];
|
|
|
|
/** @var \DOMElement $element */
|
|
foreach ($sourceDocument->getElementsByTagName('trans-unit') as $element) {
|
|
$newNode = $targetDocument->importNode($element, true);
|
|
$targetDocument->documentElement->firstElementChild->firstElementChild->appendChild($newNode); // @phpstan-ignore-line
|
|
$removeNodes[] = $element;
|
|
}
|
|
|
|
foreach ($removeNodes as $node) {
|
|
$sourceDocument->documentElement->firstElementChild->firstElementChild->removeChild($node); // @phpstan-ignore-line
|
|
}
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($sourceDocument->saveXML()); // @phpstan-ignore-line
|
|
file_put_contents($source, $xmlDocument->saveXML());
|
|
|
|
$xmlDocument = new \DOMDocument('1.0');
|
|
$xmlDocument->preserveWhiteSpace = false;
|
|
$xmlDocument->formatOutput = true;
|
|
$xmlDocument->loadXML($targetDocument->saveXML()); // @phpstan-ignore-line
|
|
file_put_contents($target, $xmlDocument->saveXML());
|
|
}
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|