Translate WPML via WordPress REST API

Although REST API is now included into the WordPress core, WPML – one of the most popular translation plugin – still didn’t catch up to that. Anyways, after extensive googling I decided it’s better to implement the feature myself. Here is how the translation worked.

Step 1: Add a custom end-point

I added an end-point /translate to do all the translation related works. Regardless of the translation type (string, postcategory etc.) all requested are sent to this end-point.

add_action( 'rest_api_init', function () {
    register_rest_route( 'mysite', '/translate', array(
        'methods' => 'POST',
        'callback' => 'mysite_translate',
        'args' => array(
            'lang_source' => array(
                'required' => true,
                'validate_callback' => function($param, $request, $key) {
                    if( !empty( $param ) ) {
                        return preg_match( '/[a-zA-Z]{2}/', $param );
                    }

                    return false;
                }
            ),
            'lang_dest' => array(
                'required' => true,
                'validate_callback' => function($param, $request, $key) {
                    if( !empty( $param ) ) {
                        return preg_match( '/[a-zA-Z]{2}/', $param );
                    }

                    return false;
                }
            ),
            'type' => array(
                'required' => true,
                'validate_callback' => function($param, $request, $key) {
                    if( !empty( $param ) ) {
                        return in_array( $param, array(
                            'post',
                            'page',
                            'category',
                            'string'
                        ) );
                    }

                    return false;
                }
            ),
            'params' => array(
                'required' => true,
                'validate_callback' => function($param, $request, $key) {
                    return is_array( $param );
                }
            )
        ),
        'permission_callback' => function () {
            return current_user_can( 'edit_others_posts' );
        }
    ) );
} );

So the API call should send a POST request to http://websiteurl.com/mysite/translate and the request should contain four parameters.

lang_source

The source language code (e.g. bn, en etc.). This should match the language code set from WPML settings. Generally, this should be the default language.

lang_dest

The target language code (e.g. bn, en etc.). This should also match a language code available in WPML settings.

type

Type of the translation request. This can be post, page, category, string etc. While post, page and category are set in WordPress core, string refers to the WPML strings that can be translated.

params

This is an array containing all the parameters required for the requested translation type. This may vary based on the request. For example, post requires title, description etc. while string requires string_source, context, string_dest etc.

Step 2: Implement the callback function

function mysite_translate( WP_REST_Request $request ) {
    // If wordpress isn't loaded load it up.
    if ( !defined('ABSPATH') ) {
        $path = $_SERVER['DOCUMENT_ROOT'];
        include_once $path . '/wp-load.php';
    }

    $parameters = $request->get_params();

    if( !isset( $parameters[ 'lang_source' ] ) || empty( $parameters[ 'lang_source' ] ) ) {
        return new WP_Error( 'invalid_translation_request', 'Parameter "lang_source" is required.' );
    }

    if( !isset( $parameters[ 'lang_dest' ] ) || empty( $parameters[ 'lang_dest' ] ) ) {
        return new WP_Error( 'invalid_translation_request', 'Parameter "lang_dest" is required.' );
    }

    if( !isset( $parameters[ 'type' ] ) || empty( $parameters[ 'type' ] ) ) {
        return new WP_Error( 'invalid_translation_request', 'Parameter "type" is required.' );
    }

    if( !isset( $parameters[ 'params' ] ) || empty( $parameters[ 'params' ] ) || !is_array( $parameters[ 'params' ] ) ) {
        return new WP_Error( 'invalid_translation_request', 'Parameter "params" is required and must be an array.' );
    }

    $langSource = $parameters['lang_source'];
    $langDest = $parameters['lang_dest'];
    $type = $parameters['type'];
    $params = $parameters['params'];

    switch( $type ) {
        case 'post':
        case 'page':
            return mysite_translate_post( $langSource, $langDest, $params );
            break;
        case 'category':
            return mysite_translate_term( $langSource, $langDest, $params );
            break;
        case 'string':
            return mysite_translate_string( $langSource, $langDest, $params );
            break;
        default:
            return new WP_Error( 'invalid_translation_request', '"' . $type . '" is not a valid translation type!');
            break;
    }
}

