This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
16.

Translatable Mapping

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

It's time to dive into how users will mark their app's entities for translation. For our app, we have four entities: Article, Category, Tag, and Translation. The Translation entity of course stores our translations, but the other three need translations.

For instance, check out Article. We need to mark this class as translatable. And down here, we want the title and content properties to be translatable fields on this entity.

Choosing a Translation Approach

We've got a couple ways to achieve this. One method could be to create some sort of interface in our bundle. User's would then implement this on entities they want translatable. But a more modern and slick approach is to use PHP attributes.

I'm all for slick and modern, so let's go with attributes.

These will be similar to these Doctrine ORM mapping attributes. Ours will map translatable entities and properties.

Creating the Translatable Attribute

In our object-translation-bundle/src directory, create a new directory, Mapping. This is where we'll store our attributes.

Inside, create a new PHP class called Translatable. This attribute will be used to mark an entity as translatable. Make it final. To let PHP know that this is an attribute, we need to use an attribute! Above the class, write #[\Attribute()]. The first argument is what type of element this attribute can be applied to. In our case, we want to apply this to classes, so write \Attribute::TARGET_CLASS:

// ... lines 1 - 4
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Translatable
{
// ... lines 8 - 11
}

We need to pass in one argument to this attribute: the name. This is that string alias we'll store in the database instead of the class name. Create a constructor with public function __construct(). Now add a single, mandatory argument (that's also a property): public string $name:

// ... lines 1 - 4
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Translatable
{
public function __construct(
public string $name,
) {
}
}

Creating the TranslatableProperty Attribute

We also need to let our bundle know what properties should be translatable. Now, we could use an array argument in the Translatable attribute to list the properties... but I think it's cleaner to use a separate attribute for this.

Also in the Mapping directory, create another PHP class called TranslatableProperty. This class will be empty, no arguments - it's just used as a property marker.

Mark the class as an attribute with #[\Attribute()]. This time, we want to apply this attribute to properties, so use \Attribute::TARGET_PROPERTY:

// ... lines 1 - 4
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class TranslatableProperty
{
}

Marking Entities as Translatable

Now that our attributes are ready, let's go to our app's entities and mark them. Start with Article. Above the class, add the attribute #[Translatable('article')]:

158 lines | src/Entity/Article.php
// ... lines 1 - 13
#[Translatable('article')]
class Article
{
// ... lines 17 - 156
}

Now, identify the properties that are going to be translatable. Above $title, add #[TranslatableProperty] and do the same for $content:

158 lines | src/Entity/Article.php
// ... lines 1 - 14
class Article
{
// ... lines 17 - 25
#[TranslatableProperty]
private ?string $title = null;
// ... lines 28 - 35
#[TranslatableProperty]
private ?string $content = null;
// ... lines 38 - 156
}

Next, mark Category as translatable with #[Translatable('category')]. The $name property will be a TranslatableProperty:

83 lines | src/Entity/Category.php
// ... lines 1 - 12
#[Translatable('category')]
class Category
{
// ... lines 16 - 21
#[TranslatableProperty]
private ?string $name = null;
// ... lines 24 - 81
}

Finally, mark Tag as #[Translatable('tag')] and again, the $name property will be the only TranslatableProperty:

77 lines | src/Entity/Tag.php
// ... lines 1 - 12
#[Translatable('tag')]
class Tag
{
// ... lines 16 - 21
#[TranslatableProperty]
private ?string $name = null;
// ... lines 24 - 75
}

Both our Tag and Category have a single translatable property. Article has two: $content and $title. Nice!

Creating Translation Fixtures

There's one last thing I want to do. If we look in our app's Story directory, we'll find AppStory where we're loading the articles for our fixture data. To test this more effectively, we'll create some translation fixtures. This way, when we switch to French on our site, we'll display some mock French data, just like we currently have mock English data.

We need to create a factory using Foundry. So, run:

symfony console make:factory

Create a factory for the Translation entity: 0 in this list.

If we navigate back to our Factory directory, here it is: TranslationFactory. In defaults(), it's detected all fields and is generating fake data for them. We're going to override all of these when we create it, so no need to edit anything here - we just need this factory to exist.

Now, in AppStory, find the first article "Why asteroids taste like bacon". Assign this created article to a variable with $article1 = :

189 lines | src/Story/AppStory.php
// ... lines 1 - 12
final class AppStory extends Story
{
public function build(): void
{
// ... lines 17 - 23
$article1 = ArticleFactory::createOne([
// ... lines 25 - 67
]);
// ... lines 69 - 186
}
}

We do this because we need to get its ID to link our translations to it.

Below this fixture, create our first translation with TranslationFactory::createOne(). Inside, an array: 'locale' => 'fr', 'objectId' => $article1->getId(), 'objectType' => 'article'. Now, the first field we want to translate is the title, so 'field' => 'title'. For the value, 'value' => 'French title...':

189 lines | src/Story/AppStory.php
// ... lines 1 - 12
final class AppStory extends Story
{
public function build(): void
{
// ... lines 17 - 69
TranslationFactory::createOne([
'locale' => 'fr',
'objectId' => $article1->getId(),
'objectType' => 'article',
'field' => 'title',
'value' => 'French title...',
]);
// ... lines 77 - 186
}
}

Not super creative but it gets the job done.

Now for the content translation. Copy this whole TranslationFactory::createOne() and paste below. Change the field to 'content' and the value to 'French content...'. Leave everything else the same - as it's for the same object and locale:

189 lines | src/Story/AppStory.php
// ... lines 1 - 12
final class AppStory extends Story
{
public function build(): void
{
// ... lines 17 - 76
TranslationFactory::createOne([
'locale' => 'fr',
'objectId' => $article1->getId(),
'objectType' => 'article',
'field' => 'content',
'value' => 'French content...',
]);
// ... lines 84 - 186
}
}

Good enough to get started!

At your terminal, reload the fixtures with:

symfony console foundry:load-fixtures

Choose y to confirm recreating the database.

Let's do a quick sanity check to ensure these translations were indeed loaded:

symfony console doctrine:query:sql 'SELECT * FROM translation'

(translation is the table name for our Translation entity.)

Perfect! Our two translation fixtures are in the database.

Next, we'll write the logic that loads translations from the database and translates our Translatable entities.