{"id":18773123,"url":"https://github.com/webfactory/webfactorypolyglotbundle","last_synced_at":"2025-12-13T03:49:04.632Z","repository":{"id":20559031,"uuid":"23839046","full_name":"webfactory/WebfactoryPolyglotBundle","owner":"webfactory","description":"Symfony bundle simplifying translations in Doctrine.","archived":false,"fork":false,"pushed_at":"2025-02-24T17:45:36.000Z","size":239,"stargazers_count":3,"open_issues_count":7,"forks_count":3,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-13T09:07:16.754Z","etag":null,"topics":["bundle","doctrine","i18n","l10n","php","polyglot","symfony","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/webfactory.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-09-09T15:24:09.000Z","updated_at":"2025-02-24T17:45:05.000Z","dependencies_parsed_at":"2024-01-08T09:42:26.124Z","dependency_job_id":"7ffdfe8c-dec0-4691-854e-a4f5ebe2efdb","html_url":"https://github.com/webfactory/WebfactoryPolyglotBundle","commit_stats":{"total_commits":152,"total_committers":10,"mean_commits":15.2,"dds":0.5855263157894737,"last_synced_commit":"6127dc45ec7ef1bef7a4ebde7194c00d41b83e02"},"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webfactory%2FWebfactoryPolyglotBundle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webfactory%2FWebfactoryPolyglotBundle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webfactory%2FWebfactoryPolyglotBundle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webfactory%2FWebfactoryPolyglotBundle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webfactory","download_url":"https://codeload.github.com/webfactory/WebfactoryPolyglotBundle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248688567,"owners_count":21145766,"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","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":["bundle","doctrine","i18n","l10n","php","polyglot","symfony","translations"],"created_at":"2024-11-07T19:32:54.439Z","updated_at":"2025-12-13T03:49:04.603Z","avatar_url":"https://github.com/webfactory.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ![webfactory Logo](https://www.webfactory.de/bundles/webfactorytwiglayout/img/logo.png) WebfactoryPolyglotBundle\n\n![Tests](https://github.com/webfactory/WebfactoryPolyglotBundle/workflows/Tests/badge.svg)\n![Dependencies](https://github.com/webfactory/WebfactoryPolyglotBundle/workflows/Dependencies/badge.svg)\n\nA bundle to simplify translations for Doctrine entities.\n\nIts main advantages over similar bundles are:\n\n* Transparency: Add translations to existing entities without any API changes.\n* Fast: Entity translations are loaded eagerly from separate translation tables.\n* Polyglot: Easy access to all available translations of an entity without additional database requests.\n\n[We](https://www.webfactory.de/) use it to create multilingual navigation menus and links like \"view this article in\nGerman\", where the linked URL has a locale specific slug.\n\n## Installation\n\nJust like any other Symfony bundle, and no additional configuration is required (wheeho!).\n\n## Underlying Data Model and How It Works\n\nThe assumption is that you already have a \"main\" Doctrine entity class with some fields that you now need to make locale-specific.\n\nTo do so, we'll add a new _translation entity class_ that contains these fields plus a field for the locale. The main entity and the translation entity class will be connected with a `OneToMany` association. \n \nSo, for one single _main_ entity instance, there are zero to many _translation entity_ instances – one for every locale that you have a translation for.\n \nThis approach reflects our experience that almost always the relevant content (field values) are maintained for a \"primary\" locale. This is the \"authoritative\" version of your content/data. Then, this content is translated for one or several \"secondary\" locales. The _translation entity class_ is concerned with holding this translated data only.\n\nTechnically, this bundle sets up a Doctrine event handler (`\\Webfactory\\Bundle\\PolyglotBundle\\Doctrine\\PolyglotListener`) to\nbe notified whenever a Doctrine entity is hydrated, that is, re-created as a PHP object based on database values.\n  \nThis listener finds all fields in the entity that are marked as being locale-specific and replaces their value with\na value holder object. These value holders are instances of `\\Webfactory\\Bundle\\PolyglotBundle\\TranslatableInterface`. \nTo learn more about the Value Holder pattern, see [Lazy Load in PoEAA](https://martinfowler.com/eaaCatalog/lazyLoad.html).\n \nYou can then use this interface's `translate()` method to obtain the field value for a locale of your choice:\nThe value holder will take care of returning either the original value present in your _main_ entity or finding the \nright _translation_ entity instance (for the matching locale) and take the field value \nfrom there, depending on whether you requested the _primary_ or one of the _additional_ locales. If no matching translation is found, the primary\nlocale's data will be used.\n\nWhile this approach should work for any type of data, including objects, in your locale-dependent fields, it works particularly \nwell for strings: The value holder features a `__toString()` method that will return the value for the currently\nactive locale whenever the value holder object is used in a string context.\n \nYet, it is worth noting that you're now dealing with the value holders in places where you previously had \"your\"\ndata or objects. They are *not* \"almost\" transparent proxies as those used by Doctrine because they do not\nprovide the same interface as the original values. Only for strings, the difference is sufficiently small.\n\nThe good news for Twig users is that `__toString()` support in Twig is good enough so that you need\n not care about the distinction of strings and translation value holders. So, Twig constructs like `{{ someObject.field }}` or\n `{% if someObject.field is not empty %}...` will work the same regardless of your `getField()` method returns a string \n value or the translation value holder.\n  \nYou think an example could help clearing up the confusion? Read on!\n  \n## Usage Example\n\nLet's say you have an existing Doctrine entity `Document` that looks like this:\n\n```php\n\u003c?php\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\n\n#[ORM\\Table]\n#[ORM\\Entity]\nclass Document\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column]\n    private int $id;\n\n    #[ORM\\Column]\n    private string $text;\n\n    public function getText(): string\n    {\n        return $this-\u003etext;\n    }\n}\n```\n\nNow, we want to make the `text` field translatable.\n\n### Step 1) Update the Main Entity\n\n1. For the main entity class, add the `Webfactory\\Bundle\\PolyglotBundle\\Attribute\\Locale` attribute to indicate your\n   primary locale. That's the locale you have used for your fields so far.\n2. Add the `Webfactory\\Bundle\\PolyglotBundle\\Attribute\\Translatable` attribute to all translatable fields. \n3. Add the collection to hold translation instances (more about that in the next section), \n   and add the `Webfactory\\Bundle\\PolyglotBundle\\Attribute\\TranslationCollection` attribute to its field. Also make sure it \n   is initialized with an empty Doctrine collection.\n4. Change the type hints for the translated fields in the main entity class from `string` to `TranslatableInterface`,\n   and use the special `translatable_string` Doctrine column type for it.\n\nThe `translatable_string` column type behaves like the built-in `string` type, but allows for type hinting with \n`TranslatableInterface`. If you want it to behave like the `text` type instead, add the `use_text_column` option\nlike so: `#[ORM\\Column(type: \"translatable_string\", options: [\"use_text_column\" =\u003e true])]`.\n\nThis will lead you to something like the following, with some code skipped for brevity:\n\n```php \n\u003c?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\Common\\Collections\\Collection;\nuse Doctrine\\Common\\Collections\\ArrayCollection;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Webfactory\\Bundle\\PolyglotBundle\\Attribute as Polyglot;\nuse Webfactory\\Bundle\\PolyglotBundle\\TranslatableInterface;\n\n#[Polyglot\\Locale(primary: \"en_GB\")]\nclass Document\n{\n    #[Polyglot\\TranslationCollection]\n    #[ORM\\OneToMany(targetEntity: \\DocumentTranslation::class, mappedBy: 'entity')]\n    private Collection $translations;\n\n    /**\n     * @var TranslatableInterface\u003cstring\u003e\n     */\n    #[Polyglot\\Translatable]\n    #[ORM\\Column(type: 'translatable_string')]\n    private TranslatableInterface $text;\n\n    public function __construct(...)\n    {\n        // ...\n        $this-\u003etranslations = new ArrayCollection();\n    }\n\n    public function getText(): string\n    {\n        return $this-\u003etext-\u003etranslate();\n    }\n}\n```\n\n### Step 2) Create the Translation Entity\n\n1. Create a class for the translation entity. As for the name, we suggest suffixing your main entity's name with\n   `Translation`. It has to contain fields for all the fields in your main entity that are to be translated. Declare\n   these fields as regular Doctrine ORM column, using plain column types like `text` (e.g. `#[ORM\\Column(type: \"text\")]`).\n   You may want to extend `\\Webfactory\\Bundle\\PolyglotBundle\\Entity\\BaseTranslation` to save yourself some boilerplate\n   code, but extending this class is not necessary.\n2. To implement the one-to-many relationship, the translation entity needs to reference to the original entity. \n   In the following example, this is the `$entity` field.\n\nYour code should look similar to this:\n\n```php\n\u003c?php\n\nnamespace App\\Entity;\n    \nuse Doctrine\\ORM\\Mapping as ORM;\nuse Webfactory\\Bundle\\PolyglotBundle\\Entity\\BaseTranslation;\nuse Webfactory\\Bundle\\PolyglotBundle\\Attribute as Polyglot;\nuse Webfactory\\Bundle\\PolyglotBundle\\TranslatableInterface;\n\n#[ORM\\Table]\n#[ORM\\UniqueConstraint(columns: ['entity_id', 'locale'])]\n#[ORM\\Entity]\nclass DocumentTranslation\n{\n    #[ORM\\Id]\n    #[ORM\\GeneratedValue]\n    #[ORM\\Column]\n    private int $id;\n\n    #[ORM\\Column]\n    #[Polyglot\\Locale]\n    private string $locale;\n\n    #[ORM\\ManyToOne(targetEntity: Document::class, inversedBy: 'translations')]\n    private Document $entity;\n\n    public function getLocale(): string\n    {\n        return $this-\u003elocale;\n    }\n\n    #[ORM\\Column]\n    private string $text;\n}\n```\n\n### Step 3) Update your database schema\n\nUse Doctrine to update your database schema, e.g. via the\n[DoctrineMigrationsBundle](https://github.com/Doctrine/DoctrineMigrationsBundle).\n\n### That's it!\n\nYour entities will now be automatically loaded in the language corresponding to the current request's locale. If there\nis no translation for the current locale, the primary locale is used as a fallback.\n\nYou probably noticed that we changed the `getText()` getter in our `Document` class above to call the `translate()` \nmethod. This uses the _value holder_ to obtain the underlying value, effectively returning the same type of data\nas before the change. Clients calling this method will not notice a difference, but can only obtain the value for the\ncurrently active locale.\n\nOf course, you could also change the method to something along the lines of\n\n```php\n\u003c?php \n...\nclass Document \n{ \n    ... \n    public function getText(string $locale = null): string\n    {\n        return $this-\u003etext-\u003etranslate($locale);    \n    }\n}\n```\n\n... which should be backwards-compatible as well, but allows client code to access the values for locales of their \nchoice.\n\nYour last option would be to leave the getter unchanged, return the value holder object and have your client code \ndeal with it. This is not a 100% backwards-compatible solution, but as stated above, chances are you might get away with\nit when changing string-typed fields only. The benefit of this approach would be that you can still choose the\nlocale further down the line.\n\n**Caveat:** Note some subtle changes in case you're trying this approach:\n```php\n    $myDocument = ...;\n    $text = $myDocument-\u003egetText();\n    \n    if ($text) { ... }  // Never holds because the value holder is returned (even if it contains a \"\" translation value)\n    if ($text === 'someValue') { ... } // Strict type check prevents calling the __toString() method\n```\n\n## Translations for Doctrine column types other than `string`\n\nIn fact, the fields that are used to hold translated values need not use the `translatable` Doctrine column type declaration\nand can be of other type than `string`. \n\nIn this case, you need to use a union type for your field type declaration as in the following example.\n\n```php\n\u003c?php\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Webfactory\\Bundle\\PolyglotBundle\\Attribute as Polyglot;\nuse Webfactory\\Bundle\\PolyglotBundle\\TranslatableInterface;\n\n// ...\nclass Document\n{\n    // ... fields and collections omitted for brevity\n    #[ORM\\Column(type: '...yourtype')]\n    #[Polyglot\\Translatable]\n    private TranslatableInterface|\u003cother type\u003e $text;\n\n    // ...\n}\n```\n\nWith this declaration, the field can serve a dual use: Right before the ORM flushes data, the field value will be switched to the \nvalue of the \"primary\" locale, so that the ORM can persist that data as usual. Similarly, after the ORM has loaded the entity,\nit will replace the field value with the translation value holder (an instance of `TranslatableInterface`) that you can use\nto obtain the translated values as well.\n\nNote that it is not necessary to do this in the translations class (`DocumentTranslation` in the above examples), since that \nclass represents the values of a single locale only and never contains `TranslatableInterface` instances.\n\n## Credits, Copyright and License\n\nThis Bundle was written by webfactory GmbH, Bonn, Germany. We're a software development agency with a focus on PHP (mostly [Symfony](http://github.com/symfony/symfony)). If you're a developer looking for new challenges, we'd like to hear from you!\n\n- \u003chttps://www.webfactory.de\u003e\n\nCopyright 2012-2025 webfactory GmbH, Bonn. Code released under [the MIT license](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebfactory%2Fwebfactorypolyglotbundle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebfactory%2Fwebfactorypolyglotbundle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebfactory%2Fwebfactorypolyglotbundle/lists"}