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
26.

Export Translations Command

|

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

Alright, we're almost done the coding for a 1.0 release. But, we've got one thing left to tackle - managing the translations. Eventually, I'd like this bundle to include perhaps a FormType that can allow users to integrate management of translations into their admin UI. But for now, let's create two console commands: one for exporting translatable fields in the default locale, English in our case, to a CSV file. The user would then take this file to a translation service that translates the values into their supported locales. Then, we'll have import command to suck these translated files back in.

The Export Command

First things first, let's build that export command. To make things quicker, I've already created it. In the tutorial directory, copy ObjectTranslationExportCommand.php into the bundle's src/Command directory. If you don't see it in your tutorial directory, don't worry. You can copy it from the script below:

// ... lines 1 - 2
namespace SymfonyCasts\ObjectTranslationBundle\Command;
// ... lines 4 - 12
/**
* @internal
*/
#[AsCommand(
name: 'object-translation:export',
description: 'Exports object translations to a CSV.',
)]
final class ObjectTranslationExportCommand extends Command
{
public function __construct(
private TranslatableMappingManager $mappingManager,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to export to.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$file = $input->getArgument('file');
$io->title('Exporting Object Translations to CSV');
$fp = fopen($file, 'w');
fputcsv($fp, [
'type',
'id',
'field',
'value',
]);
foreach ($io->progressIterate($this->mappingManager->allTranslatableObjects()) as $object) {
$type = $this->mappingManager->translatableTypeFor($object);
$id = $this->mappingManager->idFor($object);
foreach ($this->mappingManager->translatableValuesFor($object) as $field => $value) {
fputcsv($fp, [
$type,
$id,
$field,
$value,
]);
}
}
fclose($fp);
$io->success(sprintf('Exported to "%s"', $file));
return self::SUCCESS;
}
}

Now, let's dissect this command a bit. This should look familiar. Command name: object-translation:export, description: Exports object translations to a CSV.

For the constructor, we're just injecting our TranslatableMappingManager - that's all we'll need.

The configure() method is where we set up arguments and options. This command will take one argument: the file path to export the CSV to. The user can pass an absolute or relative path.

In execute(), create the $io, grab the $file argument, then output a title. Next, we fopen the file for writing. Since this will be a CSV, we're using fputcsv() to write the column headers: type, ID, field, and value.

Just like our warmup command, we progress iterate over all our translatable objects. Grab the type and ID for each object, then iterate over this mapping manager translatableValuesFor() method, which we'll create soon. This method iterates over the translatable object's translatable field names and values.

Inside, we fputcsv() a row with the type, ID, field, and value.

Finally, below the two loops, we fclose() the file and output a success message.

Wiring Up the Command

Now we need to wire up this command. Head over to our bundle's services.php file. Add ->set('.symfonycasts.object_translator.export_command, ObjectTranslationExportCommand::class). For the args(), just the one service: copy and paste this from the command above.

Finally, ->tag('console.command'):

// ... lines 1 - 10
return static function (ContainerConfigurator $container): void {
$container->services()
// ... lines 13 - 37
->set('.symfonycasts.object_translator.export_command', ObjectTranslationExportCommand::class)
->args([
service('.symfonycasts.object_translator.mapping_manager'),
])
->tag('console.command')
// ... lines 43 - 45
;
};

Creating a New Method

Back in the ObjectTranslationExportCommand, this translatableValuesFor() method on our mapping manager service doesn't exist. Create it. Use object as the argument type and iterable as the return type:

// ... lines 1 - 13
final class TranslatableMappingManager
{
// ... lines 16 - 92
public function translatableValuesFor(object $object): iterable
{
// ... lines 95 - 103
}
}

We'll use reflection to grab these properties, so first, get the reflection class with $class = new \ReflectionClass($object);. Next, loop over the properties with foreach ($class->getProperties() as $property):

// ... lines 1 - 13
final class TranslatableMappingManager
{
// ... lines 16 - 92
public function translatableValuesFor(object $object): iterable
{
$class = new \ReflectionClass($object);
// ... line 96
foreach ($class->getProperties() as $property) {
// ... lines 98 - 102
}
}
}

How do we know what properties are translatable? Remember the TranslatableProperty attribute we created earlier? We haven't used it yet but now is it's time to shine! In our app's entities, this attribute marks the translatable properties.

First, exclude the properties that don't have this attribute. if (!$property->getAttributes(TranslatableProperty::class)), continue:

// ... lines 1 - 13
final class TranslatableMappingManager
{
// ... lines 16 - 92
public function translatableValuesFor(object $object): iterable
{
// ... lines 95 - 96
foreach ($class->getProperties() as $property) {
if (!$property->getAttributes(TranslatableProperty::class)) {
continue;
}
// ... lines 101 - 102
}
}
}

Now we know the property is translatable, so, yield $property->getName() => $property->getValue($object):

// ... lines 1 - 13
final class TranslatableMappingManager
{
// ... lines 16 - 92
public function translatableValuesFor(object $object): iterable
{
// ... lines 95 - 96
foreach ($class->getProperties() as $property) {
// ... lines 98 - 101
yield $property->getName() => $property->getValue($object);
}
}
}

Even if the property is private or protected, when using reflection like this, we can get the value.

Back in the command... sweet! No more warning!

Time to test 'er out! Over in the terminal, run:

symfony console object-translation:export var/export.csv

Nice! The progress bar shows 12 objects were exported and saved to our project's var directory as export.csv.

Back in your IDE, find that file and open it!

Boom! Here it is! First row is our column headers, and each row below is the exported English version of our translatable object's translatable fields. This looks kind of nasty because of all the line breaks in our article's content fields, but rest assured, this is a valid CSV.

Next, we'll create the import command to bring the translated version of this file back into our project.