Skip to content

Translate Custom fields

Certain Ibexa installations make use of custom fields. Those fields do need additional configuration to make them available for translation.

Custom field handlers

Use custom field handlers to tell the translator how to generate XLIFF from a custom field type. Custom field handlers should implement the interface Xrow\TranslationBundle\API\FieldHandlerInterface. If they implement this interface then after clearing the cache, they will automatically be registered.

The interface defines the following methods:

public static function getFieldType(): string

Return the field type identifier that the field handler works with.

public function isFieldEmpty(Field $field): bool

Test whether the field value is empty (or only contains white space characters).

public function addSegments(Field $field, FieldDefinition $fieldDefinition, DOMElement $unit)

Add zero or more elements to represent the field value.

public function getValue(Field $field, FieldDefinition $fieldDefinition, DOMElement $unit): ?ValueInterface`

Get translated value for a field from imported XLIFF, or null for a missing field value.

Example custom field handler

Consider the hello_world custom field type described in the Ibexa Developer Documentation. The field handler would look like:

<?php

namespace App\FieldType\HelloWorld;

use DOMElement;
use eZ\Publish\API\Repository\Values\Content\Field;
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
use eZ\Publish\SPI\FieldType\Value as ValueInterface;
use Xrow\TranslationBundle\API\FieldHandlerInterface;

/**
 * Translation handler for custom 'hello_world' field type.
 */
class HelloWorldFieldHandler implements FieldHandlerInterface
{
    /**
     * Return the field type identifier that the field handler works with.
     *
     * @return string
     */
    public static function getFieldType(): string
    {
        return 'hello_world';
    }

    /**
     * Test whether the field value contains white space characters.
     *
     * @param Field $field
     * @return bool
     */
    public function isFieldEmpty(Field $field): bool
    {
        return trim($field->value->getName()) == '';
    }

    /**
     * Add exactly one <segment> element to represent the 'hello_world' field value.
     * Modify the containing <unit>.
     *
     * @param Field $field
     * @param FieldDefinition $fieldDefinition
     * @param DOMElement $unit
     */
    public function addSegments(Field $field, FieldDefinition $fieldDefinition, DOMElement $unit)
    {
        $source = $unit->ownerDocument->createElement('source');
        $text = $unit->ownerDocument->createTextNode($field->value->getName());
        $source->appendChild($text);
        $segment = $unit->ownerDocument->createElement('segment');
        $segment->appendChild($source);
        $unit->appendChild($segment);
        $unit->setAttribute('canResegment', 'no');
    }

    /**
     * Generate a field value from the translated <target> element inside the <unit>.
     * For 'hello_world' field type, the <unit> contains one <segment> element which contains one <target>.
     *
     * @param Field $field
     * @param FieldDefinition $fieldDefinition
     * @param DOMElement $unit
     * @return ValueInterface|null
     */
    public function getValue(Field $field, FieldDefinition $fieldDefinition, DOMElement $unit): ?ValueInterface
    {
        foreach ($unit->childNodes as $child) {
            if ($child->tagName == 'segment') {
                foreach ($child->childNodes as $grandchild) {
                    if ($grandchild->tagName == 'target') {
                        $value = new Value();
                        $value->setName($grandchild->textContent);

                        return $value;
                    }
                }
            }
        }

        return null;
    }
}

Translatable text or internal data?

When writing a custom field handler consider what information needs to be presented to the translator (text) and what information is for internal Ibexa consumption only.

All the field data must be saved in the XLIFF. The translatable text should be saved in one or more <segment> elements, whilst the internal data should be saved inside a <skeleton> tree. If your field has internal data that should not be displayed to the translator then your custom field handler can extend the class Xrow\TranslationBundle\Service\FieldHandler\SkeletonHandler which provides helpful methods for handling <skeleton>.

Look at the ezurl field handler class Xrow\TranslationBundle\Service\FieldHandler\EzUrlFieldHandler as an example. It generates (simplified) XLIFF looking something like:

<file ...>
    <skeleton>
        ...
        <xrow:unit>
            <xrow:value></xrow:value>
            <xrow:value>{"link":"http://www.example.com","text":"some text"}</xrow:value>
        </xrow:unit>
    </skeleton>
    ...
    <unit>
        <segment>
            <source>some text</source>
        </segment>
    </unit>
</file>

The original field value is stored as JSON in an <xrow:value> sub-node.

The text sub-field value is translatable, so is stored in the <source> sub-node. The translator will add a corresponding <target> element with the translated text.

Custom field value deserializers

Untranslatable fields and fields that are only partly translatable must store their serialized values in the <skeleton> in a serialized form.

When a collection is exported, the field values are serialized into JSON which is stored in XLIFF in <xrow:value> elements. Usually the default serialize method is sufficient.

When a collection is imported, the field values are deserialized from JSON. In most cases the default deserialize method is sufficient but some field types may need special handling.

If your custom field type needs a custom field value deserializer because the default deserializer doesn't work, it must implement the interface Xrow\TranslationBundle\API\FieldValueSerializerInterface. If they implement this interface then after clearing the cache, they will automatically be registered.

The interface defines the following methods:

public static function getFieldType(): string

Return the field type identifier that the field value serializer and serializer work with.

public function serialize(Field $field): string;

Create serialized data from an Ibexa field value.

public function deserialize(Field $field, string $serialized): ?ValueInterface;

Create an Ibexa field value from serialized data.

So far, the default serializer provided by the bundle is sufficient. To reuse this, your custom class should extend Xrow\TranslationBundle\Service\Serializer\AbstractFieldValueSerializer. Then you only need to implement the getFieldType and deserialize methods:

use Ibexa\Contracts\Core\FieldType\Value as ValueInterface;
use Ibexa\Contracts\Core\Repository\Values\Content\Field;
use Xrow\TranslationBundle\API\FieldValueSerializerInterface;
use Xrow\TranslationBundle\Service\Serializer\AbstractFieldValueSerializer;

/**
 * Serializer and deserializer for my custom Ibexa field type.
 */
class MyCustomFieldValueSerializer extends AbstractFieldValueSerializer implements FieldValueSerializerInterface
{
    public static function getFieldType(): string
    {
        return 'my_custom_field_type';
    }

    public function deserialize(Field $field, string $serialized): ?ValueInterface
    {
        ...
    }
}

Last update: February 15, 2023