PHPackages                             chamber-orchestra/translation-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. chamber-orchestra/translation-bundle

ActiveSymfony-bundle[Database &amp; ORM](/categories/database)

chamber-orchestra/translation-bundle
====================================

Symfony bundle for multilingual entity localization and database-backed form field translations with XLIFF export.

v8.1.2(1mo ago)01.8k↓82.4%MITPHPPHP ^8.5CI passing

Since Feb 19Pushed 5mo agoCompare

[ Source](https://github.com/chamber-orchestra/translation-bundle)[ Packagist](https://packagist.org/packages/chamber-orchestra/translation-bundle)[ Docs](https://github.com/chamber-orchestra/translation-bundle)[ RSS](/packages/chamber-orchestra-translation-bundle/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (40)Versions (6)Used By (0)

[![PHP Composer](https://github.com/chamber-orchestra/translation-bundle/actions/workflows/php.yml/badge.svg)](https://github.com/chamber-orchestra/translation-bundle/actions/workflows/php.yml)

ChamberOrchestra Translation Bundle
===================================

[](#chamberorchestra-translation-bundle)

A Symfony 8 bundle for multilingual applications. Provides two complementary i18n systems:

1. **Entity localization** — multi-locale Doctrine entity pairs with automatic ORM relationship mapping at runtime.
2. **Form field localization** — database-backed translation keys for individual form fields with YAML export.

Features
--------

[](#features)

- **Automatic ORM mapping** via `TranslateSubscriber`: maps `oneToMany`/`manyToOne` associations at runtime — no manual Doctrine mapping required.
- **Locale fallback chain** in `translate()`: requested locale → language fallback (`en_US` → `en`) → kernel default locale.
- **`TranslatableProxyTrait`** for transparent property delegation: `$post->title` reads from the current translation without extra calls.
- **Form field localization** via `localization: true` on `TextType`, `TextareaType`, and `WysiwygType` — stores opaque UUID-based keys in the entity, displays human-readable values in the form.
- **Built-in `TranslationEventSubscriber`** — automatically creates or updates `Translation` entities when a localized form field is submitted.
- **`LocalizationLoaderChain`** — tagged, prioritized loader chain for resolving translation values when rendering a localized form field; extend with custom loaders.
- **`ExportTranslationCommand`** (`translation:export`) — writes un-exported `Translation` records to `{domain}+intl-icu.{locale}.yaml` files grouped by domain, marks them as exported, and dispatches `TranslationExportedEvent`.
- **CMS integration** (optional, requires `chamber-orchestra/cms-bundle`) — `TranslationsType` collection pre-populated per locale, rendered as Bootstrap nav tabs.

Requirements
------------

[](#requirements)

- PHP ^8.5
- Symfony 8.0 (framework-bundle, form, translation, uid, console, http-foundation)
- Doctrine ORM ^3.0 + DoctrineBundle ^3.0

Optional:

- `chamber-orchestra/doctrine-clock-bundle` — required if translatable entities use `TimestampCreateTrait`
- `chamber-orchestra/cms-bundle` — CMS form integration (`TranslationsType`, `AbstractTranslatableDto`)

Installation
------------

[](#installation)

```
composer require chamber-orchestra/translation-bundle
```

Enable the bundle in `config/bundles.php`:

```
return [
    // ...
    ChamberOrchestra\TranslationBundle\ChamberOrchestraTranslationBundle::class => ['all' => true],
];
```

Usage
-----

[](#usage)

### System 1: Entity Localization

[](#system-1-entity-localization)

Define a translatable/translation entity pair. The `TranslateSubscriber` maps their Doctrine relationship automatically.

**Translatable entity** — implements `TranslatableInterface` + uses `TranslatableTrait`:

```
use ChamberOrchestra\TranslationBundle\Contracts\Entity\TranslatableInterface;
use ChamberOrchestra\TranslationBundle\Entity\TranslatableTrait;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Post implements TranslatableInterface
{
    use TranslatableTrait;

    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private int $id;

    // No manual Doctrine mapping needed for $translations —
    // TranslateSubscriber wires it automatically at loadClassMetadata.
}
```

**Translation entity** — implements `TranslationInterface` + uses `TranslationTrait`. The class name **must** be the translatable class name suffixed with `Translation`:

```
use ChamberOrchestra\TranslationBundle\Contracts\Entity\TranslationInterface;
use ChamberOrchestra\TranslationBundle\Entity\TranslationTrait;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'post_translation')]
class PostTranslation implements TranslationInterface
{
    use TranslationTrait;

    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private int $id;

    #[ORM\Column]
    public string $title = '';

    // $locale and $translatable are provided by TranslationTrait.
    // The ManyToOne → Post association is mapped automatically.

    public function __construct(Post $post, string $locale, string $title)
    {
        $this->translatable = $post;
        $this->locale = $locale;
        $this->title = $title;
    }

    public function getId(): int { return $this->id; }
}
```

**Reading translations:**

```
// Current request locale (injected by TranslateSubscriber on postLoad):
$post->translate()->title;

// Explicit locale:
$post->translate('ru')->title;

// Fallback chain: fr_CA → fr → kernel default locale:
$post->translate('fr_CA')->title;
```

**Template shorthand** with `TranslatableProxyTrait` — delegates `$post->title` to `$post->translate()->title`:

```
use ChamberOrchestra\TranslationBundle\Entity\TranslatableProxyTrait;

class Post implements TranslatableInterface
{
    use TranslatableTrait;
    use TranslatableProxyTrait; // enables $post->title in Twig

    // ...
}
```

```
{# Both are equivalent after using TranslatableProxyTrait: #}
{{ post.translate().title }}
{{ post.title }}
```

**What `TranslateSubscriber` does automatically:**

TriggerAction`loadClassMetadata` on `Post`Maps `oneToMany` `translations` collection indexed by `locale`, cascade persist/remove`loadClassMetadata` on `PostTranslation`Maps `manyToOne` `translatable` with `CASCADE DELETE`; adds unique constraint `(translatable_id, locale)``postLoad`Injects `currentLocale` and `defaultLocale` from `RequestStack` / kernel default`prePersist`Injects `currentLocale` and `defaultLocale` on new entities---

### System 2: Form Field Localization

[](#system-2-form-field-localization)

Add `localization: true` to any `TextType`, `TextareaType`, or `WysiwygType` field. The entity stores an opaque key; the form shows the human-readable value.

```
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

class ServiceType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class, [
                'localization' => true,
                'localization_domain' => 'entity',   // default: 'entity'
                'localization_context' => 'service', // optional
            ])
            ->add('description', TextareaType::class, [
                'localization' => true,
            ]);
    }
}
```

#### How it works

[](#how-it-works)

**On render (PRE\_SET\_DATA):**

- If the entity field is `null` (new record) — a new `entity@{uuid}` key is generated but not yet stored; if the user submits empty, the field stays `null` and nothing is persisted.
- If the entity field already has a key — `LocalizationLoaderChain` resolves it to a human-readable value for display.

**On submit (PRE\_SUBMIT):**

- `TranslatableTypeExtension` dispatches `TranslationEvent(key, value, locale, context)`.
- The built-in `TranslationEventSubscriber` creates or updates the `Translation` entity in the database and marks it as needing export (`exported = false`).
- The entity field stores the opaque key, not the text.

#### Reading translations: two paths

[](#reading-translations-two-paths)

ConsumerMechanismSourceCMS form render`LocalizationLoaderChain`Configurable — see loaders belowPublic site (`|trans` Twig filter)Symfony `Translator`YAML catalog built by `cache:warmup`The public site uses the standard Twig `trans` filter:

```
{{ entity.name|trans([], 'entity') }}
```

This reads from the Symfony translation catalog (compiled from YAML), **not** from the database. A `translation:export` + `cache:warmup` cycle is required for changes to appear on the public site.

#### Exporting to YAML

[](#exporting-to-yaml)

```
php bin/console translation:export
```

Reads all `Translation` records with `exported = false`, writes them to:

```
{translations_path}/{domain}+intl-icu.{locale}.yaml

```

marks them as exported, and dispatches `TranslationExportedEvent`.

**Recommended deploy integration** — run export before `cache:warmup` so new values are compiled into the catalog immediately:

```
# In your deploy script, after stopping workers:
php bin/console translation:export
php bin/console cache:clear --no-warmup
php bin/console cache:warmup
# Start workers — they boot with the fresh catalog.
```

#### TranslationExportedEvent

[](#translationexportedevent)

Dispatched after a successful export. Handle it in your application to perform any post-export actions (e.g., notifying external systems):

```
use ChamberOrchestra\TranslationBundle\Events\TranslationExportedEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener(TranslationExportedEvent::class)]
class MyExportListener
{
    public function __invoke(TranslationExportedEvent $event): void
    {
        // e.g. notify a CDN, trigger a webhook, etc.
    }
}
```

#### Translation key format

[](#translation-key-format)

```
{domain}@{uuid}

```

```
use ChamberOrchestra\TranslationBundle\Utils\TranslationHelper;
use Symfony\Component\Uid\Uuid;

$key = TranslationHelper::getLocalizationKey('entity', Uuid::v7());
// → "entity@{uuid}"

TranslationHelper::getDomain($key);  // "entity"
TranslationHelper::getMessage($key); // "{uuid}"
TranslationHelper::parseId($key);    // "{uuid}" string
```

---

### CMS Integration (optional)

[](#cms-integration-optional)

Requires `chamber-orchestra/cms-bundle`. Renders per-locale tabs in CMS edit forms:

```
use ChamberOrchestra\TranslationBundle\Cms\Form\Type\TranslatableTypeTrait;

class PostType extends AbstractType
{
    use TranslatableTypeTrait; // adds $builder->add('translations', TranslationsType::class, ...)

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $this->addTranslationsField($builder, PostTranslationType::class);
    }
}
```

Configure available locales in `config/services.yaml`:

```
parameters:
    chamber_orchestra.translation_locales: [ru, en, de]
```

---

### Custom Localization Loaders

[](#custom-localization-loaders)

Implement `LocalizationLoaderInterface` and tag the service with `localization.loader`. The `LocalizationLoaderChain` tries loaders in descending priority order, returning the first non-`null` result.

**Default loader** (`DefaultLocalizationLoader`, priority 0) reads from the Symfony translator (YAML catalog). This means the CMS form shows the last exported value, not the latest saved value.

**Recommended: add a DB loader** (priority &gt; 0) so CMS forms always show the current database value without requiring an export:

```
use ChamberOrchestra\TranslationBundle\Contracts\Provider\LocaleProviderInterface;
use ChamberOrchestra\TranslationBundle\Form\Loader\LocalizationLoaderInterface;
use ChamberOrchestra\TranslationBundle\Repository\TranslationRepository;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(tags: ['localization.loader'])]
#[AsTaggedItem(priority: 10)]
class DbLocalizationLoader implements LocalizationLoaderInterface
{
    public function __construct(
        private readonly TranslationRepository $repository,
        private readonly LocaleProviderInterface $localeProvider,
    ) {}

    public function load(string $key): ?string
    {
        $locale = $this->localeProvider->provideCurrentLocale()
            ?? $this->localeProvider->provideFallbackLocale()
            ?? 'en';

        return $this->repository->findOneByKey($key, $locale)?->getValue();
        // Returns null if not found → chain falls through to DefaultLocalizationLoader
    }
}
```

With this loader the CMS form reflects the saved value immediately after submit, while the public site only updates after export + `cache:warmup`.

---

Testing
-------

[](#testing)

Integration tests require a PostgreSQL database. Set `DATABASE_URL` or use the default from `phpunit.xml.dist`:

```
composer install
DATABASE_URL="postgresql://user:pass@127.0.0.1:5432/mydb?serverVersion=17&charset=utf8" \
    ./vendor/bin/phpunit
```

Run only unit tests (no database required):

```
./vendor/bin/phpunit --testsuite Unit
```

License
-------

[](#license)

MIT

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance81

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~34 days

Total

5

Last Release

35d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/44037eb1c8dc2c4fa9871ac213653f33e22a9348dcec7132df07cc71933f2a2e?d=identicon)[wtorsi](/maintainers/wtorsi)

---

Top Contributors

[![wtorsi](https://avatars.githubusercontent.com/u/2115840?v=4)](https://github.com/wtorsi "wtorsi (1 commits)")

---

Tags

symfonyintllocalizationi18nl10ntranslationormdoctrinemultilingualtranslatableSymfony Bundlexliff

###  Code Quality

TestsPHPUnit

Code StylePHP CS Fixer

### Embed Badge

![Health badge](/badges/chamber-orchestra-translation-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/chamber-orchestra-translation-bundle/health.svg)](https://phpackages.com/packages/chamber-orchestra-translation-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k18.3M416](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k6.0M770](/packages/sylius-sylius)[chameleon-system/chameleon-base

The Chameleon System core.

1029.4k6](/packages/chameleon-system-chameleon-base)[contao/core-bundle

Contao Open Source CMS

1301.7M3.0k](/packages/contao-core-bundle)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M534](/packages/pimcore-pimcore)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M229](/packages/sulu-sulu)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