Notice that this callback does not actually do any translation. Here I just checked the parameters and then called the appropriate callback function based on request type. Another thing to note that any custom post type can also use the callback for post and any custom taxonomy can use the callback for category.

Step 3: Translate posts

Post translation is done within the mysite_translate_post function. The params parameter should contain the array with translated values for the post (or page or any custom post type).

Parameters

Values that can be sent within the params parameter field are these:

  • post_id
    – Numeric ID of the source object.
  • title
    – Translated title of the post.
  • content
    – Translated content of the post.
  • excerpt
    – Translated excerpt of the post.
  • custom_fields
    – A list of all the custom fields that need to be copied from the source language. This should only contain the field names and no values. Example values in custom_fields should be like this:

    'custom_fields' => array( 
        'custom_field_1', 
        'custom_field_2', 
        '...', 
        '...'
    )

The mysite_translate_post function description is like this:

function mysite_translate_post( $langSource, $langDest, $parameters )
{
    // If wordpress isn't loaded load it up.
    if ( !defined('ABSPATH') ) {
        $path = $_SERVER['DOCUMENT_ROOT'];
        include_once $path . '/wp-load.php';
    }

    // Include WPML API
    include_once( WP_PLUGIN_DIR . '/sitepress-multilingual-cms/inc/wpml-api.php' );

    if( !isset( $parameters[ 'post_id' ] ) || !is_numeric( $parameters[ 'post_id' ] ) ) {
        return new WP_Error( 'invalid_translation_parameters', 'Parameter "post_id" is required for the translation type.');
    }

    /* @var $post WP_Post */
    $post = get_post( $parameters['post_id'] );
    if ( is_null($post) ) {
        return new WP_Error( 'post_not_fount', 'Post with ID "'. $parameters['post_id'] .'" not found!' );
    }

    $postId = get_post_field( 'ID', $post );
    $postType = get_post_type( $post );

    //Copy the featured image ID.
    $postThumbnailId = get_post_thumbnail_id( $post );

    //Set title or copy from source object
    $postTranslatedTitle = $parameters[ 'title' ] ? sanitize_text_field( $parameters[ 'title' ] ) : get_post_field( 'post_title', $post );
    //Set the content or set from source object
    $postTranslatedContent = $parameters[ 'content' ] ? wpautop( wp_kses_post( $parameters[ 'content' ] ) ) : get_post_field( 'post_content', $post );
    //Set the excerpt or set from source object
    $postTranslatedExcerpt = $parameters[ 'excerpt' ] ? sanitize_textarea_field( $parameters[ 'excerpt' ] ) : get_post_field( 'post_excerpt', $post );
    
    // Check if translated post already exists
    if ( !is_null( wpml_object_id_filter( $postId, $postType, false, $langDest ) ) ) {
        return new WP_Error( 'post_exists', ucfirst($postType) . ' "'. get_the_title($post) .'" translated to "'. $langDest .'" already exists!' );
    }

    // Check if translated post title already exists
    if ( get_page_by_title( $postTranslatedTitle, OBJECT, $postType ) ) {
        return new WP_Error( 'post_title_exists', ucfirst($postType) . ' "'. $postTranslatedTitle .'" title already exists!' );
    }

    // Prepare post terms to duplicate
    $postTaxonomies = get_post_taxonomies( $post );
    $postTaxonomiesTranslated = array();
    foreach ( $postTaxonomies as $postTax ) {
        $postTerms = wp_get_post_terms( $postId, $postTax );
        
        foreach ( $postTerms as $postTerm ) {
            // Check if terms already translated
            $postTermTranslatedId = wpml_object_id_filter( get_term_field( 'term_id', $postTerm ), $postTax, false, $langDest );
            
            if ( is_null($postTermTranslatedId) ) {
                return new WP_Error( 'post_translated', 'Translate "'. get_term_field( 'name', $postTerm ) .'" term with ID = "'. get_term_field( 'term_id', $postTerm ) .'" in "'. $postTax .'" taxonomy first!' );
            }
            
            $postTaxonomiesTranslated[ $postTax ][] = $postTermTranslatedId;
        }
    }
    
    // Insert translated post
    $postTranslatedId = wp_insert_post( array(
                            'post_title' => $postTranslatedTitle,
                            'post_content' => $postTranslatedContent,
                            'post_excerpt' => $postTranslatedExcerpt,
                            'post_type' => $postType,
                            'post_status' => 'publish'
                        ), true );
    if ( $postTranslatedId instanceof WP_Error ) {
        return $postTranslatedId;
    }
    
    // Set post terms
    foreach ( $postTaxonomiesTranslated as $postTaxonomyTranslated => $postTermsTranslated ) {
        wp_set_post_terms( $postTranslatedId, $postTermsTranslated, $postTaxonomyTranslated );
    }

    // Set post featured image if any
    if ( $postThumbnailId ) {
        set_post_thumbnail( $postTranslatedId, $postThumbnailId );
    }
    
    // Set post custom fields
    foreach ( $parameters[ 'custom_fields' ] as $customFieldName ) {
        $customFieldValue = get_post_meta( $postId, $customFieldName, true );
        
        if ($customFieldValue) {
            add_post_meta( $postTranslatedId, $customFieldName, $customFieldValue );
        }
    }
    
    // Get trid of original post
    $trid = wpml_get_content_trid( 'post_' . $postType, $postId );
    
    // Associate original post and translated post
    global $wpdb;
    $wpdb->update( $wpdb->prefix.'icl_translations', array( 'trid' => $trid, 'element_type' => 'post_' . $postType, 'language_code' => $langDest, 'source_language_code' => $langSource ), array( 'element_id' => $postTranslatedId ) );

    // Return translated post
    return get_post( $postTranslatedId );
}

