From 1d84135830e87d79aa16ba70a4eceefa8fe5fbf3 Mon Sep 17 00:00:00 2001 From: Georg Ringer Date: Mon, 15 Jul 2024 12:19:56 +0200 Subject: [PATCH] [FEATURE] Xlf2csv --- ...tCommand.php => ConvertCsv2XlfCommand.php} | 4 +- Classes/Command/ConvertXlf2CsvCommand.php | 52 ++++++++++++++++++ Classes/Service/ConvertService.php | 53 ++++++++++++++++--- .../Service/{CsvReader.php => CsvService.php} | 18 +++++-- Classes/Service/XlfFileService.php | 3 ++ Configuration/Services.yaml | 11 +++- Readme.md | 11 +++- .../Private/Examples/{ => csv2xlf}/de.out.xlf | 0 .../Private/Examples/{ => csv2xlf}/in.csv | 0 .../Private/Examples/{ => csv2xlf}/out.xlf | 0 Resources/Private/Examples/xlf2csv/de.in.xlf | 16 ++++++ Resources/Private/Examples/xlf2csv/fr.in.xlf | 16 ++++++ Resources/Private/Examples/xlf2csv/in.xlf | 14 +++++ Resources/Private/Examples/xlf2csv/out.csv | 3 ++ Tests/Unit/Service/ConvertServiceTest.php | 23 ++++++-- .../{CsvReaderTest.php => CsvServiceTest.php} | 8 +-- Tests/Unit/Service/Fixtures/xlf2csv/de.in.xlf | 16 ++++++ Tests/Unit/Service/Fixtures/xlf2csv/fr.in.xlf | 16 ++++++ Tests/Unit/Service/Fixtures/xlf2csv/in.xlf | 14 +++++ .../Unit/Service/Fixtures/xlf2csv/result.csv | 3 ++ Tests/Unit/Service/Result/result.csv | 3 ++ ext_emconf.php | 2 +- 22 files changed, 259 insertions(+), 27 deletions(-) rename Classes/Command/{ConvertCommand.php => ConvertCsv2XlfCommand.php} (94%) create mode 100644 Classes/Command/ConvertXlf2CsvCommand.php rename Classes/Service/{CsvReader.php => CsvService.php} (69%) rename Resources/Private/Examples/{ => csv2xlf}/de.out.xlf (100%) rename Resources/Private/Examples/{ => csv2xlf}/in.csv (100%) rename Resources/Private/Examples/{ => csv2xlf}/out.xlf (100%) create mode 100644 Resources/Private/Examples/xlf2csv/de.in.xlf create mode 100644 Resources/Private/Examples/xlf2csv/fr.in.xlf create mode 100644 Resources/Private/Examples/xlf2csv/in.xlf create mode 100644 Resources/Private/Examples/xlf2csv/out.csv rename Tests/Unit/Service/{CsvReaderTest.php => CsvServiceTest.php} (85%) create mode 100644 Tests/Unit/Service/Fixtures/xlf2csv/de.in.xlf create mode 100644 Tests/Unit/Service/Fixtures/xlf2csv/fr.in.xlf create mode 100644 Tests/Unit/Service/Fixtures/xlf2csv/in.xlf create mode 100644 Tests/Unit/Service/Fixtures/xlf2csv/result.csv create mode 100644 Tests/Unit/Service/Result/result.csv diff --git a/Classes/Command/ConvertCommand.php b/Classes/Command/ConvertCsv2XlfCommand.php similarity index 94% rename from Classes/Command/ConvertCommand.php rename to Classes/Command/ConvertCsv2XlfCommand.php index c2b49f1..2c7f14a 100644 --- a/Classes/Command/ConvertCommand.php +++ b/Classes/Command/ConvertCsv2XlfCommand.php @@ -13,7 +13,7 @@ use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Utility\GeneralUtility; -class ConvertCommand extends Command +class ConvertCsv2XlfCommand extends Command { protected function configure(): void { @@ -52,7 +52,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $convertService = GeneralUtility::makeInstance(ConvertService::class); - $statistic = $convertService->convert($in, $out, $rebuild); + $statistic = $convertService->convertCsv2Xlf($in, $out, $rebuild); $io->table(['Language', 'Count'], array_map(fn ($k, $v) => [$k, $v], array_keys($statistic), $statistic)); diff --git a/Classes/Command/ConvertXlf2CsvCommand.php b/Classes/Command/ConvertXlf2CsvCommand.php new file mode 100644 index 0000000..eae1c8c --- /dev/null +++ b/Classes/Command/ConvertXlf2CsvCommand.php @@ -0,0 +1,52 @@ +addArgument('in', InputArgument::REQUIRED, 'XLF file to convert'); + $this->addArgument('out', InputArgument::REQUIRED, 'Path to save the CSV file'); + $this->addArgument('languages', InputArgument::REQUIRED, 'Comma separated list of languages'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $io->title($this->getDescription()); + + $in = $input->getArgument('in') ?? ''; + $out = $input->getArgument('out') ?? ''; + $languages = GeneralUtility::trimExplode(',', $input->getArgument('languages') ?? '', true); + + if (!str_starts_with('/', $in)) { + $in = Environment::getProjectPath() . '/' . $in; + } + + if (!is_file($in)) { + $io->error(sprintf('File "%s" could not be found', $in)); + return Command::FAILURE; + } + if (empty($languages)) { + $io->error('No languages provided'); + return Command::FAILURE; + } + + $convertService = GeneralUtility::makeInstance(ConvertService::class); + $statistic = $convertService->convertXlf2Csv($in, $out, $languages); + + return Command::SUCCESS; + } +} diff --git a/Classes/Service/ConvertService.php b/Classes/Service/ConvertService.php index dbf2838..cfc62b2 100644 --- a/Classes/Service/ConvertService.php +++ b/Classes/Service/ConvertService.php @@ -11,19 +11,53 @@ class ConvertService { public function __construct( protected readonly XlfFileService $xlfFileService, - protected readonly CsvReader $csvReader + protected readonly CsvService $csvService ) {} - public function convert(string $csvFilePath, string $out, bool $forceRebuild): array + public function convertXlf2Csv(string $xlfFilePath, string $out, array $languages): array + { + $headers = ['key', 'en']; + $defaultLabels = $this->xlfFileService->getLabels($xlfFilePath, 'en'); + $translations = []; + foreach ($languages as $language) { + if ($language === 'en') { + throw new \UnexpectedValueException('Language "en" is not allowed', 1721035547); + } + $headers[] = $language; + $path = $this->getXlfFileNameForLanguage($xlfFilePath, $language); + if (!is_file($path)) { + continue; + } + $translations[$language] = $this->xlfFileService->getLabels($path, $language); + } + + $flatList = []; + + foreach ($defaultLabels as $label) { + $flatList[$label->key]['key'] = $label->key; + $flatList[$label->key]['en'] = $label->source; + + foreach ($languages as $language) { + if (isset($translations[$language][$label->key])) { + $flatList[$label->key][$language] = $translations[$language][$label->key]->translation; + } else { + $flatList[$label->key][$language] = ''; + } + } + } + + $this->csvService->generateCsv($out, $flatList, $headers); + return $flatList; + } + + public function convertCsv2Xlf(string $csvFilePath, string $out, bool $forceRebuild): array { $stats = []; - $data = $this->csvReader->getFromFile($csvFilePath); + $data = $this->csvService->getFromFile($csvFilePath); foreach ($data as $language => $labels) { if ($language !== 'en') { - $fileInfo = pathinfo($out); - - $targetFilename = sprintf('%s/%s.%s', $fileInfo['dirname'], $language, $fileInfo['basename']); + $targetFilename = $this->getXlfFileNameForLanguage($out, $language); } else { $targetFilename = $out; } @@ -53,4 +87,11 @@ protected function addExistingLabels(array $labels, string $language, string $pa return array_merge($existingLabels, $labels); } + + protected function getXlfFileNameForLanguage(string $out, string $language): string + { + $fileInfo = pathinfo($out); + + return sprintf('%s/%s.%s', $fileInfo['dirname'], $language, $fileInfo['basename']); + } } diff --git a/Classes/Service/CsvReader.php b/Classes/Service/CsvService.php similarity index 69% rename from Classes/Service/CsvReader.php rename to Classes/Service/CsvService.php index 6d57a37..0157ee6 100644 --- a/Classes/Service/CsvReader.php +++ b/Classes/Service/CsvService.php @@ -5,16 +5,17 @@ namespace StudioMitte\Csv2Xlf\Service; use League\Csv\Reader; +use League\Csv\Writer; use StudioMitte\Csv2Xlf\Domain\Model\Dto\Label; -class CsvReader +class CsvService { public function getFromFile(string $filePath): array { - $csvReader = Reader::createFromPath($filePath, 'r'); - $csvReader->setHeaderOffset(0); + $reader = Reader::createFromPath($filePath, 'r'); + $reader->setHeaderOffset(0); - $header = $csvReader->getHeader(); + $header = $reader->getHeader(); if ($header[0] !== 'key') { throw new \RuntimeException('CSV file has no "key" column on 1st position', 1719919250); } @@ -23,7 +24,7 @@ public function getFromFile(string $filePath): array } $labels = []; - foreach ($csvReader->getRecords() as $row) { + foreach ($reader->getRecords() as $row) { $key = $row['key']; $default = $row['en']; unset($row['key'], $row['en']); @@ -40,4 +41,11 @@ public function getFromFile(string $filePath): array return $labels; } + + public function generateCsv(string $path, array $data, array $headers) + { + $writer = Writer::createFromPath($path, 'w'); + $writer->insertOne($headers); + $writer->insertAll($data); + } } diff --git a/Classes/Service/XlfFileService.php b/Classes/Service/XlfFileService.php index 42f883a..10115e6 100644 --- a/Classes/Service/XlfFileService.php +++ b/Classes/Service/XlfFileService.php @@ -9,6 +9,9 @@ class XlfFileService { + /** + * @return Label[] + */ public function getLabels(string $path, string $language): array { $xmlContent = file_get_contents($path); diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 0c84e50..b96c103 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -7,13 +7,20 @@ services: StudioMitte\Csv2Xlf\: resource: '../Classes/*' - StudioMitte\Csv2Xlf\Command\ConvertCommand: + StudioMitte\Csv2Xlf\Command\ConvertCsv2XlfCommand: tags: - name: 'console.command' - command: 'csv2xlf:convert' + command: 'csv2xlf:csv2xlf' description: 'Convert CSV to XLF' schedulable: true + StudioMitte\Csv2Xlf\Command\ConvertXlf2CsvCommand: + tags: + - name: 'console.command' + command: 'csv2xlf:xlf2csv' + description: 'Convert XLF to CSV' + schedulable: true + StudioMitte\Csv2Xlf\Service\ConvertService: public: true diff --git a/Readme.md b/Readme.md index eb4fb82..fb397b5 100644 --- a/Readme.md +++ b/Readme.md @@ -15,6 +15,8 @@ composer require studiomitte/csv2xlf ## Usage +### CSV to XLF + CSV looks like this ```csv "key","en","de" @@ -24,14 +26,19 @@ CSV looks like this With the following requirements: - The first row is the header -- The header starts with `key`, followed by `en` and afterwards the language codes +- The header starts with `key`, followed by `en` and afterward the language codes - Default is always `en` ```bash -./bin/typo3 csv2xlf:convert packages/csv2xlf/Resources/Private/Examples/in.csv packages/csv2xlf/Resources/Private/Examples/out.xlf +./bin/typo3 csv2xlf:csv2xlf packages/csv2xlf/Resources/Private/Examples/csv2xlf/in.csv packages/csv2xlf/Resources/Private/Examples/csv2xlf/out.xlf ``` +### XLF to CSV + +```bash +./bin/typo3 csv2xlf:xlf2csv packages/csv2xlf/Resources/Private/Examples/xlf2csv/in.xlf packages/csv2xlf/Resources/Private/Examples/xlf2csv/out.csv de,fr,es +``` ## Credits diff --git a/Resources/Private/Examples/de.out.xlf b/Resources/Private/Examples/csv2xlf/de.out.xlf similarity index 100% rename from Resources/Private/Examples/de.out.xlf rename to Resources/Private/Examples/csv2xlf/de.out.xlf diff --git a/Resources/Private/Examples/in.csv b/Resources/Private/Examples/csv2xlf/in.csv similarity index 100% rename from Resources/Private/Examples/in.csv rename to Resources/Private/Examples/csv2xlf/in.csv diff --git a/Resources/Private/Examples/out.xlf b/Resources/Private/Examples/csv2xlf/out.xlf similarity index 100% rename from Resources/Private/Examples/out.xlf rename to Resources/Private/Examples/csv2xlf/out.xlf diff --git a/Resources/Private/Examples/xlf2csv/de.in.xlf b/Resources/Private/Examples/xlf2csv/de.in.xlf new file mode 100644 index 0000000..354c1fd --- /dev/null +++ b/Resources/Private/Examples/xlf2csv/de.in.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example NEW + Das ist ein Beispiel + + + <![CDATA[<h3>Privacy</h3> + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + + + + diff --git a/Resources/Private/Examples/xlf2csv/fr.in.xlf b/Resources/Private/Examples/xlf2csv/fr.in.xlf new file mode 100644 index 0000000..a49e852 --- /dev/null +++ b/Resources/Private/Examples/xlf2csv/fr.in.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example + Ceci est un exemple + + + <![CDATA[<h3>Datenschutzhinweis</h3> + <![CDATA[<h3>Protection des données</h3> + + + + diff --git a/Resources/Private/Examples/xlf2csv/in.xlf b/Resources/Private/Examples/xlf2csv/in.xlf new file mode 100644 index 0000000..03ce13b --- /dev/null +++ b/Resources/Private/Examples/xlf2csv/in.xlf @@ -0,0 +1,14 @@ + + + +
+ + + This is an example NEW + + + <![CDATA[<h3>Price</h3> + + + + diff --git a/Resources/Private/Examples/xlf2csv/out.csv b/Resources/Private/Examples/xlf2csv/out.csv new file mode 100644 index 0000000..a3263ca --- /dev/null +++ b/Resources/Private/Examples/xlf2csv/out.csv @@ -0,0 +1,3 @@ +key,en,de,fr +example,"This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +privacy,Price,"Datenschutzhinweis (bs)","Protection des données" diff --git a/Tests/Unit/Service/ConvertServiceTest.php b/Tests/Unit/Service/ConvertServiceTest.php index 265ca42..44aee90 100644 --- a/Tests/Unit/Service/ConvertServiceTest.php +++ b/Tests/Unit/Service/ConvertServiceTest.php @@ -5,24 +5,37 @@ namespace StudioMitte\Csv2Xlf\Tests\Service; use StudioMitte\Csv2Xlf\Service\ConvertService; -use StudioMitte\Csv2Xlf\Service\CsvReader; +use StudioMitte\Csv2Xlf\Service\CsvService; use StudioMitte\Csv2Xlf\Service\XlfFileService; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; class ConvertServiceTest extends UnitTestCase { - public function testConversion(): void + public function testConversionCsv2Xlf(): void { $exampleFile = __DIR__ . '/Fixtures/example.csv'; $out = __DIR__ . '/Result/result.xlf'; $forceRebuild = true; $xlfService = new XlfFileService(); - $csvReaderService = new CsvReader(); - $convertService = new ConvertService($xlfService, $csvReaderService); - $convertService->convert($exampleFile, $out, $forceRebuild); + $csvService = new CsvService(); + $convertService = new ConvertService($xlfService, $csvService); + $convertService->convertCsv2Xlf($exampleFile, $out, $forceRebuild); self::assertFileEquals(__DIR__ . '/Fixtures/result.xlf', $out, 'Conversion failed for default'); self::assertFileEquals(__DIR__ . '/Fixtures/de.result.xlf', __DIR__ . '/Result/de.result.xlf', 'Conversion failed for de'); } + + public function testConversionXlf2Csv(): void + { + $exampleFile = __DIR__ . '/Fixtures/xlf2csv/in.xlf'; + $out = __DIR__ . '/Result/result.csv'; + + $xlfService = new XlfFileService(); + $csvService = new CsvService(); + $convertService = new ConvertService($xlfService, $csvService); + $convertService->convertXlf2Csv($exampleFile, $out, ['de', 'fr']); + + self::assertFileEquals(__DIR__ . '/Fixtures/xlf2csv/result.csv', $out, 'Conversion failed for default'); + } } diff --git a/Tests/Unit/Service/CsvReaderTest.php b/Tests/Unit/Service/CsvServiceTest.php similarity index 85% rename from Tests/Unit/Service/CsvReaderTest.php rename to Tests/Unit/Service/CsvServiceTest.php index 898ec59..d532148 100644 --- a/Tests/Unit/Service/CsvReaderTest.php +++ b/Tests/Unit/Service/CsvServiceTest.php @@ -4,16 +4,16 @@ namespace StudioMitte\Csv2Xlf\Tests\Service; -use StudioMitte\Csv2Xlf\Service\CsvReader; +use StudioMitte\Csv2Xlf\Service\CsvService; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; -class CsvReaderTest extends UnitTestCase +class CsvServiceTest extends UnitTestCase { - protected CsvReader $subject; + protected CsvService $subject; public function setUp(): void { - $this->subject = new CsvReader(); + $this->subject = new CsvService(); parent::setUp(); } diff --git a/Tests/Unit/Service/Fixtures/xlf2csv/de.in.xlf b/Tests/Unit/Service/Fixtures/xlf2csv/de.in.xlf new file mode 100644 index 0000000..354c1fd --- /dev/null +++ b/Tests/Unit/Service/Fixtures/xlf2csv/de.in.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example NEW + Das ist ein Beispiel + + + <![CDATA[<h3>Privacy</h3> + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + + + + diff --git a/Tests/Unit/Service/Fixtures/xlf2csv/fr.in.xlf b/Tests/Unit/Service/Fixtures/xlf2csv/fr.in.xlf new file mode 100644 index 0000000..a49e852 --- /dev/null +++ b/Tests/Unit/Service/Fixtures/xlf2csv/fr.in.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example + Ceci est un exemple + + + <![CDATA[<h3>Datenschutzhinweis</h3> + <![CDATA[<h3>Protection des données</h3> + + + + diff --git a/Tests/Unit/Service/Fixtures/xlf2csv/in.xlf b/Tests/Unit/Service/Fixtures/xlf2csv/in.xlf new file mode 100644 index 0000000..03ce13b --- /dev/null +++ b/Tests/Unit/Service/Fixtures/xlf2csv/in.xlf @@ -0,0 +1,14 @@ + + + +
+ + + This is an example NEW + + + <![CDATA[<h3>Price</h3> + + + + diff --git a/Tests/Unit/Service/Fixtures/xlf2csv/result.csv b/Tests/Unit/Service/Fixtures/xlf2csv/result.csv new file mode 100644 index 0000000..a3263ca --- /dev/null +++ b/Tests/Unit/Service/Fixtures/xlf2csv/result.csv @@ -0,0 +1,3 @@ +key,en,de,fr +example,"This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +privacy,Price,"Datenschutzhinweis (bs)","Protection des données" diff --git a/Tests/Unit/Service/Result/result.csv b/Tests/Unit/Service/Result/result.csv new file mode 100644 index 0000000..a3263ca --- /dev/null +++ b/Tests/Unit/Service/Result/result.csv @@ -0,0 +1,3 @@ +key,en,de,fr +example,"This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +privacy,Price,"Datenschutzhinweis (bs)","Protection des données" diff --git a/ext_emconf.php b/ext_emconf.php index 4184162..2c8a479 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -17,5 +17,5 @@ ], ], 'state' => 'beta', - 'version' => '0.1.0', + 'version' => '0.2.0', ];