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
, post
, category
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