Just like that, the post is translated and linked to the original post through WPML.

Step 4: Translate taxonomy

All taxonomy terms along with custom taxonomies are translated using the mysite_translate_term function. This function also expects a set of values in the params parameter.

Parameters

Values that can be sent within the params parameter field are these:

  • term_id
    – Numeric ID of the source object.
  • name
    – Translated title of the term.
  • description
    – Translated description of the term.
  • custom_fields
    – A list of all the custom fields that need to be copied from the source language. This should only contain the field names and no values. Example values in custom_fields should be like this:

    'custom_fields' => array( 
        'custom_field_1', 
        'custom_field_2', 
        '...', 
        '...'
    )

The mysite_translate_term function description is like this:

function mysite_translate_term( $langSource, $langDest, $parameters )
{
    // If wordpress isn't loaded load it up.
    if ( !defined('ABSPATH') ) {
        $path = $_SERVER['DOCUMENT_ROOT'];
        include_once $path . '/wp-load.php';
    }

    // Include WPML API
    include_once( WP_PLUGIN_DIR . '/sitepress-multilingual-cms/inc/wpml-api.php' );

    if( !isset( $parameters[ 'term_id' ] ) || !is_numeric( $parameters[ 'term_id' ] ) ) {
        return new WP_Error( 'invalid_translation_parameters', 'Parameter "term_id" is required for the translation type.');
    }

    /* @var $term WP_Term */
    $term = get_term( $parameters[ 'term_id' ] );
    if ( $term instanceof WP_Error ) {
        return $term;
    }

    $termId = get_term_field( 'term_id', $term );
    $taxonomyName = get_term_field( 'taxonomy', $term );
    
    //Set the term name or copy from source language
    $termTranslatedName = $parameters[ 'name' ] ? sanitize_text_field( $parameters[ 'name' ] ) : get_term_field( 'name', $term ) .' ('. strtoupper( $langDest ).')';
    //Set the term description or copy from source language
    $termTranslatedDescription = $parameters[ 'description' ] ? wpautop( wp_kses_post( $parameters[ 'description' ] ) ) : get_term_field( 'description', $term );
    
    // Check if translated term already exists
    if ( !is_null( wpml_object_id_filter( $termId, $taxonomyName, false, $langDest ) ) ) {
        return new WP_Error( 'termt_exists', ucfirst( $taxonomyName ) . ' "'. get_term_field( 'name', $term ) .'" translated to "'. $langDest .'" already exists!' );
    }

    // Insert translated term
    $termData = wp_insert_term( $termTranslatedName, $taxonomyName, array( 'description' => $termTranslatedDescription ) );
    if ($termData instanceof WP_Error) {
        return $termData;
    }

    // Set term custom fields
    foreach ( $parameters[ 'custom_fields' ] as $customFieldName ) {
        $customFieldValue = get_term_meta( $termId, $customFieldName, true );
        
        if ($customFieldValue) {
            add_term_meta( $termData[ 'term_id' ], $customFieldName, $customFieldValue );
        }
    }

    // Get trid of original term
    $trid = wpml_get_content_trid( 'tax_' . $taxonomyName, $termId );
    
    // Associate original term and translated term
    global $wpdb;
    $wpdb->update( $wpdb->prefix.'icl_translations', array( 'trid' => $trid, 'language_code' => $langDest, 'source_language_code' => $langSource ), array( 'element_id' => $termData[ 'term_id' ], 'element_type' => 'tax_' . $taxonomyName ) );

    // Return translated term
    return get_term( $termData[ 'term_id' ] );
}

