New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Is there anything new to use translatable entities? #1621
Comments
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 ( 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 |
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: Integrating KnpLabs/DoctrineBehaviors and A2lixTranslationFormBundleInstall the 2 Bundles as per their documentation: Translatable entityAdd 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 configurationNothing 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 {% 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 : Here is my entity
It's Translatable counterpart
and my config
And for EasyAdmin
|
@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
|
I used: |
The trick is still working ? |
Hello guys! I have troubles with Symfony 5, EasyAdmin 3 and KNP translatable
Translation entity:
AdminController:
alix config:
easyadmin config:
And i get an error: What problem can be? Help please! Thank you |
I made new Field in EasyAdmin:
And got new error from A2lix: I removed parameter type in __construct and method |
Hi, Any news to make it work with symfo 5 and EasyAdmin 3.x ? I am also having this error : I guess it come from this :
|
@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" ? |
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 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 |
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
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? |
Hi) If you have tabs, then Translatable bundle works. It problem of easyadmin. Check js is included in your source code from path: |
@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 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
|
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.
|
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:
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 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. |
Tersoal With a2lix/translation-form-bundle 3.2 you can simply use the Bootstrap 5 template:
|
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:
I therefore created 2 translations fields:
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 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 I made a custom 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 {% 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 .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 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 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 ...
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 |
Hi, Exists one issue in 2016 (#865) but is there anything new to use them?
The text was updated successfully, but these errors were encountered: