How to implement hooks

e-Spirit AG

2020-02-20
Table of Contents

1. Introduction

Hooks are little plugins which allow you add further functionality to TranslationStudio. The following hook types are available:

DescriptionInterfaceHook Type

After a successful job creation

IJobCreationHook

jobcreation

to process the content of an exported FirstSpirit PAGE data xml file

IFirstSpiritFileExportPageHook

fsexporthook_page

to process the content of an exported FirstSpirit ENTITY data xml file

IFirstSpiritFileExportEntityHook

fsexporthook_entity

Process custom message xmls

ICustomMessageXmlHook

custommessagehook

Before a translatable file is being imported into a TMS

ITmsImportHook

tmsimport

After a translatable XML file has been downloaded from the TMS

ITmsExportHook

tmsexport

After a FirstSpirit ImportOperation has been completed

IFirstSpiritImportHook

fsimporthook

TranslationStudio allows you to add custom message fields to the translation workflow. Yet, these fields have to be evaluated in order to do something with them. For example, you may wish to create a meta file based on the selection of a cost centre.

The translation workflow stores your custom fields in an XML format in which the FirstSpirit input component’s name is the node name and the value stored as CDATA. This tutorial will demonstrate how to implement such a hook.

You will find a Javadoc available at api/javadoc/hooks

2. Setup the projects

First, create a Java project, for example HOOK - Custom Messages and add the library TranslationStudio Connector API jar from your TranslationStudio package at /api/.

3. Implementation

Open the newly created project and create a new class

com.idmedia.translationstudio.hooks.mycustommessage.MyCustomMessageBeforeTmsImportHook implementing ICustomMessageXmlHook

3.1. Create methods

The interface requies you to implement following methods:

    /**
     * Get the name of this hook
     * @return Name
     */
    @NotNull
    public String getName()
    {
        return "My Custom Message Hook";
    }


    /**
     * Init this hook
     *
     * @param pStorage Storage Object
     * @param pContext Hook Context
     */
    public void init(IHookChainStorage pStorage, IHookContext pContext)
    {
        // TODO
    }

    /**
     * Process Hook
     *
     * @param pObject The translatable xml file
     */
    public void onHook(ITranslatableFile pObject)
    {
        // TODO
    }

3.2. Understanding the methods

The method getName() will be logged before the hook will be called.

The method public void init(IHookChainStorage pStorage, IHookContext pContext) allows you to process the hook storage values (e.g. if you chain hooks and want to pass results) if necessary. It will be called before onHook(ITranslatableFile pObject).

The method public void onHook(ITranslatableFile pObject) performs the actual task of the hook. It also allows you to access information related to the translatable xml file this hook was called upon.

3.3. Preparing the workflow

Since this tutorial demonstrates how to process the custom message values from your TranslationStudio workflow, we need to agree on a sample set of fields. In this example, we will assume the following input components:

For example, edit the TranslationStudio script translationstudio_customfields and insert the following component:

<CMS_INPUT_RADIOBUTTON name="sc_custom_radio" allowEmpty="no" gridHeight="2" gridWidth="1" hFill="yes" hidden="no" useLanguages="no">
    <ENTRIES>
        <ENTRY value="marketing">
            <LANGINFOS>
                <LANGINFO lang="*" label="Marketing &amp; PR"/>
            </LANGINFOS>
        </ENTRY>
        <ENTRY value="other">
            <LANGINFOS>
                <LANGINFO lang="*" label="Other"/>
            </LANGINFOS>
        </ENTRY>
    </ENTRIES>
    <LANGINFOS>
        <LANGINFO lang="*" label="Cost Centre"/>
    </LANGINFOS>
</CMS_INPUT_RADIOBUTTON>

When you execute the TranslationStudio workflow, the new fields can be edited as shown below:

Sample Form. 

Your custom input will be stored in the TranslationStudio data source temporarily. Once TranslationStudio has received the translation request, the custom XML will be stored in the TranslationStudio working directory working/custom-messages in a base64 encoded text file.

<CUSTOM>
    <sc_custom_radio><![CDATA[marketing]]></sc_custom_radio>
    <sc_message><![CDATA[Send questions to

        reply@mycompany.com]]></sc_message>
</CUSTOM>

3.4. Implementation - init

Since we want to create a new file, we need to store the target folder and also store the open FirstSpirit connection in class member fields.

    private final static Logger LOGGER = Logger.getLogger(MyCustomMessageBeforeTmsImportHook.class.getName());
    private String m_sWorkingDir = null;
    private IFirstSpiritConnection m_pConnection;

    /**
     * Init this hook
     *
     * @param pStorage Storage Object
     * @param pContext Hook Context
     */
    public void init(IHookChainStorage pStorage, IHookContext pContext)
    {
        /**
         * get directory configuration
         */
        m_sWorkingDir = pContext.requestDirectoryConfiguration().getWorkingDirectoryTranslatable();
        m_pConnection = pContext.getConnection();
    }

3.5. Implementation - onHook

Now that we know the target folder and have a FirstSpirit connection at hand, we can start adding functionality to the onInit method.

First, we check if the parameter is valid at all. If it is not, there is no need to proceed:

if (pObject == null)
    return;

Second, we need to create a StringBuilder to store all necessary data in:

final StringBuilder psMessage = new StringBuilder(0);

Third, let’s obtain the stored custom XML shown above and process it. It could not be simpler. However, since there might be several pages and entities in the translatable XML file, we will obtain a list of custom XMLs. The evaluation of the XMLs will be encapsulated in a separate method.

final List<String> vsItems = pObject.getCustomFieldValues(m_pConnection);

/**
 * add each message
 */
for (String _item : vsItems)
    processXml(_item, psMessage);

Fourth, if there is no XML (i.e. no custom data input), there is no need to proceed.

/**
 * No custom message set - no file needs to be stored
 */
if (psMessage.length() == 0)
    return;

Fifth, we want to add some header and footer information to the text file we are about to create.

/**
 * Add Header and Footer to this file
 */
addCustomHeader(psMessage, pObject);
addCustomFooter(psMessage);

Sixth, let’s store the file.

final String sStoredFile = storeNewFile(pObject, psMessage);
if (sStoredFile == null)
{
    LOGGER.warning("Could not store custom file.");
    return;
}

Finally, since the file has been created, we want to associate it to the translatable xml file. This will guarantee that any connector will also forward this file and consider it a part of the translatable xml file package.

/**
 * add to additional files for later use
 */
pObject.addAdditionalFile(sStoredFile);

LOGGER.log(Level.INFO, "Additional information stored in file {0}", sStoredFile);

3.6. Implementation - additional methods

The example above delegated the processing of a custom XML to a separate method

Importantly, since there might be several pages and entities in a single translatable XML file, we have too associate the respective custom XML to the target section of the translatable XML file. TranslationStudio provides all necessary markers and information in the XML which will look similar to the following example:

<?xml version="1.0" encoding="UTF-8"?>
<CUSOM_XML>
    <CUSTOM_ID>FirstSpiritProjectId-lTranslationStudioContent2Id-lTaskTicketDatasetId</CUSTOM_ID>
    <CUSTOM>
        <sc_custom_radio><![CDATA[marketing]]></sc_custom_radio>
        <sc_message><![CDATA[Send questions to

reply@mycompany.com]]></sc_message>
    </CUSTOM>
</CUSOM_XML>

So, since we have all the background knowledge necessary, let us have a look at it using pseudo code

/**
 * Extract the message content from each message xml
 *
 * @param sXml A custom XML
 * @param psTargetMessage Target StringBuilder to be stored in a file later
 */
private void processXml(@Nullable String sXml, @NotNull StringBuilder psTargetMessage)
{
    if (sXml == null || sXml.trim().isEmpty())
        return;

    final String psId = extractTag(sXml, "CUSTOM_ID");
    final Element pCustomXml = parseXml( extractTag(sXml, "CUSTOM") );

    final String sCostCentre = getNodeVale(pCustomXml, "sc_custom_radio");
    final String sMessage = getNodeVale(pCustomXml, "sc_message");

    /** Store data */
    psTargetMessage.append("Cost Centre: ").append(sCostCentre).append("\n");
    psTargetMessage.append("Additional Information: ").append(sMessage).append("\n");
}

Finally, let’s add some header and footer information:

/**
 * Add header information
 *
 * @param psMessage
 * @param pObject
 */
private void addCustomHeader(@NotNull StringBuilder psMessage, @NotNull ITranslatableFile pObject)
{
    final StringBuilder psHeader = new StringBuilder();

    psHeader.append("\n");
    psHeader.append("Additional Information\n");
    psHeader.append("======================\n");
    psHeader.append("\n");
    psHeader.append("Source Language: ").append(pObject.getInitialLanguage()).append("\n");
    psHeader.append("Target Language: ").append(pObject.getTargetLanguage()).append("\n");

    final Date pDate = pObject.getDeadline();
    if (pDate != null)
    {
        psHeader.append("Deadline: ").append(getFormattedDate(pDate.getTime()));
        psHeader.append("\n");
    }

    psHeader.append("\n");
    psHeader.append("\n");

    psMessage.insert(0, psHeader);
}

/**
 * Format Date
 * @param lDate
 * @return hh:mm:ss
 */
@NotNull
private String getFormattedDate(final long lDate)
{
    if (lDate > 0) try
    {
        return new SimpleDateFormat("dd.MM.yyyy").format(new Date(lDate));
    }
    catch (IllegalArgumentException ex)
    {
        LOGGER.warning(ex.getMessage());
    }

    return "";
}

/**
 * Add footer information
 *
 * @param psMessage
 */
private void addCustomFooter(@NotNull StringBuilder psMessage)
{
    psMessage.append("Each entity, paragraph or page has an TRANSLATABLE-ID attribute.\n");
    psMessage.append("This will allow you to relate additional information to their respective translatable element(s).");
    psMessage.append("\n\n- end.");
}

4. Adding the hook to TranslationStudio

Adding your hook to TranslationStudio consists of the following steps:

  1. Stop the TranslationStudio application.
  2. Deploy the JAR file to TranslationStudio’s lib directory.
  3. Update TranslationStudio’s conf/hooks.xml file.
  4. Start the TranslationStudio application.

The xml file contains a root node hooks and you have to add your class name to the respective hook node, for example:

<?xml version="1.0" encoding="UTF-8"?>
<hooks>
    <hook type="custommessagehook">
        <class name="com.idmedia.translationstudio.hooks.mycustommessage.MyCustomMessageBeforeTmsImportHook" />
    </hook>
    <hook type="jobcreation" />
    <hook type="tmsimport" />
    <hook type="processimportablefile" />
    <hook type="tmsexport" />
    <hook type="fsexporthook_page" />
    <hook type="fsexporthook_entity" />
    <hook type="fsimporthook" />
</hooks>

5. Final words

Congratulations, you have successfully created your own hook.