Closed mazetuski closed 7 years ago
@mazetuski
I use translatable behavior Doctrine extension with EasyAdminBundle but in a very simple way : changing the locale (with a menu integrated to EasyAdmin) permits to change the translation. I have a mechanism to impose that any new article (my app is a kind of blog) has to be first filled in the default locale (en
) and then can be translated in other locales (fr
).
I don't know if it is enough for you or if you want something more sophisticated as described in #865.
You can have look to my repo. If you do, please chose the branch fix_sensiolabs_insight_violations
(work in progress).
I'm afraid there's no news about this and I can't work on this. I know nothing about translatable entities, except that people have a lot of issues working with them. My guess is that this is something that Doctrine should fix or improve and then we (Symfony, bundles) could use them more easily. I'm really sorry.
While preparing to upgrade to SF4 and Flex I had a go at this:
Install the 2 Bundles as per their documentation:
Add a __get function to your translatable entities:
/**
* @ORM\Entity()
*/
class Article
{
use ORMBehaviors\Translatable\Translatable;
public function __call($method, $arguments)
{
$method = ('get' === substr($method, 0, 3) || 'set' === substr($method, 0, 3)) ? $method : 'get'. ucfirst($method);
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
public function __get($name)
{
$method = 'get'. ucfirst($name);
$arguments = [];
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
The __get function will be called by EasyAdmin and so you can reference the fields in the show and list views.
Nothing daunting here:
easy_admin:
entities:
Article:
class: App\Entity\Article
label: 'Artikel'
list:
fields:
- { property: 'title' } #A Translatable field
- { property: 'public' }
form:
fields:
- { property: 'translations', label: false, type: A2lix\TranslationFormBundle\Form\Type\TranslationsType ,
type_options: {
default_locale: '%locale%',
fields: {
title: {field_type: 'Symfony\Component\Form\Extension\Core\Type\TextType' },
description: {field_type: 'Ivory\CKEditorBundle\Form\Type\CKEditorType' }
}
}
}
- { property: 'public' }
That's it, you now have fully translatable entities integrated into EasyAdmin.
The type_options are optional, and only needed if you want to adjust to your needs, like in this example integrating a Wysiwig editor.
For SF4 I had to use a fork fsi-open/IvoryCKEditorBundle. Resources/public is empty in that fork, so I got that from the original.
On SF4 I experienced some CSS small inconsistencies using this approach. For a better matching with the default EasyAdminBundle stylesheet I'd recommend to override the default template file from A2lixTranslationFormBundle into templates/bundles/A2lixTranslationFormBundle/bootstrap_4_layout.html.twig
and use this markup which have already the small adjustments needed:
{% block a2lix_translations_widget %}
{{ form_errors(form) }}
<div class="a2lix_translations">
<ul class="a2lix_translationsLocales nav nav-tabs" role="tablist">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
<li class="nav-item {% if app.request.locale == locale %}active{% endif %}">
<a href="#" class="nav-link" data-toggle="tab" data-target=".{{ translationsFields.vars.id }}_a2lix_translationsFields-{{ locale }}" role="tab">
{{ translationsFields.vars.label|default(locale|humanize)|trans }}
{% if form.vars.default_locale == locale %}{{ '[Default]'|trans }}{% endif %}
{% if translationsFields.vars.required %}*{% endif %}
</a>
</li>
{% endfor %}
</ul>
<div class="a2lix_translationsFields tab-content">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
<div class="{{ translationsFields.vars.id }}_a2lix_translationsFields-{{ locale }} tab-pane {% if app.request.locale == locale %}active{% endif %} {% if not form.vars.valid %}sonata-ba-field-error{% endif %}" role="tabpanel">
{{ form_errors(translationsFields) }}
{{ form_widget(translationsFields) }}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block a2lix_translationsForms_widget %}
{{ block('a2lix_translations_widget') }}
{% endblock %}
(Do a diff between original and overrided file to see difference/changes)
@FireFoxIXI thanks for your input, i'm using SF3.4 and have implemented yur solution, but I get the following error now : The "name" field cannot be used in the "sort" option of the "list" view of the "Project" entity because it's a virtual property that is not persisted in the database.
name
being a translatable property. Can you help?
Here is my entity
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Project
*
* @ORM\Table(name="project")
* @ORM\Entity(repositoryClass="AppBundle\Repository\ProjectRepository")
* @Vich\Uploadable
*/
class Project
{
use ORMBehaviors\Translatable\Translatable;
/**
* @var int
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="city", type="string", length=255)
* @Assert\NotBlank()
*/
private $city;
/**
* @var string
*
* @ORM\Column(name="region", type="string", length=255, nullable=true)
*/
private $region;
/**
* @var string
*
* @ORM\Column(name="country", type="string", length=255)
* @Assert\Country()
*/
private $country;
/**
* @var boolean
*
* @ORM\Column(name="isPublished", type="boolean")
*/
private $isPublished = 0;
/**
* @var boolean
*
* @ORM\Column(name="isFocused", type="boolean")
*/
private $isFocused = 0;
/**
* @var string
*
* @ORM\Column(type="string", length=255)
*/
private $cover;
/**
* @var File
*
* @Vich\UploadableField(mapping="project_covers", fileNameProperty="cover")
* @Assert\Image(
* maxSize="1024k",
* mimeTypes={"image/jpeg", "image/gif","image/png"},
* minWidth = 250,
* minHeight = 250
* )
*/
private $coverFile;
/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
*/
private $updatedAt;
/**
* @ORM\OneToMany(targetEntity="AppBundle\Entity\ProjectMedia", mappedBy="project", cascade={"persist","remove"}, orphanRemoval=true)
*/
private $medias;
/**
* @ORM\ManyToMany(targetEntity="AppBundle\Entity\Tag", inversedBy="projects")
*/
private $tags;
/**
* Project constructor.
*/
public function __construct()
{
$this->medias = new ArrayCollection();
$this->tags = new ArrayCollection();
}
/**
* @param $method
* @param $arguments
* @return mixed
*/
public function __call($method, $arguments)
{
$method = ('get' === substr($method, 0, 3) || 'set' === substr($method, 0, 3)) ? $method : 'get'. ucfirst($method);
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
/**
* @param $name
* @return mixed
*/
public function __get($name)
{
$method = 'get'. ucfirst($name);
$arguments = [];
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
/**
* Get id
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set city
*
* @param string $city
*
* @return Project
*/
public function setCity($city)
{
$this->city = $city;
return $this;
}
/**
* Get city
*
* @return string
*/
public function getCity()
{
return $this->city;
}
/**
* Set region
*
* @param string $region
*
* @return Project
*/
public function setRegion($region)
{
$this->region = $region;
return $this;
}
/**
* Get region
*
* @return string
*/
public function getRegion()
{
return $this->region;
}
/**
* Set country
*
* @param string $country
*
* @return Project
*/
public function setCountry($country)
{
$this->country = $country;
return $this;
}
/**
* Get country
*
* @return string
*/
public function getCountry()
{
return $this->country;
}
/**
* Set isPublished
*
* @param boolean $isPublished
*
* @return Project
*/
public function setIsPublished($isPublished)
{
$this->isPublished = $isPublished;
return $this;
}
/**
* Get isPublished
*
* @return boolean
*/
public function getIsPublished()
{
return $this->isPublished;
}
/**
* Set isFocused
*
* @param boolean $isFocused
*
* @return Project
*/
public function setIsFocused($isFocused)
{
$this->isFocused = $isFocused;
return $this;
}
/**
* Get isFocused
*
* @return boolean
*/
public function getIsFocused()
{
return $this->isFocused;
}
/**
* Set coverFile
*
* @param File|null $cover
*/
public function setCoverFile(File $cover = null)
{
$this->coverFile = $cover;
if ($cover) {
$this->updatedAt = new \DateTime('now');
}
}
/**
* Get coverFile
*
* @return File
*/
public function getCoverFile()
{
return $this->coverFile;
}
/**
* Set cover
*
* @param $cover
*/
public function setCover($cover)
{
$this->cover = $cover;
}
/**
* Get cover
*
* @return mixed
*/
public function getCover()
{
return $this->cover;
}
/**
* Get updatedAt
*
* @return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* Set updatedAt
*
* @param \DateTime $updatedAt
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
}
/**
* Add media
*
* @param ProjectMedia $media
*
* @return Project
*/
public function addMedia(ProjectMedia $media)
{
$this->medias[] = $media;
$media->setProject($this);
return $this;
}
/**
* Remove media
*
* @param ProjectMedia $media
*/
public function removeMedia(ProjectMedia $media)
{
$this->medias->removeElement($media);
}
/**
* Get medias
*
* @return ArrayCollection
*/
public function getMedias()
{
return $this->medias;
}
/**
* Add tag
*
* @param Tag $tag
*
* @return Project
*/
public function addTag(Tag $tag)
{
$this->tags[] = $tag;
$tag->addProject($this);
return $this;
}
/**
* Remove tag
*
* @param Tag $tag
*/
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
It's Translatable counterpart
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
use Symfony\Component\Validator\Constraints as Assert;
/**
* ProjectTranslation
*
* @ORM\Table(name="project_translation")
* @ORM\Entity()
*/
class ProjectTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
* @Assert\NotBlank()
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="tech", type="text")
* @Assert\NotBlank()
*/
private $tech;
/**
* @var string
*
* @ORM\Column(name="description", type="text")
* @Assert\NotBlank()
*/
private $description;
/**
* @var string
*
* @ORM\Column(name="slug", type="string", length=128, unique=true)
* @Gedmo\Slug(fields={"name"})
*/
private $slug;
/**
* Set name
*
* @param string $name
*
* @return ProjectTranslation
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set tech
*
* @param string $tech
*
* @return ProjectTranslation
*/
public function setTech($tech)
{
$this->tech = $tech;
return $this;
}
/**
* Get tech
*
* @return string
*/
public function getTech()
{
return $this->tech;
}
/**
* Set description
*
* @param string $description
*
* @return ProjectTranslation
*/
public function setDescription($description)
{
$this->description = $description;
return $this;
}
/**
* Get description
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* Set slug
*
* @param string $slug
*
* @return ProjectTranslation
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Get slug
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
}
and my config
a2lix_translation_form:
locale_provider: default
locales: [en, fr]
default_locale: fr
required_locales: [en]
templating: "@A2lixTranslationForm/bootstrap_4_layout.html.twig"
And for EasyAdmin
Project:
class: AppBundle\Entity\Project
list:
title: 'Projets'
fields:
- { property: 'cover', type: 'image', base_path: '%app.path.project_covers%' }
- { property: 'name', label: 'Nom' }
- { property: 'city', label: 'Ville' }
- { property: 'country', label: 'Pays' }
- { property: 'isFocused', label: 'Focus' }
- { property: 'isPublished', label: 'Publication' }
- { property: 'tags', label: 'Tags' }
sort: ['name', 'ASC']
form:
fields:
- { property: 'translations', label: false, type: A2lix\TranslationFormBundle\Form\Type\TranslationsType }
- { property: 'coverFile', label: 'Vignette', type: 'vich_image' }
- { property: 'name', label: 'Nom du projet' }
- { property: 'tags', label: 'Tags', type: 'easyadmin_autocomplete' }
- { property: 'city', label: 'Ville' }
- { property: 'region', label: 'Région' }
- { property: 'country', label: 'Pays', type: 'country' }
- { property: 'tech', label: 'Caratéristiques techniques', type: 'FOS\CKEditorBundle\Form\Type\CKEditorType', type_options: { config_name: 'tech_config' } }
- { property: 'description', label: 'Description', type: 'FOS\CKEditorBundle\Form\Type\CKEditorType' }
- { property: 'medias', label: 'Slider', type: 'collection', type_options: { entry_type: 'AppBundle\Form\ProjectMediaType', by_reference: false } }
@FireFoxIXI, how do you solve the issue with search through the field that is translatable?
Also you have to add @A2lixTranslationForm/bootstrap_4_layout.html.twig to easy admin config
easy_admin:
design:
form_theme:
- '@EasyAdmin/form/bootstrap_4.html.twig'
- '@A2lixTranslationForm/bootstrap_4_layout.html.twig'
For SF4 I had to use a fork fsi-open/IvoryCKEditorBundle. Resources/public is empty in that fork, so I got that from the original.
I used:
composer require friendsofsymfony/ckeditor-bundle:2.x-dev
since 4.4 support is not yet ready: https://github.com/FriendsOfSymfony/FOSCKEditorBundle/issues/204
The trick is still working ?
Hello guys! I have troubles with Symfony 5, EasyAdmin 3 and KNP translatable First Entity:
class GradeTaskMark implements TranslatableInterface
{
use TranslatableTrait;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="integer")
*/
private $value;
public function getId(): ?int
{
return $this->id;
}
public function getValue(): ?int
{
return $this->value;
}
public function setValue(int $value): self
{
$this->value = $value;
return $this;
}
public function __call($method, $arguments)
{
$method = ('get' === substr($method, 0, 3) || 'set' === substr($method, 0, 3)) ? $method : 'get'. ucfirst($method);
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
public function __get($name)
{
$method = 'get'. ucfirst($name);
$arguments = [];
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
}
Translation entity:
class GradeTaskMarkTranslation implements TranslationInterface
{
use TranslationTrait;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
}
AdminController:
class GradeTaskMarkCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return GradeTaskMark::class;
}
public function configureFields(string $pageName): iterable
{
return [
Field::new('translations')
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => 'ru',
'fields' => [
'title' => TextType::class,
],
]
)->hideOnIndex(),
TextField::new('title'),
IntegerField::new('value'),
];
}
}
alix config:
a2lix_translation_form:
locale_provider: default
locales: [en, ru]
default_locale: ru
required_locales: [ru, en]
templating: "@A2lixTranslationForm/bootstrap_4_layout.html.twig"
easyadmin config:
easy_admin:
design:
form_theme:
- '@EasyAdmin/form/bootstrap_4.html.twig'
- '@A2lixTranslationForm/bootstrap_4_layout.html.twig'
And i get an error:
The Doctrine type of the "translations" field is "4", which is not supported by EasyAdmin yet.
What problem can be? Help please! Thank you
I made new Field in EasyAdmin:
final class TranslationField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => '%locale%',
]
);
}
}
And got new error from A2lix:
Argument 1 passed to A2lix\AutoFormBundle\ObjectInfo\DoctrineORMInfo::__construct() must be an instance of Doctrine\Common\Persistence\Mapping\ClassMetadataFactory, instance of Doctrine\ORM\Mapping\ClassMetadataFactory given, called in /var/www/kr/sova-grade-form/var/cache/dev/ContainerSd2qxYP/getA2lixAutoForm_Form_Manipulator_DoctrineOrmManipulatorService.php on line 24
I removed parameter type in __construct and method getAssocsConfig
for test, and it works. How can i fix it without changing a2lix bundle?)
Hi,
Any news to make it work with symfo 5 and EasyAdmin 3.x ?
I am also having this error :
The Doctrine type of the "translations" field is "4", which is not supported by EasyAdmin yet.
I guess it come from this :
public function configureFields(string $pageName): iterable
{
return [
Field::new('translations')
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => 'fr',
]
),
IdField::new('id', 'Id')->hideOnForm(),
TextField::new('label', 'Label'),
NumberField::new('coefficient', 'Coefficient'),
];
}
@e1sep0 you have any solution? "The Doctrine type of the "translations" field is "4", which is not supported by EasyAdmin yet."
@e1sep0 you have any solution? "The Doctrine type of the "translations" field is "4", which is not supported by EasyAdmin yet."
I decided so:
<?php
namespace App\Admin\Field;
use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
final class TranslationField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null, $fieldsConfig = []): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => '%locale%',
'fields' => $fieldsConfig,
]
);
}
}
and in CrudController:
public function configureFields(string $pageName): iterable
{
$fieldsConfig = [
'subject' => [
'field_type' => TextareaType::class,
'required' => true,
'label' => 'Тема',
],
'text' => [
'field_type' => CKEditorType::class,
'required' => true,
'label' => 'Текст',
],
];
return [
TranslationField::new('translations', 'Переводы', $fieldsConfig)
->setRequired(true)
->hideOnIndex(),
TextField::new('subject')->hideOnForm()->setLabel('Тема'),
BooleanField::new('isActive')->setLabel('Активность'),
];
}
@e1sep0 thanks a lot, it works! Did you make beautiful tabs?
bootstrap templating not including? how it include "@A2lixTranslationForm/bootstrap_4_layout.html.twig" ?
bootstrap templating not including? how it include "@A2lixTranslationForm/bootstrap_4_layout.html.twig" ?
included:
public function configureCrud(Crud $crud): Crud
{
return $crud
->setFormThemes(
[
'@A2lixTranslationForm/bootstrap_4_layout.html.twig',
'@EasyAdmin/crud/form_theme.html.twig',
'@FOSCKEditor/Form/ckeditor_widget.html.twig',
]
);
}
@e1sep0 thanks a lot, it works! Did you make beautiful tabs?
@e1sep0 it work! thanks a lot!
I hope someone can help me. I have an error when I try to add the translation in the crud. Its : Notice: Undefined index: translatable
I hope someone can help me. I have an error when I try to add the translation in the crud. Its : Notice: Undefined index: translatable
Try use translation. Your question is very abstract, give some your code (Entity, EasyAdmin Controller, Field)
Thanks @e1sep0 for your explanations. It's working!
I just have one problem on the detail view, as I get this exception
Object of class Doctrine\ORM\PersistentCollection could not be converted to string
If I define my own template in the CrudController, it works
->setTemplatePath('admin/.../translations.html.twig')
How did you manage the view on the detail page?
Thanks @e1sep0 for your explanations. It's working!
I just have one problem on the detail view, as I get this exception
Object of class Doctrine\ORM\PersistentCollection could not be converted to string
If I define my own template in the CrudController, it works
->setTemplatePath('admin/.../translations.html.twig')
How did you manage the view on the detail page?
Hi) I don`t use detail view in project. But you can hide TranslationField in detail view and add there your custom fields)
Alright, thanks for your answer @e1sep0 😊
@e1sep0 When you click on the tabs, languages are not switched. There are no errors in the console. What could be the problem?
@e1sep0 When you click on the tabs, languages are not switched. There are no errors in the console. What could be the problem?
Hi) If you have tabs, then Translatable bundle works. It problem of easyadmin. Check js is included in your source code from path:
vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/edit.html.twig
@e1sep0 No, scripts are not loaded. What needs to be done to load?)
@CoolS2 , did you insert in your crud controller ?:
public function configureCrud(Crud $crud): Crud
{
return $crud
->setFormThemes(
[
'@A2lixTranslationForm/bootstrap_4_layout.html.twig',
'@EasyAdmin/crud/form_theme.html.twig',
]
);
}
@e1sep0
public function configureCrud(Crud $crud): Crud
{
return $crud
->setEntityLabelInSingular('Game')
->setEntityLabelInPlural('Games')
->setFormThemes(
[
'@A2lixTranslationForm/bootstrap_4_layout.html.twig',
'@EasyAdmin/crud/form_theme.html.twig',
'@FOSCKEditor/Form/ckeditor_widget.html.twig',
]
)
;
}
public function configureFields(string $pageName): iterable
{
$fieldsConfig = [
'title' => [
'field_type' => TextType::class,
'required' => true,
'label' => 'Title',
],
'description' => [
'field_type' => CKEditorType::class,
'required' => true,
'label' => 'Description',
],
];
$tField = TranslationField::new('translations', 'Translates', $fieldsConfig)
->setRequired(true)
->hideOnIndex();
$id = IdField::new('id');
$title = TextField::new('title')->hideOnForm();
$image = ImageField::new('image')
->setUploadDir('public/assets')
->setBasePath('public/assets');
$createdAt = DateTimeField::new('created_at')->setFormTypeOptions([
'html5' => true,
'years' => range(date('Y'), date('Y') + 5),
'widget' => 'single_text',
]);
$enabled = BooleanField::new('enabled');
$tasks = AssociationField::new('tasks')->setFormTypeOptions([
'by_reference' => false
]);
if (Crud::PAGE_NEW === $pageName || Crud::PAGE_EDIT === $pageName) {
if(Crud::PAGE_EDIT === $pageName) {
$createdAt->setFormTypeOption('disabled', true);
} else {
$today = new \DateTime('now');
$createdAt->setValue($today);
}
$id->hideOnForm();
}
yield $tField;
yield $id;
yield $enabled;
yield $title;
yield $image;
yield $tasks;
yield $createdAt;
}
@e1sep0 In template _js_assets.html.twig Variable "js_asset" does not exist.
I need somewhere set it?
I don`t know, what is it)
@e1sep0 Solved this problem by manually adding custom scripts (jquery and bootstrap) in templates/bundles/EasyAdminBundle/crud/edit.html.twig
{% block configured_javascripts %}
{{ parent() }}
{{ include('@EasyAdmin/includes/_js_assets.html.twig', { assets: edit_form.vars.ea_crud_form.assets.jsAssets }, with_context = false) }}
{{ include('@EasyAdmin/includes/_encore_script_tags.html.twig', { assets: edit_form.vars.ea_crud_form.assets.webpackEncoreAssets }, with_context = false) }}
<script src="{{ asset('jquery.js') }}"></script>
<script src="{{ asset('bootstrap.min.js') }}"></script>
{% endblock %}
I didn't need to include any js. What I've made is a bootstrap 5 template for a2lix translation field instead of use default bootstrap 4.
{% block a2lix_translations_widget %}
{{ form_errors(form) }}
<div class="a2lix_translations">
<div class="a2lix_translationsLocales nav nav-tabs" role="tablist">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
<button class="nav-link {% if app.request.locale == locale %}active{% endif %}" id="{{ translationsFields.vars.id }}_a2lix_translations-tab" data-bs-toggle="tab" data-bs-target="#{{ translationsFields.vars.id }}_a2lix_translations-fields" type="button" role="tab" aria-controls="{{ translationsFields.vars.id }}_a2lix_translations-fields" aria-selected="{{ app.request.locale == locale }}">
{{ translationsFields.vars.label|default(locale|humanize)|trans }}
{% if form.vars.default_locale == locale %}{{ '[Default]'|trans }}{% endif %}
{% if translationsFields.vars.required %}*{% endif %}
</button>
{% endfor %}
</div>
<div class="a2lix_translationsFields tab-content">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
<div id="{{ translationsFields.vars.id }}_a2lix_translations-fields" class="tab-pane fade {% if app.request.locale == locale %}show active{% endif %} {% if not form.vars.valid %}sonata-ba-field-error{% endif %}" role="tabpanel" aria-labelledby="{{ translationsFields.vars.id }}_a2lix_translations-tab">
{{ form_errors(translationsFields) }}
{{ form_widget(translationsFields) }}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block a2lix_translationsForms_widget %}
{{ block('a2lix_translations_widget') }}
{% endblock %}
It is the beginning of 2022 and this solution still works (great). I only have trouble to understand how we can make filters work for translated fields. I have an entity Answer and its content is stored in the translation table under content field. I want to be able to filter answers, cause i have a lot of them. When I try to put something like this in my AnswerCrudController:
public function configureFilters(Filters $filters): Filters
{
return $filters
->add('translations.content')
;
}
where content is the content of the question, I have a Reflection Exception : Given object is not an instance of the class this property was declared in. I cannot figure out what I have to override to make this work. Anyone has any ideas? Thanks!
It is the beginning of 2022 and this solution still works (great). I only have trouble to understand how we can make filters work for translated fields. I have an entity Answer and its content is stored in the translation table under content field. I want to be able to filter answers, cause i have a lot of them. When I try to put something like this in my AnswerCrudController:
public function configureFilters(Filters $filters): Filters { return $filters ->add('translations.content') ; }
where content is the content of the question, I have a Reflection Exception : Given object is not an instance of the class this property was declared in. I cannot figure out what I have to override to make this work. Anyone has any ideas? Thanks!
You can overwrite createIndexQueryBuilder
function like:
public function createIndexQueryBuilder(
SearchDto $searchDto,
EntityDto $entityDto,
FieldCollection $fields,
FilterCollection $filters
): QueryBuilder {
$qb = parent::createIndexQueryBuilder($searchDto, $entityDto, $fields, $filters);
$searchQuery = $searchDto->getQuery();
if ($searchQuery) {
$qb
->leftJoin(UserTranslation::class, 'ut', 'WITH', 'ut.translatable = entity.id')
->orWhere(
'LOWER(ut.firstname) LIKE LOWER(:search) OR LOWER(ut.middlename) LIKE LOWER(:search) OR LOWER(ut.lastname) LIKE LOWER(:search) OR entity.id LIKE :search'
)
->setParameter('search', "%$searchQuery%");
}
return $qb;
}
As you can see, filters data are transferred to function, where you can make your query ;)
@e1sep0 You propose not to use filter but the search bar. I think this is a good idea. With this modification the search bar is kind of useful now. Still would be good to have examples to how to implement it for filter system.
I didn't need to include any js. What I've made is a bootstrap 5 template for a2lix translation field instead of use default bootstrap 4.
Tersoal With a2lix/translation-form-bundle 3.2 you can simply use the Bootstrap 5 template:
public function configureCrud(Crud $crud): Crud
{
return $crud
->setFormThemes(
[
'@A2lixTranslationForm/bootstrap_5_layout.html.twig',
'@EasyAdmin/crud/form_theme.html.twig',
]
);
}
Great solution for translating entities :-). However, I still have one issue. I have not yet found a way to set the translated field "required".
namespace App\Controller\Admin;
// use [...];
class SomeCrudController extends AbstractCrudController
{
// [...]
public function configureFields(string $pageName): iterable
{
// [...]
$translatedFields = [
'test1' => [
'field_type' => TextType::class,
'required' => true,
],
'test2' => [
'field_type' => TextType::class,
'required' => false,
],
];
yield TranslationField::new(
'translations',
'translations',
$translatedFields
)
->setLabel('Translations')
->setTemplatePath('admin/fields/translations.html.twig')
->setRequired(true)
->setColumns('col-md-6 col-xxl-5')
->hideOnIndex();
}
}
The above code makes the field "test1" required in the default locale, but how is it possible to make the translation field required as well?
This seems to be possible for all translated fields if the locale is in "required_locales" in a2lix.yaml but how would you do it for one field only, not for all the translated fields (in all entities)?
a2lix_translation_form:
locale_provider: default
locales: [de, en]
default_locale: de
required_locales: [de, en]
templating: "@A2lixTranslationForm/bootstrap_5_layout.html.twig"
... as it turns out, this can be done in the TranslationField, but as it seems not for one subfield of one TranslationField like "test1" or "test2".
namespace App\Admin\Field;
use App\Admin\GeneralTrait;
use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
final class TranslationField implements FieldInterface
{
use FieldTrait;
/**
* @param string $propertyName
* @param string|null $label
* @param array $fieldsConfig
* @param array $localesRequired
* @return static
*/
public static function new(string $propertyName, ?string $label = null, array $fieldsConfig = [], array $localesRequired = ['de']): self
{
$localeDefault = (new class { use GeneralTrait; })->getAppLocalesDefault();
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => $localeDefault,
'required_locales' => $localesRequired,
'fields' => $fieldsConfig,
]
);
}
}
Found out it is possible for a subfield too. There is an option "locale_options" in A2LIX, see https://a2lix.fr/bundles/translation-form/3.x.html#bundle-configuration
Hi there.
I'm trying to do something similar, with more customization.
I was thinking of something like this:
public function configureFields(string $pageName): iterable
{
$fieldsTextConfig = [
'title' => [
'field_type' => TextType::class,
'required' => true,
'label' => 'Title',
],
'subtitle' => [
'field_type' => TextType::class,
'required' => false,
'label' => 'Subtitle',
],
];
$fieldsTextAreaConfig = [
'summary' => [
'field_type' => TextareaType::class,
'required' => false,
'label' => 'Subtitle',
],
];
return [
IdField::new('id')
->onlyOnIndex(),
TextTranslationField::new('translations', 'Label HERE ?', $fieldsTextConfig),
TextAreaTranslationField::new('translations', 'Label aussi', $fieldsTextAreaConfig)
];
I therefore created 2 translations fields:
final class TextTranslationField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null, $fieldsConfig = []): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/text')
->addCssClass('field-text')
->setDefaultColumns('col-md-6 col-xxl-5')
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => '%locale%',
'fields' => $fieldsConfig,
'excluded_fields' => [
'content',
'slug',
'summary'
]
]
)
;
}
}
final class TextAreaTranslationField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null, $fieldsConfig = []): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/textarea')
->addCssClass('field-textarea')
->addJsFiles(Asset::fromEasyAdminAssetPackage('field-textarea.js')
->onlyOnForms()
)
->setDefaultColumns('col-md-9 col-xxl-7')
->setFormType(TranslationsType::class)
->setFormTypeOptions(
[
'default_locale' => '%locale%',
'fields' => $fieldsConfig,
'excluded_fields' => [
'title',
'subtitle',
'slug',
'content'
]
]
)
;
}
}
Unfortunately the result is not what I expected: only the 'summary' field is displayed because the excluded_fields is general and not specific to the new field created.
Any ideas?
Hello, it works very great! I created the TranslationField and used it in the crudController like this :
public function configureFields(string $pageName): iterable
{
yield TextField::new('tag', 'Tag');
yield TextField::new('name', 'Nom')
->hideOnForm();
yield TextareaField::new('content', 'Contenu')
->hideOnForm();
yield TranslationField::new('translations', 'Traductions', [
'name' => [
'field_type' => TextType::class,
'required' => true,
'label' => 'Name'
],
'content' => [
'field_type' => TextareaType::class,
'required' => true,
'label' => 'Contenu'
]
])
->onlyOnForms();
}
But in index page, there is null
values for 'name' and 'content' instead of their value according the current locale. In a normal controller, I should use $theEntity->translate('theLocale')->getName()
to get the name, or myEntity.translate('theLocale').name
in Twig.
But here, how can I do it automatically ?
EDIT : Ok just add in your entity this :
public function __call($method, $arguments)
{
$method = ('get' === substr($method, 0, 3) || 'set' === substr($method, 0, 3)) ? $method : 'get'. ucfirst($method);
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
public function __get($name)
{
$method = 'get'. ucfirst($name);
$arguments = [];
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
I also used knplabs/doctrine-behaviors
and a2lix/translation-form-bundle
, but solved this task in a different way.
I made a custom TranslationsField
field with the addTranslatableField()
method:
namespace App\EasyAdmin\Field;
use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
class TranslationsField implements FieldInterface
{
use FieldTrait;
public const OPTION_FIELDS_CONFIG = 'fieldsConfig';
public static function new(string $propertyName, ?string $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->onlyOnForms()
->setRequired(true)
->addFormTheme('admin/crud/form/field/translations.html.twig')
->addCssFiles('build/translations-field.css')
->setFormType(TranslationsType::class)
->setFormTypeOption('block_prefix', 'translations_field')
;
}
public function addTranslatableField(FieldInterface $field): self
{
$fieldsConfig = (array)$this->getAsDto()->getCustomOption(self::OPTION_FIELDS_CONFIG);
$fieldsConfig[] = $field;
$this->setCustomOption(self::OPTION_FIELDS_CONFIG, $fieldsConfig);
return $this;
}
}
Then created the admin/crud/form/field/translations.html.twig
theme (based on @A2lixTranslationForm/bootstrap_5_layout.html.twig
template) for this field:
{% block a2lix_translations_widget %}
{{ form_errors(form) }}
<div class="a2lix_translations form-tabs">
<ul class="a2lix_translationsLocales nav nav-tabs" role="tablist">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
{% set errorsNumber = 0 %}
{% for translation in form | filter(translation => translation.vars.name == locale) %}
{% for translationField in translation.children %}
{% if translationField.vars.errors|length %}
{% set errorsNumber = errorsNumber + translationField.vars.errors|length %}
{% endif %}
{% endfor %}
{% endfor %}
<li class="nav-item">
<a href="#{{ translationsFields.vars.id }}_a2lix_translations-fields" class="nav-link {% if app.request.locale == locale %}active{% endif %}" data-bs-toggle="tab" role="tab">
{{ translationsFields.vars.label|default(locale|humanize)|trans }}
{% if translationsFields.vars.required %}<span class="locale-required"></span>{% endif %}
{% if errorsNumber > 0 %}<span class="badge badge-danger" title="{{ errorsNumber }}">{{ errorsNumber }}</span>{% endif %}
</a>
</li>
{% endfor %}
</ul>
<div class="a2lix_translationsFields tab-content">
{% for translationsFields in form %}
{% set locale = translationsFields.vars.name %}
<div id="{{ translationsFields.vars.id }}_a2lix_translations-fields" class="tab-pane {% if app.request.locale == locale %}show active{% endif %} {% if not form.vars.valid %}sonata-ba-field-error{% endif %}" role="tabpanel">
{{ form_errors(translationsFields) }}
{{ form_widget(translationsFields, {'attr': {'class': 'row'}} ) }}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
{% block a2lix_translations_label %}{% endblock %}
{% block a2lix_translationsForms_widget %}
{{ block('a2lix_translations_widget') }}
{% endblock %}
and styles file build/translations-field.css
(just to simplify):
.a2lix_translations > .nav-tabs .nav-item .locale-required:after {
background: var(--color-danger);
border-radius: 50%;
content: "";
display: inline-block;
filter: opacity(75%);
height: 4px;
position: relative;
right: -2px;
top: -8px;
width: 4px;
z-index: var(--zindex-700);
}
Since in this implementation Easyadmin fields will be passed directly and not form types, we need to add field Configurator, which will synchronize parameters for form types, load resources like css and js for the specified fields, and then pass everything to TranslationsType
:
namespace App\EasyAdmin\Field\Configurator;
use App\EasyAdmin\Field\TranslationsField;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldConfiguratorInterface;
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto;
use Symfony\Component\Validator\Constraints\Valid;
class TranslationsConfigurator implements FieldConfiguratorInterface
{
public function __construct(private iterable $fieldConfigurators)
{
}
public function supports(FieldDto $field, EntityDto $entityDto): bool
{
return $field->getFieldFqcn() === TranslationsField::class;
}
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
{
$formTypeOptionsFields = [];
$fieldsCollection = FieldCollection::new(
(array) $field->getCustomOption(TranslationsField::OPTION_FIELDS_CONFIG)
);
foreach ($fieldsCollection as $dto) {
/** @var FieldDto $dto */
// run field configurator manually as translatable fields are not returned/yielded from configureFields()
foreach ($this->fieldConfigurators as $configurator) {
if (!$configurator->supports($dto, $entityDto)) {
continue;
}
$configurator->configure($dto, $entityDto, $context);
}
foreach ($dto->getFormThemes() as $formThemePath) {
$context?->getCrud()?->addFormTheme($formThemePath);
}
// add translatable fields assets
$context->getAssets()->mergeWith($dto->getAssets());
$dto->setFormTypeOption('field_type', $dto->getFormType());
$formTypeOptionsFields[$dto->getProperty()] = $dto->getFormTypeOptions();
}
$field->setFormTypeOptions([
'ea_fields' => $fieldsCollection,
'fields' => $formTypeOptionsFields,
'constraints' => [
new Valid(),
],
]);
}
}
This configurator must be added to config/services.yml:
App\EasyAdmin\Field\Configurator\TranslationsConfigurator:
arguments:
$fieldConfigurators: !tagged_iterator ea.field_configurator
tags:
- { name: 'ea.field_configurator', priority: -10 }
After that, the only thing left to do is to pass ea_crud_form
form view variable to these form types, and for that I made a App\Form\Extension\TranslationsTypeExtension
form type extension:
namespace App\Form\Extension;
use A2lix\TranslationFormBundle\Form\Type\TranslationsType;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TranslationsTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [TranslationsType::class];
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('ea_fields');
$resolver->setAllowedTypes('ea_fields', FieldCollection::class);
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
/** @var FieldCollection $fields */
$fields = $options['ea_fields'];
foreach ($view->children as $translationView) {
foreach ($translationView->children as $fieldView) {
$fieldView->vars['ea_crud_form'] = [
'ea_field' => $fields->getByProperty($fieldView->vars['name'])
];
}
}
}
}
And now it is possible to directly pass Easyadmin fields to TranslationsField
via addTranslatableField()
method:
...
class ArticleCrudController extends AbstractCrudController
{
...
public function configureFields(string $pageName): iterable
{
yield TranslationsField::new('translations')
->addTranslatableField(
TextField::new('title')->setRequired(true)->setHelp('Help message for title')->setColumns(12)
)
->addTranslatableField(
SlugField::new('slug')->setTargetFieldName('title')->setRequired(true)->setHelp('Help message for slug')->setColumns(12)
)
->addTranslatableField(
TextEditorField::new('body')->setRequired(true)->setHelp('Help message for body')->setNumOfRows(6)->setColumns(12)
)
;
}
}
You can see the full code here https://github.com/dzhebrak/easyadmin-translation-form-demo/
@dzhebrak Congratulations!
Hi, Exists one issue in 2016 (#865) but is there anything new to use them?