This function creates a new term in the target language and associate that with the source term.

Step 5: Translate string

WPML scans strings from themes and plugins and add them as source string in the database. I wanted to add the option to translate those strings as well using the API. This function does just that. This function also requires an array in the params parameter.

Parameters

Values that can be sent within the params parameter field are these:

  • context
    – The domain of the string (e.g. wordpress).
  • string_source
    – The string in the source language.
  • string_dest
    – The translated string in the target language.

The mysite_translate_string function descrption is like this:

function mysite_translate_string( $langSource, $langDest, $parameters )
{
    global $wpdb;
    
    //Hardcode translated status
    $statusTranslated = '10';

    $stringResults = $wpdb->get_results( $wpdb->prepare( "SELECT id, status FROM {$wpdb->prefix}icl_strings WHERE value=%s AND language=%s AND context=%s AND status!=%s", $parameters[ 'string_source' ], $langSource, $parameters[ 'context' ], $statusTranslated ), ARRAY_A );
    if ( !count( $stringResults ) ) {
        return new WP_Error( 'string_not_fount', 'Untranslated string "'. $parameters['string_source'] .'" with context "'. $parameters['context'] .'" and language "'. $langSource .'" not found!' );
    }

    foreach ( $stringResults as $stringRow ) {
        $stringTranslatedId = $wpdb->insert( $wpdb->prefix . 'icl_string_translations', array( 'string_id' => $stringRow[ 'id' ], 'language' => $langDest, 'status' => $statusTranslated, 'value' => sanitize_text_field( $parameters[ 'string_dest' ] ) ) );
        if ( !is_numeric( $stringTranslatedId ) ) {
            return new WP_Error( 'string_translation_failed', 'String "'. $parameters[ 'string_source' ] .'" with context "'. $parameters[ 'context' ] .'" and language "'. $langSource .'" translation failed!' );
        }

        $wpdb->update( $wpdb->prefix . 'icl_strings', array( 'status' => $statusTranslated ), array( 'id' => $stringRow[ 'id' ] ) );
        if ( !is_numeric( $stringTranslatedId ) ) {
            return new WP_Error( 'string_translated_status_set_failed', 'String "'. $parameters[ 'string_source' ] .'" with context "'. $parameters[ 'context' ] .'" and language "'. $langSource .'" translated status set failed!' );
        }
    }
    
    return true;
}

String translation will return true when successful otherwise an WP_ERROR will be returned.

This article in WPML documentation helps understand the table structure used by the WPML plugin.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.