arxy/entity-translations-bundle
Installation
composer require arxy/entity-translations-bundle
Register the bundle in config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3):
Arxy\EntityTranslationsBundle\ArxyEntityTranslationsBundle::class => ['all' => true],
Configure Doctrine
Add to config/packages/doctrine.yaml:
doctrine:
orm:
resolve_target_entities:
Arxy\EntityTranslationsBundle\Model\Language: App\Entity\Language
Define Core Entities
\Arxy\EntityTranslationsBundle\Model\Language):
#[ORM\Entity]
class Language implements LanguageInterface
{
#[ORM\Id, ORM\Column(type: 'string', length: 5)]
private string $locale;
}
\Arxy\EntityTranslationsBundle\Model\Translatable):
#[ORM\Entity]
class Product implements Translatable
{
#[ORM\OneToMany(targetEntity: ProductTranslation::class, mappedBy: 'translatable', cascade: ['ALL'], orphanRemoval: true)]
private Collection $translations;
public function __construct() {
$this->translations = new ArrayCollection();
}
public function addTranslation(ProductTranslation $translation): void {
$this->translations->add($translation);
$translation->setTranslatable($this);
}
public function removeTranslation(ProductTranslation $translation): void {
$this->translations->removeElement($translation);
}
public function setCurrentTranslation(?Translation $translation): void {
// Used internally by the bundle
}
}
\Arxy\EntityTranslationsBundle\Model\Translation):
#[ORM\Entity]
class ProductTranslation implements Translation
{
#[ORM\Id, ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'translations')]
private ?Product $translatable;
#[ORM\Id, ORM\ManyToOne(targetEntity: Language::class)]
private ?Language $language;
#[ORM\Column(type: 'string')]
private string $name;
}
First Usage Inject the translator service and translate an entity:
use Arxy\EntityTranslationsBundle\Translator;
public function __construct(private Translator $translator) {}
public function showProduct(Product $product, string $locale = 'en') {
$this->translator->initializeTranslation($product, $locale);
return $product->getName(); // Returns translated name
}
$this->translator->initializeTranslation($entity, 'fr');
// Automatically sets $entity->setCurrentTranslation() and falls back to configured locales if needed.
$translatedName = $this->translator->translate($product, 'name', 'es');
// Bypasses entity methods; useful for dynamic fields.
class ProductTranslationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('name', TextType::class, [
'constraints' => [
new NotBlank(['groups' => ['en']]), // English is required
new Length(['max' => 100, 'groups' => ['fr']]), // French has length constraint
],
]);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(['data_class' => ProductTranslation::class]);
}
}
$builder->add('translations', TranslationsType::class, [
'entry_type' => ProductTranslationType::class,
'em' => 'doctrine.orm.entity_manager',
'entry_language_options' => [
'en' => ['required' => true],
'fr' => ['required' => false],
],
]);
# config/packages/twig.yaml
twig:
form_themes: ['@ArxyEntityTranslations/bootstrap_4_tab_layout.html.twig']
{{ product|translate('name') }} {# Uses current locale #}
{{ product|translate('name', 'de') }} {# Forces German #}
{% set translation = product|translation('ja') %}
{% if translation %}
{{ translation.name }} {# Japanese name #}
{% endif %}
$this->translator->setLocale('pt_BR');
// Affects all managed entities.
$this->translator->detach($product);
// Prevents automatic translation updates.
$builder->add('description', TextType::class, [
'constraints' => [
new NotBlank(['groups' => ['en']]),
new Length(['max' => 500, 'groups' => ['es']]),
],
]);
$entityManager->getEventManager()->addEventListener(
ORM\Events::postPersist,
function (ORM\Event\LifecycleEventArgs $args) {
$entity = $args->getObject();
if ($entity instanceof Translatable) {
$this->translator->initializeTranslation($entity);
}
}
);
#[Serializer\SerializedName('name')]
public function getName(): ?string {
return $this->currentTranslation?->getName();
}
$translator = $this->createMock(Translator::class);
$translator->method('translate')->willReturn('Mocked Name');
$this->container->set(Translator::class, $translator);
setCurrentTranslationCall to undefined method App\Entity\Product::setCurrentTranslation().Translatable entity implements:
public function setCurrentTranslation(?Translation $translation): void {}
groups: ['en']) are ignored.TranslationsType requires explicit required: true in entry_language_options.entry_language_options:
en: { required: true } # Explicitly mark as required
Cannot determine which entity class to use for form type "...".data_class is set in your TranslationType:
$resolver->setDefault('data_class', ProductTranslation::class);
initializeTranslation() returns a fallback locale (e.g., en instead of fr).framework.translator.fallback_locales in config/packages/framework.yaml:
framework:
translator:
fallbacks: ["en"] # Ensure fallback is configured
JOIN:
$qb = $entityManager->createQueryBuilder()
->select('p, t')
->from(Product::class, 'p')
->leftJoin('p.translations', 't')
->where('t.language = :locale')
->setParameter('locale', 'fr');
$locale = $this->translator->initializeTranslation($product, 'it');
// Log
How can I help you explore Laravel packages today?