PHPackages                             mnapoli/doctrine-translated - 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. mnapoli/doctrine-translated

ActiveLibrary[Database &amp; ORM](/categories/database)

mnapoli/doctrine-translated
===========================

Translated strings for Doctrine

0.2.0(11y ago)11223MITPHPPHP &gt;=5.4.0

Since May 15Pushed 11y ago3 watchersCompare

[ Source](https://github.com/mnapoli/DoctrineTranslated)[ Packagist](https://packagist.org/packages/mnapoli/doctrine-translated)[ Docs](https://github.com/mnapoli/DoctrineTranslated)[ RSS](/packages/mnapoli-doctrine-translated/feed)WikiDiscussions master Synced today

READMEChangelog (2)Dependencies (1)Versions (3)Used By (0)

Translated strings for Doctrine
===============================

[](#translated-strings-for-doctrine)

[![Build Status](https://camo.githubusercontent.com/5a13b0b6b2b09b91dbce19d806bc77f0359f5cd5705d69a2ee494a6b2bb44ec7/68747470733a2f2f7472617669732d63692e6f72672f6d6e61706f6c692f446f637472696e655472616e736c617465642e7376673f6272616e63683d6d6173746572)](https://travis-ci.org/mnapoli/DoctrineTranslated)[![Coverage Status](https://camo.githubusercontent.com/c6fac852978fdb296038f8f204dee056a60bf715b0d6f837cc60e30f535db683/68747470733a2f2f636f766572616c6c732e696f2f7265706f732f6d6e61706f6c692f446f637472696e655472616e736c617465642f62616467652e706e67)](https://coveralls.io/r/mnapoli/DoctrineTranslated)[![Scrutinizer Code Quality](https://camo.githubusercontent.com/cd39eff1befd037ad6c45922ed83990a662ea27610b81722f0cf583db36a40ae/68747470733a2f2f7363727574696e697a65722d63692e636f6d2f672f6d6e61706f6c692f446f637472696e655472616e736c617465642f6261646765732f7175616c6974792d73636f72652e706e673f623d6d6173746572)](https://scrutinizer-ci.com/g/mnapoli/DoctrineTranslated/?branch=master)[![Total Downloads](https://camo.githubusercontent.com/6f2303403400ace94e6996c47b53aa5115c94776652569ee5966609cc9571b3d/68747470733a2f2f706f7365722e707567782e6f72672f6d6e61706f6c692f646f637472696e652d7472616e736c617465642f646f776e6c6f6164732e737667)](https://packagist.org/packages/mnapoli/doctrine-translated)

This library is an alternative to the Translatable extension for Doctrine.

The basic idea is to shift from "something that magically manages several versions of the same entity" to "my entity's field is an object that contains several translations".

It aims to be extremely simple and explicit on its behavior, so that it can be reliable, maintainable, easily extended and understood. The goal is to do more with less.

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

[](#requirements)

This library requires **PHP 5.4** and **Doctrine 2.5**!

How it works
------------

[](#how-it-works)

The library relies on a new major feature of Doctrine 2.5: embedded objects. An embedded object will see its properties inserted in the entity that uses it.

Example:

```
namespace Acme\Model;

/**
 * @Entity
 */
class Product
{
    /**
     * @var TranslatedString
     * @Embedded(class = "Acme\Model\TranslatedString")
     */
    protected $name;

    public function __construct()
    {
        $this->name = new TranslatedString();
    }

    public function getName()
    {
        return $this->name;
    }
}
```

If you use YAML instead of annotations:

```
Acme\Model\Product:
  type: entity

  embedded:
    name:
      class: Acme\Model\TranslatedString
```

The `TranslatedString` is defined by you by extending `Mnapoli\Translated\AbstractTranslatedString`. That way, you can define the languages you want to support. This class is reusable everywhere in your application, so you only need to define it once.

```
namespace Acme\Model;

/**
 * @Embeddable
 */
class TranslatedString extends \Mnapoli\Translated\AbstractTranslatedString
{
    /**
     * @Column(type = "string", nullable=true)
     */
    public $en;

    /**
     * @Column(type = "string", nullable=true)
     */
    public $fr;
}
```

As you can see, the properties must be public.

Here is the same mapping in YAML:

```
Acme\Model\TranslatedString:
  type: embeddable
  fields:
    en:
      type: string
      nullable: true
    fr:
      type: string
      nullable: true
```

You can then start translating that field:

```
$product = new Product();

$product->getName()->en = 'Some english here';
$product->getName()->fr = 'Un peu de français là';

echo $product->getName()->en;
```

Usually in your application, you will not want to hardcode "en" or "fr" when reading or setting the value. This is because the current locale varies from request to request.

That is why this library provides helpers to make it much easier, along with the `Translator` object.

Example:

```
// The default locale is "en" (you can provide a locale like "en_US" too, it will be parsed)
$translator = \Mnapoli\Translated\Translator('en');

// If a user is logged in, we can set the locale to the user's one
$translator->setLanguage('fr');

$str = new TranslatedString();
$str->en = 'foo';
$str->fr = 'bar';

// No need to manipulate the locale here
echo $translator->get($str); // foo
```

Current integrations:

- Twig

```
{{ product.name|translate }}
```

The configuration step is very straightforward:

```
$extension = new \Mnapoli\Translated\Integration\Twig\TranslatedTwigExtension($translator);
$twig->addExtension($extension);
```

- Symfony 2

The `Mnapoli\Translated\Integration\Symfony2\TranslatedBundle` is provided. You need to register the bundle in `AppKernel.php`:

```
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // ...
            new \Mnapoli\Translated\Integration\Symfony2\TranslatedBundle(),
        ];

        // ...
```

Then in your `app/config/config.yml`:

```
translated:
    default_locale: %locale%
```

The TranslatedBundle will automatically listen to the request's locale and configure the `Translator` accordingly.

That means you have nothing to do: just use the Translator, and it will use the request's locale to translate things.

If the current locale is not stored inside the request, you will need to set up an event listener manually. Here is an basic example using the session:

```
class LocaleListener
{
    private $translator;
    private $session;

    public function __construct(Translator $translator)
    {
        $this->translator = $translator;
    }

    public function setSession(Session $session)
    {
        $this->session = $session;
    }

    public function onRequest(GetResponseEvent $event)
    {
        if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
            return;
        }

        $locale = $request->getSession()->get('_locale');
        if ($locale) {
            $this->translator->setLanguage($locale);
        }
    }

    public function onLogin(InteractiveLoginEvent $event)
    {
        $user = $event->getAuthenticationToken()->getUser();
        $lang = $user->getLanguage();

        if ($lang) {
            $this->session->set('_locale', $lang);
        }
    }
}
```

When the user logs in, his/her locale is stored inside the session. Here is the configuration:

```
services:
    acme.locale.interactive_login_listener:
        class: Acme\UserBundle\EventListener\LocaleListener
        calls:
            - [ setSession, [@session] ]
        tags:
            - { name: kernel.event_listener, event: security.interactive_login, method: onLogin }

    acme.locale.kernel_request_listener:
        class: Acme\UserBundle\EventListener\LocaleListener
        calls:
            - [ setSession, [@session] ]
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onRequest }
```

- Zend Framework 1

```
    // In your Bootstrap
    protected function _initViewHelpers()
    {
        $this->bootstrap('View');

        // Create or get $translator (\Mnapoli\Translated\Translator)

        // Create the helper
        $helper = new Mnapoli\Translated\Integration\Zend1\TranslateZend1Helper($translator);

        // The view helper will be accessible through the name "translate"
        $this->getResource('view')->registerHelper($helper, 'translate');
    }
```

You can then use the helper in views:

```
echo $this->translate($someTranslatedString);
```

Watch out: the `translate` view helper already exists in ZF1. The example shown here will override it. You can use a name different than "translate" if you don't want to override it.

Pros and cons
-------------

[](#pros-and-cons)

With that method, you will end up with only one table in database:

```
mysql> SELECT * FROM Product;
+----+---------+---------+
| id | name_en | name_fr |
+----+---------+---------+
| 1  | Hello   | Salut   |
+----+---------+---------+

```

This makes it very good for performances, and for other reasons:

- no round-trip to the database because you always get all the translations
- no joins, this is a perfectly simple query
- isolated translations (there isn't a single table for storing all the translations)
- no problems with indexes (you can add the indexes you want)
- very friendly with manually browsing/editing the database

However, be aware there are cons:

- if you support 100 languages, you will end up with huge tables and large objects in memory
- if you add a new language, you need to update your database (Doctrine can do it automatically though)

Translator
----------

[](#translator)

You saw above a basic example of using the translator.

Here is all you can do with it:

```
// Get the translation for the current locale
echo $translator->get($str);

// Set the translation for the current locale
$translator->set($str, 'Hello');

// Set the translation for several locales
$translator->setMany($str, [
    'en' => 'Hello',
    'fr' => 'Salut',
]);
```

To create a new translation from scratch:

```
$str = $translator->set(new TranslatedString(), 'Hello');

// Same as:
$str = new TranslatedString();
$translator->set($str, 'Hello');
```

Operations
----------

[](#operations)

Sometimes you need to concatenate strings in a model, so you can't use the translator (and you maybe don't want to).

You can do some basic operations on the translated strings.

### Concatenation

[](#concatenation)

```
$str1 = new TranslatedString();
$str1->en = 'Hello';
$str1->fr = 'Bonjour';

// $result is a TranslatedString
$result = $str1->concat(' ', $user->getName());

// Will echo "Hello John" or "Bonjour John" according to the locale
echo $translator->get($result);
```

You can also create a string concatenation from scratch:

```
$result = TranslatedString::join([
    new TranslatedString('Hello', 'en'),
    '!'
]);
```

### Implode

[](#implode)

Just like the concatenation:

```
$result = TranslatedString::implode(', ', [
    new TranslatedString('foo', 'en'),
    'bar'
]);

// "foo, bar"
echo $result->en;
```

Untranslated strings
--------------------

[](#untranslated-strings)

Sometimes you should give or return a `TranslatedString` but you have a non-translated string. For example:

```
public function getParentLabel() {
    if ($this->parent === null) {
        return '-';
    }

    return $this->parent->getLabel();
}
```

Here there is a problem: `'-'` is a simple string, and if the calling code expects a `TranslatedString`then it won't work.

For this, you can simply create an "untranslated" string:

```
return TranslatedString::untranslated('-');
```

It will have the same value (or translation) for every language.

Fallbacks
---------

[](#fallbacks)

You can define fallbacks on the `Translator`:

```
$translator = new Translator('en', [
    'fr' => ['en'],       // french fallbacks to english if not found
    'es' => ['fr', 'en'], // spanish fallbacks to french, then english if not found
]);
```

As you can see, fallbacks are optional, and can be multiple.

Now the translator will use those fallbacks:

```
$str = new TranslatedString();
$str->en = 'Hello!';

// Will show nothing (no FR value)
echo $str->fr;

$translator->setLanguage('fr');
// Will show "Hello!" because the french falls back to english if not defined
echo $translator->get($str);
```

You will note that you can also directly use fallbacks on the TranslatedString object:

```
// Nothing
echo $str->fr;
// Nothing
echo $str->get('fr');
// Will show "Hello!" (the fallback is "en")
echo $str->get('fr', ['en']);
```

Doctrine
--------

[](#doctrine)

**Documentation currently being written**

There are no changes regarding persisting or retrieving an entity. When you load an entity from database, all the translations will be loaded.

However, due to the fact that `Product::name` is not a string anymore, you cannot simply filter on the field. You need to write queries like this:

```
$query = $em->createQuery(sprintf(
    "SELECT p FROM Product p WHERE p.name.%s = 'Hello'",
    $lang
));
$products = $query->getResult();
```

The same goes for `ORDER BY`:

```
$query = $em->createQuery(sprintf(
    "SELECT p FROM Product p ORDER BY p.name.%s ASC",
    $lang
));
$products = $query->getResult();
```

The `$lang` (current locale) can be obtained from the `Translator`.

I am looking at ways to makes this more simple, for example with a DQL function (). Feel free to help, currently this is stuck because Doctrine instantiate the "function" classes itself, which prevents using dependency injection to inject the current locale (or translator).

Current issue opened at Doctrine: [\#991](https://github.com/doctrine/doctrine2/pull/991).

###  Health Score

26

—

LowBetter than 43% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity17

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity49

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 ~6 days

Total

2

Last Release

4371d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/329a6111724074f5388e95dd41a03ccf3c43f4bfe1ecf27c94c9efc6f7823228?d=identicon)[mnapoli](/maintainers/mnapoli)

---

Top Contributors

[![mnapoli](https://avatars.githubusercontent.com/u/720328?v=4)](https://github.com/mnapoli "mnapoli (67 commits)")

---

Tags

doctrinephptranslationsi18ntranslationstranslationdoctrinetranslatable

### Embed Badge

![Health badge](/badges/mnapoli-doctrine-translated/health.svg)

```
[![Health](https://phpackages.com/badges/mnapoli-doctrine-translated/health.svg)](https://phpackages.com/packages/mnapoli-doctrine-translated)
```

###  Alternatives

[knplabs/doctrine-behaviors

Doctrine Behavior Traits

92212.7M64](/packages/knplabs-doctrine-behaviors)[a2lix/translation-form-bundle

Translate your doctrine objects easily with some helpers

3376.9M38](/packages/a2lix-translation-form-bundle)[prezent/doctrine-translatable

Translatable behaviour extension for Doctrine

26754.3k5](/packages/prezent-doctrine-translatable)[prezent/doctrine-translatable-bundle

Integrate the doctrine-translatable extension in Symfony

14698.4k5](/packages/prezent-doctrine-translatable-bundle)[ao/translation-bundle

Doctrine as symfony translation provider + a nice gui for editing translations.

243.7k](/packages/ao-translation-bundle)

PHPackages © 2026

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