Skip to content

Instantly share code, notes, and snippets.

@Kocal
Last active June 14, 2024 20:21
Show Gist options
  • Save Kocal/dd3f99475521dd6525f08b2050c660fe to your computer and use it in GitHub Desktop.
Save Kocal/dd3f99475521dd6525f08b2050c660fe to your computer and use it in GitHub Desktop.
One-shot command that helped me to migrate non-ICU translations to ICU translations, and merge them with original ICU translations, inside a Symfony project. You must copy your `translations` folder to `translations.original` before running the command.
<?php
namespace App\Command\Tools;
use App\Core\Entity\Website\Locale;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
#[AsCommand(
name: 'app:tools:merge-non-icu-and-icu-translations',
description: 'Merge non-ICU and ICU translations',
)]
class MergeNonICUAndICUTranslations extends Command
{
private const ICU_COMPATIBILITY_YES = 'yes';
private const ICU_COMPATIBILITY_NO = 'no';
private const ICU_COMPATIBILITY_TO_MIGRATE = 'to_migrate';
/**
* @var list<Locale::*>
*/
private array $locales = [];
public function __construct(
private TranslationReaderInterface $translationReader,
private TranslationWriterInterface $translationWriter,
ParameterBagInterface $parameterBag,
private LoggerInterface $logger
) {
$this->locales = $parameterBag->get('locales');
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
foreach ($this->locales as $locale) {
$catalogue = new MessageCatalogue($locale);
$this->translationReader->read(__DIR__.'/../../../translations.original', $catalogue);
$catalogue = $this->reorganizeMessages($catalogue, $locale);
$this->translationWriter->write($catalogue, 'xlf', [
'path' => __DIR__.'/../../../translations',
]);
}
return self::SUCCESS;
}
private function reorganizeMessages(MessageCatalogue $oldCatalogue, string $locale): MessageCatalogue
{
$catalogue = new MessageCatalogue($locale);
foreach ($oldCatalogue->getDomains() as $domain) {
$messagesIcu = $oldCatalogue->all($domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX);
$messages = [];
foreach ($oldCatalogue->all($domain) as $id => $message) {
if (isset($messagesIcu[$id])) {
continue;
}
$messages[$id] = $message;
}
foreach ($messages as $key => $message) {
$icuCompatibility = $this->getIcuCompatibility($message);
if (self::ICU_COMPATIBILITY_NO === $icuCompatibility) {
$this->logger->info(sprintf('Message "%s" in domain "%s" is not compatible with ICU.', $key, $domain));
continue;
} elseif (self::ICU_COMPATIBILITY_TO_MIGRATE === $icuCompatibility) {
$message = $this->migrateToICU($message);
$icuCompatibility = $this->getICUCompatibility($message);
if (self::ICU_COMPATIBILITY_YES !== $icuCompatibility) {
$this->logger->error(sprintf('Unable to migrate message "%s" in domain "%s" to ICU format.', $key, $domain));
}
}
$messagesIcu[$key] = $message;
unset($messages[$key]);
}
$catalogue->add($messages, $domain);
$catalogue->add($messagesIcu, $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX);
}
return $catalogue;
}
/**
* @return self::ICU_COMPATIBILITY_*
*/
private function getICUCompatibility(string $message): string
{
if (false === $placeholderMatches = preg_match('/%\w+%/', $message)) {
throw new \Exception(sprintf('Unable to parse message "%s", reason "%s".', $message, preg_last_error_msg()));
}
if (false === $choiceMatches = preg_match('/\{\d+\}/', $message)) {
throw new \Exception(sprintf('Unable to parse message "%s", reason "%s".', $message, preg_last_error_msg()));
}
return match (true) {
0 === $placeholderMatches + $choiceMatches => self::ICU_COMPATIBILITY_YES,
$placeholderMatches + $choiceMatches > 0 => self::ICU_COMPATIBILITY_TO_MIGRATE,
default => self::ICU_COMPATIBILITY_NO,
};
}
private function migrateToICU(string $message): string
{
// dumb approach
return preg_replace('/%(\w+)%/', '{\1}', $message) ?? throw new \Exception(sprintf(
'Unable to replace in message "%s", reason "%s".',
$message,
preg_last_error_msg()
));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment