Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Command/ExportTranslationsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Lexik\Bundle\TranslationBundle\Manager\FileInterface;
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
use Lexik\Bundle\TranslationBundle\Translation\Exporter\ExporterCollector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -17,6 +18,23 @@
*
* @author CΓ©dric Girard <[email protected]>
*/
#[AsCommand(
name: 'lexik:translations:export',
description: 'Export translations from the database to files.',
help: <<<'HELP'
The <info>%command.name%</info> command exports translations from the database back to translation files.

You can filter the export by locales and domains:

<info>php %command.full_name% --locales=en,fr --domains=messages</info>

You can also specify a custom export path:

<info>php %command.full_name% --export-path=/path/to/translations</info>

By default, the command exports all translations. Use <comment>--override</comment> to export only modified translations.
HELP
)]
class ExportTranslationsCommand extends Command
{
private InputInterface $input;
Expand All @@ -37,8 +55,6 @@ public function __construct(
*/
protected function configure(): void
{
$this->setName('lexik:translations:export');
$this->setDescription('Export translations from the database to files.');

$this->addOption(
'locales', 'l', InputOption::VALUE_OPTIONAL,
Expand Down
27 changes: 25 additions & 2 deletions Command/ImportTranslationsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Lexik\Bundle\TranslationBundle\Translation\Importer\FileImporter;
use LogicException;
use ReflectionClass;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -26,6 +27,30 @@
* @author CΓ©dric Girard <[email protected]>
* @author Nikola Petkanski <[email protected]>
*/
#[AsCommand(
name: 'lexik:translations:import',
description: 'Import all translations from flat files (xliff, yml, php) into the database.',
help: <<<'HELP'
The <info>%command.name%</info> command imports translation files from your project into the database.

By default, the command imports translations from:
- Application translation files (<comment>translations/</comment> directory)
- Bundle translation files
- Component translation files

You can filter the import by locales:

<info>php %command.full_name% --locales=en,fr</info>

You can also import from a specific path:

<info>php %command.full_name% --import-path=/path/to/translations</info>

Use <comment>--force</comment> to replace existing translations in the database.
Use <comment>--merge</comment> to merge translations (keeps the latest updatedAt date).
Use <comment>--cache-clear</comment> to remove translation cache files after import.
HELP
)]
class ImportTranslationsCommand extends Command
{
/**
Expand All @@ -48,8 +73,6 @@ public function __construct(
*/
protected function configure(): void
{
$this->setName('lexik:translations:import');
$this->setDescription('Import all translations from flat files (xliff, yml, php) into the database.');

$this->addOption('cache-clear', 'c', InputOption::VALUE_NONE, 'Remove translations cache files for managed locales.');
$this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force import, replace database content.');
Expand Down
29 changes: 5 additions & 24 deletions Controller/RestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,24 @@ public function __construct(
) {
}

/**
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function listAction(Request $request)
public function listAction(Request $request): JsonResponse
{
[$transUnits, $count] = $this->dataGridRequestHandler->getPage($request);

return $this->dataGridFormatter->createListResponse($transUnits, $count);
}

/**
* @param $token
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function listByProfileAction(Request $request, $token)
public function listByProfileAction(Request $request, string $token): JsonResponse
{
[$transUnits, $count] = $this->dataGridRequestHandler->getPageByToken($request, $token);

return $this->dataGridFormatter->createListResponse($transUnits, $count);
}

/**
* @param integer $id
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function updateAction(Request $request, $id)
public function updateAction(Request $request, int $id): JsonResponse
{
$this->checkCsrf();

Expand All @@ -65,13 +55,9 @@ public function updateAction(Request $request, $id)
}

/**
* @param integer $id
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function deleteAction($id)
public function deleteAction(int $id): JsonResponse
{
$this->checkCsrf();

Expand All @@ -87,14 +73,9 @@ public function deleteAction($id)
}

/**
* @param integer $id
* @param string $locale
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function deleteTranslationAction($id, $locale)
public function deleteTranslationAction(int $id, string $locale): JsonResponse
{
$this->checkCsrf();

Expand Down
25 changes: 10 additions & 15 deletions Controller/TranslationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
use Lexik\Bundle\TranslationBundle\Form\Type\TransUnitType;
use Lexik\Bundle\TranslationBundle\Manager\LocaleManagerInterface;
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
use Lexik\Bundle\TranslationBundle\Translation\Translator;
use Lexik\Bundle\TranslationBundle\Translation\TranslatorDecorator;
use Lexik\Bundle\TranslationBundle\Util\Csrf\CsrfCheckerTrait;
use Lexik\Bundle\TranslationBundle\Util\Overview\StatsAggregator;
use Lexik\Bundle\TranslationBundle\Util\Profiler\TokenFinder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
Expand All @@ -22,16 +23,14 @@ class TranslationController extends AbstractController
{
use CsrfCheckerTrait;

public function __construct(private readonly StorageInterface $translationStorage, private readonly StatsAggregator $statsAggregator, private readonly TransUnitFormHandler $transUnitFormHandler, private readonly Translator $lexikTranslator, private readonly TranslatorInterface $translator, private readonly LocaleManagerInterface $localeManager, private readonly ?TokenFinder $tokenFinder)
public function __construct(private readonly StorageInterface $translationStorage, private readonly StatsAggregator $statsAggregator, private readonly TransUnitFormHandler $transUnitFormHandler, private readonly TranslatorInterface $lexikTranslator, private readonly TranslatorInterface $translator, private readonly LocaleManagerInterface $localeManager, private readonly ?TokenFinder $tokenFinder)
{
}

/**
* Display an overview of the translation status per domain.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function overviewAction()
public function overviewAction(): Response
{
$stats = $this->statsAggregator->getStats();

Expand All @@ -40,10 +39,8 @@ public function overviewAction()

/**
* Display the translation grid.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function gridAction()
public function gridAction(): Response
{
$tokens = null;
if ($this->getParameter('lexik_translation.dev_tools.enable') && $this->tokenFinder !== null) {
Expand All @@ -55,12 +52,12 @@ public function gridAction()

/**
* Remove cache files for managed locales.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function invalidateCacheAction(Request $request)
public function invalidateCacheAction(Request $request): Response
{
$this->lexikTranslator->removeLocalesCacheFiles($this->getManagedLocales());
if (method_exists($this->lexikTranslator, 'removeLocalesCacheFiles')) {
$this->lexikTranslator->removeLocalesCacheFiles($this->getManagedLocales());
}

$message = $this->translator->trans('translations.cache_removed', [], 'LexikTranslationBundle');

Expand All @@ -77,10 +74,8 @@ public function invalidateCacheAction(Request $request)

/**
* Add a new trans unit with translation for managed locales.
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function newAction(Request $request)
public function newAction(Request $request): Response
{
$form = $this->createForm(TransUnitType::class, $this->transUnitFormHandler->createFormData(), $this->transUnitFormHandler->getFormOptions());

Expand Down
132 changes: 128 additions & 4 deletions DependencyInjection/Compiler/RegisterMappingPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Lexik\Bundle\TranslationBundle\DependencyInjection\Compiler;

use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

Expand All @@ -27,10 +30,131 @@ public function process(ContainerBuilder $container): void
$mongodbDriverId = sprintf('doctrine_mongodb.odm.%s_metadata_driver', $name);

if (StorageInterface::STORAGE_ORM == $storage['type'] && $container->hasDefinition($ormDriverId)) {
$container->getDefinition($ormDriverId)->addMethodCall(
'addDriver',
[new Reference('lexik_translation.orm.metadata.xml'), 'Lexik\Bundle\TranslationBundle\Model']
);
// Models now use PHP attributes, so we need to use AttributeDriver
// Create attribute driver if it doesn't exist (fallback if Extension didn't create it)
$attributeDriverId = 'lexik_translation.orm.metadata.attribute';

// Ensure attribute driver exists - create it if Extension didn't create it
if (!$container->hasDefinition($attributeDriverId)) {
// Calculate bundle path using ReflectionClass
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
$bundleDir = dirname($bundleReflection->getFileName());
$modelPath = $bundleDir . '/Model';

// Try to get realpath
$realModelPath = realpath($modelPath);
if ($realModelPath) {
$modelPath = $realModelPath;
}

// Create AttributeDriver service
$driverDefinition = new Definition(AttributeDriver::class, [
[$modelPath]
]);
$driverDefinition->setPublic(false);
$container->setDefinition($attributeDriverId, $driverDefinition);
}

// Register attribute driver for models namespace
// IMPORTANT: We need to check if it's already registered to avoid duplicates
$ormDriver = $container->getDefinition($ormDriverId);
$methodCalls = $ormDriver->getMethodCalls();

// Check if attribute driver is already registered
$attributeDriverRegistered = false;
foreach ($methodCalls as $call) {
if ($call[0] === 'addDriver' &&
isset($call[1][0]) &&
$call[1][0] instanceof Reference &&
(string)$call[1][0] === $attributeDriverId) {
$attributeDriverRegistered = true;
break;
}
}

// Register XML driver for Entity namespace FIRST (entities use XML mapping)
// This must be registered before Model namespace to ensure entities are recognized
// Create XML driver for entities if it doesn't exist
$entityXmlDriverId = 'lexik_translation.orm.metadata.entity.xml';
if (!$container->hasDefinition($entityXmlDriverId)) {
// Use the same XML driver class but with different path for entities
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
$bundleDir = dirname($bundleReflection->getFileName());
$doctrinePath = $bundleDir . '/Resources/config/doctrine';

$realDoctrinePath = realpath($doctrinePath);
if ($realDoctrinePath) {
$doctrinePath = $realDoctrinePath;
}

// Create XML driver for entities using the same class as the model XML driver
$xmlDriverClass = $container->getParameter('doctrine.orm.metadata.xml.class');
$entityDriverDefinition = new Definition($xmlDriverClass, [
[$doctrinePath => 'Lexik\Bundle\TranslationBundle\Entity'],
SimplifiedXmlDriver::DEFAULT_FILE_EXTENSION,
true
]);
$entityDriverDefinition->setPublic(false);
$container->setDefinition($entityXmlDriverId, $entityDriverDefinition);
}

// Register XML driver for Entity namespace FIRST
$entityDriverRegistered = false;
foreach ($methodCalls as $call) {
if ($call[0] === 'addDriver' &&
isset($call[1][1]) &&
$call[1][1] === 'Lexik\Bundle\TranslationBundle\Entity') {
$entityDriverRegistered = true;
break;
}
}

if (!$entityDriverRegistered && $container->hasDefinition($entityXmlDriverId)) {
// Insert at the beginning to ensure Entity namespace is processed first
$newMethodCalls = [[
'addDriver',
[new Reference($entityXmlDriverId), 'Lexik\Bundle\TranslationBundle\Entity']
]];
foreach ($methodCalls as $call) {
$newMethodCalls[] = $call;
}
$ormDriver->setMethodCalls($newMethodCalls);
$methodCalls = $newMethodCalls; // Update for next checks
}

// Register attribute driver if not already registered
if (!$attributeDriverRegistered && $container->hasDefinition($attributeDriverId)) {
// Remove any existing registration for the Model namespace (XML driver)
$newMethodCalls = [];
foreach ($methodCalls as $call) {
// Skip XML driver registration for Model namespace
if ($call[0] === 'addDriver' &&
isset($call[1][1]) &&
$call[1][1] === 'Lexik\Bundle\TranslationBundle\Model' &&
isset($call[1][0]) &&
$call[1][0] instanceof Reference &&
(string)$call[1][0] === 'lexik_translation.orm.metadata.xml') {
// Skip this call - we'll replace it with AttributeDriver
continue;
}
$newMethodCalls[] = $call;
}

// Add attribute driver for Model namespace
$newMethodCalls[] = [
'addDriver',
[new Reference($attributeDriverId), 'Lexik\Bundle\TranslationBundle\Model']
];

// Update method calls
$ormDriver->setMethodCalls($newMethodCalls);
} elseif (!$container->hasDefinition($attributeDriverId) && $container->hasDefinition('lexik_translation.orm.metadata.xml')) {
// Fallback to XML driver only if attribute driver doesn't exist
$ormDriver->addMethodCall(
'addDriver',
[new Reference('lexik_translation.orm.metadata.xml'), 'Lexik\Bundle\TranslationBundle\Model']
);
}
}

if (StorageInterface::STORAGE_MONGODB == $storage['type'] && $container->hasDefinition($mongodbDriverId)) {
Expand Down
Loading