{"id":15028760,"url":"https://github.com/mnapoli/doctrinetranslated","last_synced_at":"2025-10-10T00:34:29.589Z","repository":{"id":14460856,"uuid":"17172699","full_name":"mnapoli/DoctrineTranslated","owner":"mnapoli","description":"Translated strings for Doctrine","archived":false,"fork":false,"pushed_at":"2014-07-03T19:42:04.000Z","size":452,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-09-14T19:28:31.518Z","etag":null,"topics":["doctrine","php","translations"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mnapoli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-02-25T12:47:00.000Z","updated_at":"2023-09-13T09:52:14.000Z","dependencies_parsed_at":"2022-09-14T12:40:18.779Z","dependency_job_id":null,"html_url":"https://github.com/mnapoli/DoctrineTranslated","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mnapoli/DoctrineTranslated","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnapoli%2FDoctrineTranslated","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnapoli%2FDoctrineTranslated/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnapoli%2FDoctrineTranslated/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnapoli%2FDoctrineTranslated/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mnapoli","download_url":"https://codeload.github.com/mnapoli/DoctrineTranslated/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mnapoli%2FDoctrineTranslated/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279002359,"owners_count":26083357,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["doctrine","php","translations"],"created_at":"2024-09-24T20:09:02.306Z","updated_at":"2025-10-10T00:34:29.553Z","avatar_url":"https://github.com/mnapoli.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Translated strings for Doctrine\n\n[![Build Status](https://travis-ci.org/mnapoli/DoctrineTranslated.svg?branch=master)](https://travis-ci.org/mnapoli/DoctrineTranslated)\n[![Coverage Status](https://coveralls.io/repos/mnapoli/DoctrineTranslated/badge.png)](https://coveralls.io/r/mnapoli/DoctrineTranslated)\n[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mnapoli/DoctrineTranslated/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mnapoli/DoctrineTranslated/?branch=master)\n[![Total Downloads](https://poser.pugx.org/mnapoli/doctrine-translated/downloads.svg)](https://packagist.org/packages/mnapoli/doctrine-translated)\n\nThis library is an alternative to the Translatable extension for Doctrine.\n\nThe basic idea is to shift from \"something that magically manages several versions of the same entity\"\nto \"my entity's field is an object that contains several translations\".\n\nIt aims to be extremely simple and explicit on its behavior, so that it can be\nreliable, maintainable, easily extended and understood. The goal is to do more with less.\n\n## Requirements\n\nThis library requires **PHP 5.4** and **Doctrine 2.5**!\n\n## How it works\n\nThe library relies on a new major feature of Doctrine 2.5: embedded objects.\nAn embedded object will see its properties inserted in the entity that uses it.\n\nExample:\n\n```php\nnamespace Acme\\Model;\n\n/**\n * @Entity\n */\nclass Product\n{\n    /**\n     * @var TranslatedString\n     * @Embedded(class = \"Acme\\Model\\TranslatedString\")\n     */\n    protected $name;\n\n    public function __construct()\n    {\n        $this-\u003ename = new TranslatedString();\n    }\n\n    public function getName()\n    {\n        return $this-\u003ename;\n    }\n}\n```\n\nIf you use YAML instead of annotations:\n\n```yaml\nAcme\\Model\\Product:\n  type: entity\n\n  embedded:\n    name:\n      class: Acme\\Model\\TranslatedString\n```\n\nThe `TranslatedString` is defined by you by extending `Mnapoli\\Translated\\AbstractTranslatedString`.\nThat way, you can define the languages you want to support.\nThis class is reusable everywhere in your application, so you only need to define it once.\n\n```php\nnamespace Acme\\Model;\n\n/**\n * @Embeddable\n */\nclass TranslatedString extends \\Mnapoli\\Translated\\AbstractTranslatedString\n{\n    /**\n     * @Column(type = \"string\", nullable=true)\n     */\n    public $en;\n\n    /**\n     * @Column(type = \"string\", nullable=true)\n     */\n    public $fr;\n}\n```\n\nAs you can see, the properties must be public.\n\nHere is the same mapping in YAML:\n\n```yaml\nAcme\\Model\\TranslatedString:\n  type: embeddable\n  fields:\n    en:\n      type: string\n      nullable: true\n    fr:\n      type: string\n      nullable: true\n```\n\nYou can then start translating that field:\n\n```php\n$product = new Product();\n\n$product-\u003egetName()-\u003een = 'Some english here';\n$product-\u003egetName()-\u003efr = 'Un peu de français là';\n\necho $product-\u003egetName()-\u003een;\n```\n\nUsually in your application, you will not want to hardcode \"en\" or \"fr\" when reading or setting the value.\nThis is because the current locale varies from request to request.\n\nThat is why this library provides helpers to make it much easier, along with the `Translator` object.\n\nExample:\n\n```php\n// The default locale is \"en\" (you can provide a locale like \"en_US\" too, it will be parsed)\n$translator = \\Mnapoli\\Translated\\Translator('en');\n\n// If a user is logged in, we can set the locale to the user's one\n$translator-\u003esetLanguage('fr');\n\n$str = new TranslatedString();\n$str-\u003een = 'foo';\n$str-\u003efr = 'bar';\n\n// No need to manipulate the locale here\necho $translator-\u003eget($str); // foo\n```\n\nCurrent integrations:\n\n- Twig\n\n```twig\n{{ product.name|translate }}\n```\n\nThe configuration step is very straightforward:\n\n```php\n$extension = new \\Mnapoli\\Translated\\Integration\\Twig\\TranslatedTwigExtension($translator);\n$twig-\u003eaddExtension($extension);\n```\n\n- Symfony 2\n\nThe `Mnapoli\\Translated\\Integration\\Symfony2\\TranslatedBundle` is provided.\nYou need to register the bundle in `AppKernel.php`:\n\n```php\nclass AppKernel extends Kernel\n{\n    public function registerBundles()\n    {\n        $bundles = [\n            // ...\n            new \\Mnapoli\\Translated\\Integration\\Symfony2\\TranslatedBundle(),\n        ];\n\n        // ...\n```\n\nThen in your `app/config/config.yml`:\n\n```yaml\ntranslated:\n    default_locale: %locale%\n```\n\nThe TranslatedBundle will automatically listen to the request's locale and configure the `Translator` accordingly.\n\nThat means you have nothing to do: just use the Translator, and it will use the request's locale to translate things.\n\nIf the current locale is not stored inside the request, you will need to set up an event listener manually.\nHere is an basic example using the session:\n\n```php\nclass LocaleListener\n{\n    private $translator;\n    private $session;\n\n    public function __construct(Translator $translator)\n    {\n        $this-\u003etranslator = $translator;\n    }\n\n    public function setSession(Session $session)\n    {\n        $this-\u003esession = $session;\n    }\n\n    public function onRequest(GetResponseEvent $event)\n    {\n        if (HttpKernelInterface::MASTER_REQUEST !== $event-\u003egetRequestType()) {\n            return;\n        }\n\n        $locale = $request-\u003egetSession()-\u003eget('_locale');\n        if ($locale) {\n            $this-\u003etranslator-\u003esetLanguage($locale);\n        }\n    }\n\n    public function onLogin(InteractiveLoginEvent $event)\n    {\n        $user = $event-\u003egetAuthenticationToken()-\u003egetUser();\n        $lang = $user-\u003egetLanguage();\n\n        if ($lang) {\n            $this-\u003esession-\u003eset('_locale', $lang);\n        }\n    }\n}\n```\n\nWhen the user logs in, his/her locale is stored inside the session. Here is the configuration:\n\n```yaml\nservices:\n    acme.locale.interactive_login_listener:\n        class: Acme\\UserBundle\\EventListener\\LocaleListener\n        calls:\n            - [ setSession, [@session] ]\n        tags:\n            - { name: kernel.event_listener, event: security.interactive_login, method: onLogin }\n\n    acme.locale.kernel_request_listener:\n        class: Acme\\UserBundle\\EventListener\\LocaleListener\n        calls:\n            - [ setSession, [@session] ]\n        tags:\n            - { name: kernel.event_listener, event: kernel.request, method: onRequest }\n```\n\n- Zend Framework 1\n\n```php\n    // In your Bootstrap\n    protected function _initViewHelpers()\n    {\n        $this-\u003ebootstrap('View');\n\n        // Create or get $translator (\\Mnapoli\\Translated\\Translator)\n\n        // Create the helper\n        $helper = new Mnapoli\\Translated\\Integration\\Zend1\\TranslateZend1Helper($translator);\n\n        // The view helper will be accessible through the name \"translate\"\n        $this-\u003egetResource('view')-\u003eregisterHelper($helper, 'translate');\n    }\n```\n\nYou can then use the helper in views:\n\n```php\necho $this-\u003etranslate($someTranslatedString);\n```\n\nWatch out: the `translate` view helper already exists in ZF1. The example shown here will override it.\nYou can use a name different than \"translate\" if you don't want to override it.\n\n\n## Pros and cons\n\nWith that method, you will end up with only one table in database:\n\n```\nmysql\u003e SELECT * FROM Product;\n+----+---------+---------+\n| id | name_en | name_fr |\n+----+---------+---------+\n| 1  | Hello   | Salut   |\n+----+---------+---------+\n```\n\nThis makes it very good for performances, and for other reasons:\n\n- no round-trip to the database because you always get all the translations\n- no joins, this is a perfectly simple query\n- isolated translations (there isn't a single table for storing all the translations)\n- no problems with indexes (you can add the indexes you want)\n- very friendly with manually browsing/editing the database\n\nHowever, be aware there are cons:\n\n- if you support 100 languages, you will end up with huge tables and large objects in memory\n- if you add a new language, you need to update your database (Doctrine can do it automatically though)\n\n\n## Translator\n\nYou saw above a basic example of using the translator.\n\nHere is all you can do with it:\n\n```php\n// Get the translation for the current locale\necho $translator-\u003eget($str);\n\n// Set the translation for the current locale\n$translator-\u003eset($str, 'Hello');\n\n// Set the translation for several locales\n$translator-\u003esetMany($str, [\n    'en' =\u003e 'Hello',\n    'fr' =\u003e 'Salut',\n]);\n```\n\nTo create a new translation from scratch:\n\n```php\n$str = $translator-\u003eset(new TranslatedString(), 'Hello');\n\n// Same as:\n$str = new TranslatedString();\n$translator-\u003eset($str, 'Hello');\n```\n\n\n## Operations\n\nSometimes you need to concatenate strings in a model, so you can't use the translator\n(and you maybe don't want to).\n\nYou can do some basic operations on the translated strings.\n\n### Concatenation\n\n```php\n$str1 = new TranslatedString();\n$str1-\u003een = 'Hello';\n$str1-\u003efr = 'Bonjour';\n\n// $result is a TranslatedString\n$result = $str1-\u003econcat(' ', $user-\u003egetName());\n\n// Will echo \"Hello John\" or \"Bonjour John\" according to the locale\necho $translator-\u003eget($result);\n```\n\nYou can also create a string concatenation from scratch:\n\n```php\n$result = TranslatedString::join([\n    new TranslatedString('Hello', 'en'),\n    '!'\n]);\n```\n\n### Implode\n\nJust like the concatenation:\n\n```php\n$result = TranslatedString::implode(', ', [\n    new TranslatedString('foo', 'en'),\n    'bar'\n]);\n\n// \"foo, bar\"\necho $result-\u003een;\n```\n\n\n## Untranslated strings\n\nSometimes you should give or return a `TranslatedString` but you have a non-translated string.\nFor example:\n\n```php\npublic function getParentLabel() {\n    if ($this-\u003eparent === null) {\n        return '-';\n    }\n\n    return $this-\u003eparent-\u003egetLabel();\n}\n```\n\nHere there is a problem: `'-'` is a simple string, and if the calling code expects a `TranslatedString`\nthen it won't work.\n\nFor this, you can simply create an \"untranslated\" string:\n\n```php\nreturn TranslatedString::untranslated('-');\n```\n\nIt will have the same value (or translation) for every language.\n\n\n## Fallbacks\n\nYou can define fallbacks on the `Translator`:\n\n```php\n$translator = new Translator('en', [\n    'fr' =\u003e ['en'],       // french fallbacks to english if not found\n    'es' =\u003e ['fr', 'en'], // spanish fallbacks to french, then english if not found\n]);\n```\n\nAs you can see, fallbacks are optional, and can be multiple.\n\nNow the translator will use those fallbacks:\n\n```php\n$str = new TranslatedString();\n$str-\u003een = 'Hello!';\n\n// Will show nothing (no FR value)\necho $str-\u003efr;\n\n$translator-\u003esetLanguage('fr');\n// Will show \"Hello!\" because the french falls back to english if not defined\necho $translator-\u003eget($str);\n```\n\nYou will note that you can also directly use fallbacks on the TranslatedString object:\n\n```php\n// Nothing\necho $str-\u003efr;\n// Nothing\necho $str-\u003eget('fr');\n// Will show \"Hello!\" (the fallback is \"en\")\necho $str-\u003eget('fr', ['en']);\n```\n\n\n## Doctrine\n\n**Documentation currently being written**\n\nThere are no changes regarding persisting or retrieving an entity. When you load an entity from\ndatabase, all the translations will be loaded.\n\nHowever, due to the fact that `Product::name` is not a string anymore, you cannot simply filter on\nthe field. You need to write queries like this:\n\n```php\n$query = $em-\u003ecreateQuery(sprintf(\n    \"SELECT p FROM Product p WHERE p.name.%s = 'Hello'\",\n    $lang\n));\n$products = $query-\u003egetResult();\n```\n\nThe same goes for `ORDER BY`:\n\n```php\n$query = $em-\u003ecreateQuery(sprintf(\n    \"SELECT p FROM Product p ORDER BY p.name.%s ASC\",\n    $lang\n));\n$products = $query-\u003egetResult();\n```\n\nThe `$lang` (current locale) can be obtained from the `Translator`.\n\nI am looking at ways to makes this more simple, for example with a DQL function\n(https://github.com/mnapoli/DoctrineTranslated/blob/master/src/Doctrine/TranslatedFunction.php).\nFeel free to help, currently this is stuck because Doctrine instantiate the \"function\" classes itself,\nwhich prevents using dependency injection to inject the current locale (or translator).\n\nCurrent issue opened at Doctrine: [#991](https://github.com/doctrine/doctrine2/pull/991).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnapoli%2Fdoctrinetranslated","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmnapoli%2Fdoctrinetranslated","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmnapoli%2Fdoctrinetranslated/lists"}