## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require a2lix/i18n-doctrine-bundle
Add to config/bundles.php:
A2lix\I18nDoctrineBundle\A2lixI18nDoctrineBundle::class => ['all' => true],
Configure Locales:
Update config/packages/a2lix_i18n_doctrine.yaml:
a2lix_i18n_doctrine:
default_locale: en
supported_locales: en, fr, es
Enable Translatable Entities:
Annotate an entity (e.g., Product):
use A2lix\I18nDoctrineBundle\Model\I18n;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="products")
* @I18n
*/
class Product
{
// ...
}
First Use Case:
Translate a field (e.g., name):
$product = new Product();
$product->setName('en', 'Laptop'); // Set English translation
$product->setName('fr', 'Ordinateur portable'); // Set French translation
Entity Translation:
Use setField($locale, $value) and getField($locale) for dynamic translations.
$product->setDescription('es', 'Descripción detallada');
$currentLocale = app()->getLocale(); // e.g., 'fr'
$description = $product->getDescription($currentLocale);
Form Integration: Extend Symfony forms with locale-aware fields:
$builder->add('name', TextType::class, [
'i18n' => true, // Enable translation
'locales' => ['en', 'fr'], // Supported locales
]);
Repository Queries: Filter by locale in DQL:
$qb = $entityManager->createQueryBuilder();
$qb->select('p')
->from(Product::class, 'p')
->where('p.name LIKE :name')
->andWhere('p.locale = :locale')
->setParameter('locale', 'en');
Fallback Logic:
Configure fallback locales in config:
a2lix_i18n_doctrine:
fallback_locales: [en, fr]
Automatically fall back if a translation is missing:
$product->getName(); // Falls back to 'en' if 'fr' is missing
Event Listeners:
Use prePersist/preUpdate to validate translations:
$entityManager->getEventManager()->addEventListener(
I18n::class,
new I18nEventListener($supportedLocales)
);
symfony/translation for dynamic locale switching:
$translator->trans($product->getName($locale));
JsonSerializable:
public function jsonSerialize() {
return [
'name' => $this->getName(app()->getLocale()),
'translations' => [
'en' => $this->getName('en'),
'fr' => $this->getName('fr'),
],
];
}
# EasyAdmin config
fields:
- { property: 'name', type: 'i18n', locales: ['en', 'fr'] }
Performance:
DISTINCT in queries:
$qb->select('DISTINCT p')
->from(Product::class, 'p');
Locale Mismatches:
app()->getLocale() matches the expected locale in queries/forms.$product->hasTranslation('name', 'fr'); // Returns bool
Database Schema:
locale column and translation tables (e.g., product_translations).php bin/console doctrine:schema:update --force after installation to avoid schema conflicts.Symfony 4+:
Circular References:
@I18n on ManyToOne) can cause infinite loops. Use @ORM\Transient or lazy loading:
/**
* @ORM\Transient
*/
public function getTranslatedName($locale) { ... }
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
logging: true
profiling: true
I18n events are triggered:
php bin/console debug:event-dispatcher
A2lix\I18nDoctrineBundle\Validator\Constraints\Locale validator to ensure locales are supported.Custom Fields: Extend the bundle for custom field types (e.g., rich text):
use A2lix\I18nDoctrineBundle\Mapping\Annotation as I18n;
/**
* @I18n\TranslatableField(fieldName="description", type="text")
*/
Bulk Updates: Use Doctrine batch processing for locale updates:
$em = $entityManager;
$products = $em->getRepository(Product::class)->findAll();
foreach ($products as $product) {
$product->setName('es', 'Updated Name');
$em->persist($product);
if ($i % 20 === 0) {
$em->flush();
$em->clear();
}
$i++;
}
Testing: Mock the locale service in PHPUnit:
$this->container->set('translator.default_locale', 'test');
$this->container->set('a2lix_i18n_doctrine.locale_provider', new TestLocaleProvider('test'));
Fallback Priorities: Override fallback logic in a custom listener:
public function onPreFlush(I18nEvent $event) {
$entity = $event->getEntity();
if (!$entity->hasTranslation('title', 'fr')) {
$entity->setTitle('fr', $entity->getTitle('en'));
}
}
Legacy Data: Migrate existing data with a custom migration:
public function up(SchemaManagerInterface $manager) {
$connection = $manager->getConnection();
$connection->executeStatement('
INSERT INTO product_translations (id, field, locale, value)
SELECT id, "name", "en", name FROM products
');
}
```markdown
## Extension Points
1. **Custom Storage**:
Override the default translation storage (e.g., JSON column) by implementing `A2lix\I18nDoctrineBundle\Storage\TranslationStorageInterface`.
2. **Locale Providers**:
Extend locale resolution with a custom provider:
```php
class RequestLocaleProvider implements LocaleProviderInterface {
public function getLocale() {
return request()->getLocale();
}
}
Register in services:
services:
App\Service\RequestLocaleProvider:
tags: ['a2lix_i18n_doctrine.locale_provider']
Event Subscribers:
Hook into lifecycle events (e.g., onFlush) to enforce business rules:
$subscriber = new class implements EventSubscriberInterface {
public function getSubscribedEvents() {
return [I18n::PRE_PERSIST];
}
public function prePersist(I18nEvent $event) {
// Validate translations here
}
};
Doctrine Filters: Combine with Doctrine filters for locale-specific queries:
$filter = $entityManager->getFilters()->enable('i18n');
$filter->setParameter('locale', 'fr');
API Platform: Integrate with API Platform for automatic serialization:
How can I help you explore Laravel packages today?