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
29 changes: 25 additions & 4 deletions DependencyInjection/Compiler/TranslatorPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,36 @@ public function process(ContainerBuilder $container): void
}
}

// Find the translator service by alias or class name
$translatorId = null;
if ($container->hasDefinition('lexik_translation.translator')) {
$translatorId = 'lexik_translation.translator';
} elseif ($container->hasAlias('lexik_translation.translator')) {
$translatorId = (string) $container->getAlias('lexik_translation.translator');
} elseif ($container->hasDefinition('Lexik\Bundle\TranslationBundle\Translation\Translator')) {
$translatorId = 'Lexik\Bundle\TranslationBundle\Translation\Translator';
}

if ($translatorId && $container->hasDefinition($translatorId)) {
$translatorDef = $container->findDefinition($translatorId);

if (Kernel::VERSION_ID >= 30300) {
$serviceRefs = [...$loadersReferencesById, ...['event_dispatcher' => new Reference('event_dispatcher')]];

$container->findDefinition('lexik_translation.translator')
->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceRefs))
->replaceArgument(3, $loaders);
// Use named arguments if available, otherwise use numeric indices
if ($translatorDef->getArguments() && array_key_exists('$container', $translatorDef->getArguments())) {
$translatorDef->replaceArgument('$container', ServiceLocatorTagPass::register($container, $serviceRefs));
$translatorDef->replaceArgument('$loaderIds', $loaders);
} else {
$translatorDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $serviceRefs));
$translatorDef->replaceArgument(3, $loaders);
}
} else {
$container->findDefinition('lexik_translation.translator')->replaceArgument(2, $loaders);
if ($translatorDef->getArguments() && array_key_exists('$loaderIds', $translatorDef->getArguments())) {
$translatorDef->replaceArgument('$loaderIds', $loaders);
} else {
$translatorDef->replaceArgument(2, $loaders);
}
}
}

Expand Down
60 changes: 53 additions & 7 deletions DependencyInjection/LexikTranslationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

namespace Lexik\Bundle\TranslationBundle\DependencyInjection;

use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Form\Form;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Lexik\Bundle\TranslationBundle\Manager\LocaleManagerInterface;
use Lexik\Bundle\TranslationBundle\Storage\StorageInterface;
use Symfony\Component\Config\FileLocator;
Expand Down Expand Up @@ -41,8 +42,8 @@ public function load(array $configs, ContainerBuilder $container): void
$configuration = new Configuration();
$config = $processor->processConfiguration($configuration, $configs);

$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.xml');
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');

// set parameters
sort($config['managed_locales']);
Expand All @@ -62,7 +63,9 @@ public function load(array $configs, ContainerBuilder $container): void

$objectManager = $config['storage']['object_manager'] ?? null;

$this->buildTranslatorDefinition($container);
// Translator is now handled via service decoration in services.yaml
// buildTranslatorDefinition() is deprecated and no longer needed for Symfony 8
// $this->buildTranslatorDefinition($container);
$this->buildTranslationStorageDefinition($container, $config['storage']['type'], $objectManager);

