fsi / translatable
A library for handling translations
Installs: 6 029
Dependents: 2
Suggesters: 0
Security: 0
Stars: 0
Watchers: 4
Forks: 2
Open Issues: 2
Requires
- php: ^8.1
- ext-intl: *
- beberlei/assert: ^3.2
Requires (Dev)
- ext-pdo_sqlite: *
- codeception/codeception: ^5.1
- codeception/module-asserts: ^3.0
- codeception/module-doctrine: ^3.1
- codeception/module-symfony: *
- doctrine/dbal: ^3.4
- doctrine/doctrine-bundle: ^2.4
- doctrine/lexer: ^1.2|^2.0|^3.0
- doctrine/orm: ^2.9|^3.0
- doctrine/persistence: ^2.0|^3.0
- fsi/files: ^2.0.4
- guzzlehttp/psr7: ^2.0
- monolog/monolog: ^1.25
- nyholm/psr7: ^1.4
- oneup/flysystem-bundle: ^4.4
- php-http/guzzle7-adapter: ^1.0
- php-http/httplug-bundle: ^1.20
- phpstan/phpstan: ^1.10.39
- phpstan/phpstan-beberlei-assert: ^1.0
- phpstan/phpstan-doctrine: ^1.3
- phpstan/phpstan-phpunit: ^1.3
- phpunit/phpunit: ^9.5
- psr/http-client: ^1.0
- squizlabs/php_codesniffer: ^3.7
- symfony/asset: ^4.4.30|^5.4|^6.0
- symfony/config: ^4.4.30|^5.4|^6.0
- symfony/console: ^4.4.30|^5.4|^6.0
- symfony/dependency-injection: ^4.4.30|^5.4|^6.0
- symfony/finder: ^4.4.30|^5.4|^6.0
- symfony/form: ^4.4.30|^5.4|^6.0
- symfony/framework-bundle: ^4.4.30|^5.4|^6.0
- symfony/http-client: ^4.4.30|^5.4|^6.0
- symfony/http-foundation: ^4.4.30|^5.4|^6.0
- symfony/http-kernel: ^4.4.30|^5.4|^6.0
- symfony/mime: ^4.4.30|^5.4|^6.0
- symfony/monolog-bundle: ^3.7
- symfony/property-access: ^5.4|^6.3
- symfony/routing: ^4.4.30|^5.4|^6.0
- symfony/translation: ^4.4.30|^5.4|^6.0
- symfony/twig-bundle: ^4.4.30|^5.4|^6.0
- symfony/validator: ^4.4.30|^5.4|^6.0
- twig/twig: ^3.7
README
This components serves to provide a way of creating translatable objects for multiple
languages. There are two main concepts governing this idea - a translatable and a translation.
A translatable is an object whose data is stored in a collection of translation objects,
each per a separate locale. Each of these has a separate configuration object, where all the
necessary information about the relation between the two is stored. These are TranslatableConfiguration
and TranslationConfiguration
and can be retrieved via ConfigurationResolver
.
The lifecycle of a translatable object is handled by a set of dedicated classes in the Entity
directory
and these are TranslationLoader
, TranslationUpdater
and TranslationCleaner
. They will:
- set the current locale for the translatable object,
- load data from a translation (if one exists),
- create a new translation or update the existing one,
- remove empty translation objects.
However they will not work on their own and need to be hooked up to whatever storage mechanism you are using (ORM, ODM etc.) through a subscriber(s). Currently there is out-of-the-box integration only for Doctrine / Symfony combination.
What can be a translatable field?
When it comes to storage, mapping/configuration for all of the below should be included in the translation entity files, not the translatable.
By default
- scalar values (string, integer, float),
- objects (though their storage will probably need to be handled through some integration),
WebFile
objects fromfsi/files
component will work and be persisted/removed along with the translation entity,
with Doctrine
- embeddables (also nested), although embeddables themselves cannot have translations due to not having an identifier,
- one-to-one relations,
- collection relations,
Example entities
Let us consider an example of a translatable Article
entity with ArticleTranslation
translation:
declare(strict_types=1); namespace Tests\Entity; use DateTimeImmutable; use FSi\Component\Translatable\Integration\Doctrine\ORM\ProxyTrait; class Article { use ProxyTrait; private ?int $id = null; private ?string $locale = null; private ?DateTimeImmutable $publicationDate = null; private ?string $title = null; private ?string $description = null; // getters and setters will probably be required for whatever // mechanism you use for modifying the object, though are not // required by the component itself } declare(strict_types=1); namespace Tests\Entity; class ArticleTranslation { private ?int $id = null; private ?string $locale = null; private ?string $title = null; private ?string $description = null; private ?Article $article; // getters and setters are not required }
As you can see, they are almost a mirror of each other, aside for the $publicationDate
field
in the translatable and $article
field in the translation. That is because $publicationDate
is not translatable field (it is stored directly in the translatable object) and $article
serves
as a way to bind the translation to the translatable. Both fields have the locale
field: the
translatable stores the current locale and the translation has the locale it was created for.
In order for the component to recognize these objects as the translatable-translation duo, you will need to define their configurations. When using Symfony, this can be achieved as simply as this:
fsi_translatable: entities: Tests\Entity\Article: localeField: locale # this can be skipped for a default value of locale fields: [title, description] disabledAutoTranslationsUpdate: false # optional and false by default translation: localeField: locale # also can be skipped class: Tests\FSi\ArticleTranslation relationField: article
IMPORTANT
- Both objects need to have the same fields present. It is not possible to map translatable fields to different fields in the translation entity.
- Translation objects cannot have required constructor arguments.
If you want to create your configuration manually, you will need to provide the ConfigurationResolver
with
a collection of TranslatableConfiguration
objects with the necessary data.
Usage (Doctrine + Symfony)
Unless you load your bundles and configuartion directly in the Kernel class, you will need to load
the Translatable bundle in the config/bundles.php
:
return [ // Doctrine and Symfony bundles FSi\Component\Translatable\Integration\Symfony\TranslatableBundle::class => ['all' => true] ];
and then add a config/packages/fsi_translatable.yaml
file:
fsi_translatable: entities: # configuration for specific entities, see above for an example
You can of course load the configuration manually through PHP, but XML configuration is not supported at the moment.
After that the component will mostly behave automatically. When creating a new translatable object with
any of the translatable fields filled, the current locale (via LocaleProvider
object) will be fetched, a new
instance of translation will be created and then the relevant contents of the translatable object will be copied
into it. On subsequent loading of the translatable object, the data will be loaded back from the stored translation.
Any modifications to the translatable fields in the translatable object will update the existing translation
automatically. Should the locale provided by the LocaleProvider
be different, a new translation will be created.
If for some reason you need to fetch single/all translation objects directly, you can do so via the TranslationProvider
.
If a translatable object is removed, all the translations will be cleared throught Doctrine's entity manager (via TranslationManager
),
so anysubscribers listening on the translation entity's lifecycle events will be fired as well.
IMPORTANT
- If you have translatable collection fields, you need to initialize them in the translation object's constructor.
Locale fetching and persisting
The LocaleProvider
implementation for Symfony will try to fetch the locale from three sources:
- It will check the for a persisted locale (more on that later).
- Failing to find one, it will fetch a current
Request
object from aRequestStack
and retrieve the locale from that. - Should there be no current
Request
(this will be the case for console commands and some test environments), it will return the default locale from theFrameworkBundle
.
If you want to manually set the locale that is returned from the LocaleProvider
, calling the LocaleProvider::saveLocale()
method will persist it in the service until the next request. You can allso manually call the LocaleProvider::resetSavedLocale()
to clear it.
Disabling automatic translations updates
If you prefer creating translations manually and do not want them overwritten with
contents of the translatable entity, you can set the disableAutoTranslationsUpdate
option in it's configuration to true
. This will prevent any creation or update of
translations during the flush operation, but will still populate the translatable
entity with contents of a translation entity, if one exists for the current locale.