EasyCorp / EasyAdminBundle

EasyAdmin is a fast, beautiful and modern admin generator for Symfony applications.
MIT License
4.08k stars 1.03k forks source link

Is there anything new to use translatable entities? #1621

Closed mazetuski closed 7 years ago

mazetuski commented 7 years ago

Hi, Exists one issue in 2016 (#865) but is there anything new to use them?

CruzyCruz commented 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).

javiereguiluz commented 7 years ago

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.

FireFoxIXI commented 6 years ago

While preparing to upgrade to SF4 and Flex I had a go at this:

Integrating KnpLabs/DoctrineBehaviors and A2lixTranslationFormBundle

Install the 2 Bundles as per their documentation:

KnpLabs/DoctrineBehaviors

A2lixTranslationFormBundle

Translatable entity

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.

Adjusting your EasyAdmin configuration

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.

elvismdev commented 6 years ago

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)

picks44 commented 6 years ago

@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 } }
ihortymoshenko commented 5 years ago

@FireFoxIXI, how do you solve the issue with search through the field that is translatable?

TheRatG commented 4 years ago

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'
pjezek commented 4 years ago

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

jlemale commented 4 years ago

The trick is still working ?

e1sep0 commented 4 years ago

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

e1sep0 commented 4 years ago

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?)

ProElbs commented 3 years ago

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'),
        ];
    }
allfreelancers commented 3 years ago

@e1sep0 you have any solution? "The Doctrine type of the "translations" field is "4", which is not supported by EasyAdmin yet."

e1sep0 commented 3 years ago

@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('Активность'),
        ];
    }
allfreelancers commented 3 years ago

@e1sep0 thanks a lot, it works! Did you make beautiful tabs?

allfreelancers commented 3 years ago

bootstrap templating not including? how it include "@A2lixTranslationForm/bootstrap_4_layout.html.twig" ?

e1sep0 commented 3 years ago

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 commented 3 years ago

@e1sep0 thanks a lot, it works! Did you make beautiful tabs?

image

allfreelancers commented 3 years ago

@e1sep0 it work! thanks a lot!

LwiDev commented 3 years ago

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

e1sep0 commented 3 years ago

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)

steevechristen commented 3 years ago

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?

e1sep0 commented 3 years ago

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)

steevechristen commented 3 years ago

Alright, thanks for your answer @e1sep0 😊

Warxcell commented 3 years ago

https://github.com/Warxcell/EntityTranslationsBundle

CoolS2 commented 3 years ago

@e1sep0 When you click on the tabs, languages ​​are not switched. There are no errors in the console. What could be the problem?

e1sep0 commented 3 years ago

@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

CoolS2 commented 3 years ago

@e1sep0 No, scripts are not loaded. What needs to be done to load?)

e1sep0 commented 3 years ago

@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',
                ]
            );
    }
CoolS2 commented 3 years ago

@e1sep0 form

    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;
    }
CoolS2 commented 3 years ago

@e1sep0 In template _js_assets.html.twig Variable "js_asset" does not exist.

I need somewhere set it?

e1sep0 commented 3 years ago

I don`t know, what is it)

CoolS2 commented 3 years ago

@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 %}
Tersoal commented 3 years ago

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 %}
SViarbitskaya commented 2 years ago

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!

e1sep0 commented 2 years ago

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 ;)

SViarbitskaya commented 2 years ago

@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.

localhorst commented 2 years ago

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',
                ]
            );
    }
alexander-bozung commented 2 years ago

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"
alexander-bozung commented 2 years ago

... 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,
                ]
            );
    }
}
alexander-bozung commented 2 years ago

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

chrisVdd commented 2 years ago

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?

bastien70 commented 2 years ago

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);
    }
dzhebrak commented 1 year ago

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)
            )
        ;
    }
}

Easyadmin

You can see the full code here https://github.com/dzhebrak/easyadmin-translation-form-demo/

bastien70 commented 1 year ago

@dzhebrak Congratulations!