if (true === $config['auto_cache_clean']) {
Expand Down Expand Up @@ -162,7 +165,11 @@ protected function buildTranslationStorageDefinition(ContainerBuilder $container
if (StorageInterface::STORAGE_ORM == $storage) {
$args = [new Reference('doctrine'), $objectManager ?? 'default'];

// Create XML driver for backward compatibility
$this->createDoctrineMappingDriver($container, 'lexik_translation.orm.metadata.xml', '%doctrine.orm.metadata.xml.class%');

// Create attribute driver for models (MappedSuperclass) that now use PHP attributes
$this->createDoctrineAttributeDriver($container, 'lexik_translation.orm.metadata.attribute');

$metadataListener = new Definition();
$metadataListener->setClass('%lexik_translation.orm.listener.class%');
Expand Down Expand Up @@ -209,6 +216,37 @@ protected function createDoctrineMappingDriver(ContainerBuilder $container, $dri
$container->setDefinition($driverId, $driverDefinition);
}

/**
* Create an attribute driver for models (MappedSuperclass) that use PHP attributes.
*
* @param ContainerBuilder $container
* @param string $driverId
*/
protected function createDoctrineAttributeDriver(ContainerBuilder $container, $driverId)
{
// Calculate bundle path using ReflectionClass to get the actual bundle location
// This works even when the bundle is installed via Composer or symlinked
$bundleReflection = new \ReflectionClass(\Lexik\Bundle\TranslationBundle\LexikTranslationBundle::class);
$bundleDir = dirname($bundleReflection->getFileName());
$modelPath = $bundleDir . '/Model';

// Try to get realpath, but use the calculated path if it fails
$realModelPath = realpath($modelPath);
if ($realModelPath) {
$modelPath = $realModelPath;
}

// AttributeDriver constructor expects an array of paths (directories to scan)
// It will automatically detect classes with #[ORM\MappedSuperclass] or #[ORM\Entity] attributes
$driverDefinition = new Definition(AttributeDriver::class, [
[$modelPath]
]);
$driverDefinition->setPublic(false);

// Always set/override the definition to ensure it exists with correct arguments
$container->setDefinition($driverId, $driverDefinition);
}

/**
* Load dev tools.
*/
Expand All @@ -230,15 +268,23 @@ protected function buildDevServicesDefinition(ContainerBuilder $container)
*/
protected function registerTranslatorConfiguration(array $config, ContainerBuilder $container)
{
// use the Lexik translator as default translator service
// use the Lexik translator decorator as default translator service
$alias = $container->setAlias('translator', 'lexik_translation.translator');

if (Kernel::VERSION_ID >= 30400) {
$alias->setPublic(true);
}

$translator = $container->findDefinition('lexik_translation.translator');
$translator->addMethodCall('setFallbackLocales', [$config['fallback_locale']]);
// Get the inner translator (the actual Symfony translator) for adding resources
// The decorator will delegate to it
$innerTranslator = $container->hasDefinition('lexik_translation.translator.inner')
? $container->findDefinition('lexik_translation.translator.inner')
: $container->findDefinition('translator');

$innerTranslator->addMethodCall('setFallbackLocales', [$config['fallback_locale']]);

// For adding file resources, we'll add them to the inner translator
$translator = $innerTranslator;

$registration = $config['resources_registration'];

Expand Down
24 changes: 21 additions & 3 deletions Document/TransUnitRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@ public function getAllDomainsByLocale(): array
}

$domain = $domainGroup['_id'];
$locales = \array_merge(...$domainGroup['locales']);
// Flatten the locales array - handle both array of arrays and array of strings
$locales = [];
foreach ($domainGroup['locales'] as $localeItem) {
if (\is_array($localeItem)) {
$locales = \array_merge($locales, $localeItem);
} else {
$locales[] = $localeItem;
}
}
// Remove duplicates
$locales = \array_unique($locales);

foreach ($locales as $locale) {
$domainsByLocale[] = [
Expand Down Expand Up @@ -244,8 +254,16 @@ public function getTranslationsForFile(ModelFile $file, $onlyUpdated)
while ($i < (is_countable($result['translations']) ? count($result['translations']) : 0) && null === $content) {
if ($file->getLocale() == $result['translations'][$i]['locale']) {
if ($onlyUpdated) {
$updated = ($result['translations'][$i]['createdAt'] < $result['translations'][$i]['updatedAt']);
$content = $updated ? $result['translations'][$i]['content'] : null;
// Handle MongoDB Timestamp objects - they have a 'sec' property
$createdAt = $result['translations'][$i]['createdAt'] ?? null;
$updatedAt = $result['translations'][$i]['updatedAt'] ?? null;

if ($createdAt && $updatedAt) {
$createdAtSec = \is_object($createdAt) && \property_exists($createdAt, 'sec') ? $createdAt->sec : $createdAt;
$updatedAtSec = \is_object($updatedAt) && \property_exists($updatedAt, 'sec') ? $updatedAt->sec : $updatedAt;
$updated = ($createdAtSec < $updatedAtSec);
$content = $updated ? $result['translations'][$i]['content'] : null;
}
} else {
$content = $result['translations'][$i]['content'];
}
Expand Down
80 changes: 80 additions & 0 deletions EventDispatcher/DatabaseResourcesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Lexik\Bundle\TranslationBundle\EventDispatcher;

use Lexik\Bundle\TranslationBundle\EventDispatcher\Event\GetDatabaseResourcesEvent;
use Lexik\Bundle\TranslationBundle\Translation\Loader\DatabaseLoader;
use Lexik\Bundle\TranslationBundle\Translation\TranslatorDecorator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* Event listener to add database translation resources to the translator.
*
* This listener ensures that database translations are loaded before the translator
* is used. It works with Symfony 8 where Translator is final by using the
* DatabaseLoader directly.
*
* @author Cédric Girard <[email protected]>
*/
class DatabaseResourcesListener implements EventSubscriberInterface
{
private bool $resourcesAdded = false;

public function __construct(
#[Autowire(service: 'translator')]
private readonly TranslatorInterface $translator,
#[Autowire(service: 'Lexik\Bundle\TranslationBundle\Translation\Loader')]
private readonly DatabaseLoader $databaseLoader,
#[Autowire([
'resources_type' => '%lexik_translation.resources_type%'
])]
private readonly array $options
) {
}

public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['addDatabaseResources', 10],
];
}

public function addDatabaseResources(RequestEvent $event): void
{
if ($this->resourcesAdded) {
return;
}

$resourcesType = $this->options['resources_type'] ?? 'all';
if ('all' !== $resourcesType && 'database' !== $resourcesType) {
return;
}

// Get database resources via event
$eventDispatcher = $event->getKernel()->getContainer()->get('event_dispatcher');
$getResourcesEvent = new GetDatabaseResourcesEvent();
$eventDispatcher->dispatch($getResourcesEvent);

$resources = $getResourcesEvent->getResources();

// Add resources to translator if it's our decorator
if ($this->translator instanceof TranslatorDecorator) {
$this->translator->addDatabaseResources();
} elseif (method_exists($this->translator, 'addResource')) {
// Fallback for direct translator access
foreach ($resources as $resource) {
$locale = $resource['locale'];
$domain = $resource['domain'] ?? 'messages';
$this->translator->addResource('database', 'DB', $locale, $domain);
}
}
// Note: If neither works, the DatabaseLoader will still be called
// automatically when translations are requested for a locale/domain

$this->resourcesAdded = true;
}
}
Loading