Grid background with CSS

Need to create a grid-like background? I recently needed one for an image drop-zone. It’s pretty simple to generate a grid background using the linear-gradient feature of CSS.

Simulated on CodePen

The CSS lines to notice here is this:

background-image: linear-gradient($white .4rem, transparent .4rem), linear-gradient(90deg, $white .4rem, transparent .4rem);
background-size: 10rem 10rem;

Here we’ve added two gradient backgrounds. Each linea-gradient creates a .4rem white line. And when we set the background-size to 10rem, those lines start repeating. We used two linear-gradient to create one in the horizontal orientation and one in the vertical orientation.

Traffic Simulation

Traffic Simulation with HTML/CSS

Here is how to create a traffic simulation using HTML/CSS …and a pinch of JavaScript. I used CSS3 animations, vehicle images from freepik and jQuery.

First, let’s create the road where the vehicles will run. We’ll add three lanes where one will be reserved for priority vehicles and public transports. On each lane, we’ll add some vehicles and a counter to show how many vehicles passed.

<div class="road-container" id="road">
    <div class="lane regular lane-1" id="lane-1">
        <div class="vehicle car"></div>
        <div class="vehicle truck"></div>
        <div class="vehicle-count">
            vehicles: <span id="lane-1-count">2</span>
        </div>
    </div>
    <div class="lane regular lane-2" id="lane-2">
        <div class="vehicle car"></div>
        <div class="vehicle car car-3"></div>
        <div class="vehicle car car-5"></div>
        <div class="vehicle-count">
            vehicles: <span id="lane-2-count">3</span>
        </div>
    </div>
    <div class="lane priority lane-3" id="lane-3">
        <div class="vehicle car emergency police"></div>
        <div class="vehicle car taxi"></div>
        <div class="vehicle bus"></div>
        <div class="vehicle-count">
            vehicles: <span id="lane-3-count">3</span>
        </div>
    </div>
</div>

Now that we have the basic HTML structure let’s add some CSS styles for the road and the vehicles. The roads will have a background with label displayed in the middle. I’m using SCSS format here.

.road-container {
  display: flex;
  flex-direction: column;
  height: 60vh;
  min-height: 350px;
  width: 100%;
  align-items: stretch;
  justify-content: stretch;
  border-top: 10px solid #81bf85;
  border-bottom: 10px solid #81bf85;
  overflow: hidden;
  max-width: 1920px;
  margin: 0 auto;

  .lane {
    box-sizing: border-box;
    min-height: 110px;
    height: 33.33%;
    background: #212121 url('../images/lane-border.png') repeat-x bottom left;
    display: flex;
    align-items: center;
    position: relative;

    &::before {
      content: 'NORMAL LANE';
      position: absolute;
      left: 0;
      right: 0;
      top: calc(50% - 1.5rem);
      font-size: 3rem;
      line-height: 3rem;
      font-weight: 600;
      color: rgba(255, 255, 255, 0.7);
      text-align: center;
    }

    &.lane-2 {
      &::before {
        content: 'NORMAL LANE';
      }
    }

    &.priority {
      &::before {
        content: 'PRIORITY LANE';
      }
    }

    &:last-child {
      background-image: none;
    }

    &.priority {
      background-color: #994424;
    }
  }
}

Now some styles for all the vehicles running on the lanes.

.vehicle {
  margin: 15px;
  height: 80px;
  min-width: 80px;
  background-position: center center;
  background-size: contain;
  background-repeat: no-repeat;
  position: absolute;
  left: -200px;
  top: calc(50% - 60px);

  &.car {
    background-image: url('../images/car-1.svg');

    @for $i from 1 through 7 {
      &.car-#{$i} {
        background-image: url('../images/car-#{$i}.svg');
      }
    }

    &.police {
      background-image: url('../images/police.svg');
    }

    &.ambulance {
      background-image: url('../images/ambulance.svg');
    }

    &.taxi {
      background-image: url('../images/taxi.svg');
    }
  }

  &.bus {
    background-image: url('../images/bus.svg');
    min-width: 120px;
  }

  &.truck {
    background-image: url('../images/truck.svg');
    min-width: 170px;
  }
}

Notice that we have made sure all the vehicles are out of the sight when initiated. The vehicle type truck has the maximum width of 170px and we have set the initial left position to -200px for all vehicles. This is how the lanes should look like at this point:

Lanes for the vehicles to run!

Let’s add some animation to the vehicles to make them move on the lanes.

@keyframes move {
  to {
    transform: translateX(2200px);
  }
}

@keyframes move-bus {
  40%, 100% {
    transform: translateX(2200px);
  }
}

It’s a pretty simple animation. We want the vehicles to start moving and go out of the viewing area, which we set to 1920px earlier. We added a different animation for the buses called move-bus to simulate a schedule (e.g., arriving every 15 minutes). Buses will only run on the priority lane and will show up at certain intervals.

Let’s add these animations to the vehicles and lanes. We’ll add following line to the .vehicles class to animate the vehicles.

animation: move 10s infinite linear;

And following two lines to the .vehicles.bus class to add special animation for the buses.

animation-name: move-bus;
animation-duration: 25s!important;

At this point, vehicles are moving, but we have an issue!

We’ve got movement!

Since all the vehicles are using the same animation speed, those are overlapping each other. We need to add some delay to the vehicles based on their position. We’ll add following styles inside .lane style.

.vehicle {
animation-duration: 10s;

@for $i from 1 through 30 {
&:nth-child(#{$i}) {
animation-delay: #{0.83 * ($i - 1)}s;
}
}
}

The above style will handle up to 30 vehicles per lane. But in practice, we may not have more than 17 vehicles because the road container width is set to 1920px and each vehicle requires 80px + 15px + 15px = 110px minimum width.

Also, where did we get the 0.83 second duration? From the styles for animation keyframes, we see that the animation starts at -200px and continues to 2200px, a total of 2400px. And from the 10s animation duration, we can approximate that each vehicle will pass approximately 200px within 0.83s. For the sake of simplicity, we considered each vehicle with same 200px width.

Now the animation looks pretty decent.

Vehicles are moving and respecting each other’s space!

Now we’ll try to make it a bit fancy.
First, we’ll add a few more vehicles randomly using jQuery.
Second, we’ll add slow-lane and fast-lane simulation. And when the vehicle density is at 100% we’ll simulate traffic congestion.
Finally, we’ll add a script to increase the vehicle count each time one passes out of the screen.

With all that implemented the simulation should look like this:

Vehicles with some congestion!

See the simulation in action here. The complete source code is available at GitHub.

How to add a virtual page in WordPress?

There are many use cases for a virtual page. Let’s say showing the invoice or payment status page for a payment plugin in the front-end, without compromising active theme. I needed one recently to show payment statuses. Here is how I implemented it.

1. Hook “init” to catch the specific slug

First we need to check current page slug to see if that is our target page inside the init hook.

class My_Virtual_Page {
    public function __construct() {
        add_action( 'init', array( $this, 'init' ) );
    }

    /**
     * Hook to add the virtual page
     */
    public function init() {
       if ( get_option( 'permalink_structure' ) ) {
            $param = trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
        } else {
            parse_str( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ), $params );
            $param = ( isset( $params['page_id'] ) ? $params['page_id'] : false );
        }

        if( $param == 'my-payment-page' ) {
            //Enqueue any page specific styles and scripts here
            add_filter( 'the_posts', array( $this, 'my_virtual_payment_page' ) );
        }
    }
}

Let’s say our virtual page is my-payment-page. So, the expected URL is either mydomain.com/my-payment-page or mydomain.com/?page_id=my-payment-page.

If we find our target parameter inside the init hook, we en-queue any required styles and scripts and add a filter for the content.

Note: The “slug” have to be unique to avoid conflict with any existing page.

2. Prepare the page object

Inside the filter, we prepare the content of the virtual page. Its pretty straight forward. We create a basic post object and fill the object with the custom values we want to display.

class My_Virtual_Page {
    /*........*/
    /**
     * @param $posts
     * The virtual page filter
     *
     * @return array
     */
    function my_virtual_payment_page( $posts ) {
        global $wp, $wp_query;

        //Just double checking. Can be ignored.
        if ( strcasecmp( $wp->request, 'my-payment-page' ) !== 0 ) {
            return $posts;
        }

        $content = '<p>HTML Page content here!</p>';

        $post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );
    }

    /**
     * @param $slug
     * @param $title
     * @param $content
     *
     * Generate the post object dynamically
     *
     * @return stdClass
     */
    function swp_post_object( $slug, $title, $content ) {
        $post                        = new stdClass;
        $post->ID                    = -1;
        $post->post_author           = 1;
        $post->post_date             = current_time( 'mysql' );
        $post->post_date_gmt         = current_time( 'mysql', 1 );
        $post->post_content          = $content;
        $post->post_title            = $title;
        $post->post_excerpt          = '';
        $post->post_status           = 'publish';
        $post->comment_status        = 'closed';
        $post->ping_status           = 'closed';
        $post->post_password         = '';
        $post->post_name             = $slug;
        $post->to_ping               = '';
        $post->pinged                = '';
        $post->modified              = $post->post_date;
        $post->modified_gmt          = $post->post_date_gmt;
        $post->post_content_filtered = '';
        $post->post_parent           = 0;
        $post->guid                  = get_home_url( 1, '/' . $slug );
        $post->menu_order            = 0;
        $post->post_tyle             = 'page';
        $post->post_mime_type        = '';
        $post->comment_count         = 0;

        return $post;
    }
}

3. Display the page

Finally we display the post by returning the post object. Before doing that, we need to set the flags of $wp_query to simulate that a post is found.

class My_Virtual_Page {
    /*........*/
    function my_virtual_payment_page( $posts ) {
        /*.......*/
        $post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );

        // set filter results
        $posts = array( $post );

        // reset wp_query properties to simulate a found page
        $wp_query->is_page     = true;
        $wp_query->is_singular = true;
        $wp_query->is_home     = false;
        $wp_query->is_archive  = false;
        $wp_query->is_category = false;
        unset( $wp_query->query['error'] );
        $wp_query->query_vars['error'] = '';
        $wp_query->is_404              = false;

        return ( $posts );
    }
    /*........*/
}

Putting it all together

class My_Virtual_Page {
    public function __construct() {
        add_action( 'init', array( $this, 'init' ) );
    }

    /**
     * Hook to add the virtual page
     */
    public function init() {
       if ( get_option( 'permalink_structure' ) ) {
            $param = trim( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
        } else {
            parse_str( parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ), $params );
            $param = ( isset( $params['page_id'] ) ? $params['page_id'] : false );
        }

        if( $param == 'my-payment-page' ) {
            //Enqueue any page specific styles and scripts here
            add_filter( 'the_posts', array( $this, 'my_virtual_payment_page' ) );
        }
    }

    /**
     * @param $posts
     * The virtual page filter
     *
     * @return array
     */
    function my_virtual_payment_page( $posts ) {
        global $wp, $wp_query;

        //Just double checking. Can be ignored.
        if ( strcasecmp( $wp->request, 'my-payment-page' ) !== 0 ) {
            return $posts;
        }

        $content = '<p>HTML Page content here!</p>';

        $post = $this->my_virtual_post_object( 'my-payment-page', __( 'Virtual payment page', 'domain' ), $content );

        // set filter results
        $posts = array( $post );

        // reset wp_query properties to simulate a found page
        $wp_query->is_page     = true;
        $wp_query->is_singular = true;
        $wp_query->is_home     = false;
        $wp_query->is_archive  = false;
        $wp_query->is_category = false;
        unset( $wp_query->query['error'] );
        $wp_query->query_vars['error'] = '';
        $wp_query->is_404              = false;

        return ( $posts );
    }

    /**
     * @param $slug
     * @param $title
     * @param $content
     *
     * Generate the post object dynamically
     *
     * @return stdClass
     */
    function swp_post_object( $slug, $title, $content ) {
        $post                        = new stdClass;
        $post->ID                    = -1;
        $post->post_author           = 1;
        $post->post_date             = current_time( 'mysql' );
        $post->post_date_gmt         = current_time( 'mysql', 1 );
        $post->post_content          = $content;
        $post->post_title            = $title;
        $post->post_excerpt          = '';
        $post->post_status           = 'publish';
        $post->comment_status        = 'closed';
        $post->ping_status           = 'closed';
        $post->post_password         = '';
        $post->post_name             = $slug;
        $post->to_ping               = '';
        $post->pinged                = '';
        $post->modified              = $post->post_date;
        $post->modified_gmt          = $post->post_date_gmt;
        $post->post_content_filtered = '';
        $post->post_parent           = 0;
        $post->guid                  = get_home_url( 1, '/' . $slug );
        $post->menu_order            = 0;
        $post->post_tyle             = 'page';
        $post->post_mime_type        = '';
        $post->comment_count         = 0;

        return $post;
    }
}

That’s it. Enjoy!

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.

Upload media via WordPress REST API

WordPress REST API is quite interesting especially when you are trying to update the website from some third-party resources. Recently I had to implement a similar feature where most of the things like custom post type, taxonomy etc. worked as they should except images. So, here I’m going to show how I made the media upload part work.

Although the media upload documentation says it should work with a single request, for some mysterious reason, it didn’t. I had to break the request into two parts.

Part 1: Upload the file

First I uploaded the image using a POST request. I only provided the image path in this request without any additional data.

//The image path in file system
$image_path = path/to/images/test-image.jpg;
//Upload the image
$uploaded_image = upload_image( $image_path );

I added CURL requests within the upload_image function.

function upload_image( $path ) {
    $request_url = 'http://websiteurl.com/wp/v2/media';

    $image = file_get_contents( $path );
    $mime_type = mime_content_type( $path );

    $api = curl_init();

    //set the url, POST data
    curl_setopt( $api, CURLOPT_URL, $request_url );
    curl_setopt( $api, CURLOPT_POST, 1 );
    curl_setopt( $api, CURLOPT_POSTFIELDS, $image );
    curl_setopt( $api, CURLOPT_HTTPHEADER, array( 'Content-Type: ' . $mime_type, 'Content-Disposition: attachment; filename="' . basename($path) . '"' ) );
    curl_setopt( $api, CURLOPT_RETURNTRANSFER, 1 );
    curl_setopt( $api, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
    curl_setopt( $api, CURLOPT_USERPWD, USERNAME . ':' . PASSWORD );

    //execute post
    $result = curl_exec( $api );

    //close connection
    curl_close( $api );

    return json_decode( $result );
}

The Content-Type and Content-Description values are very important while uploading image/media. Without appropriate mime type, the upload request will fail.

Part 2: Update media information

Once the image was successfully uploaded without any title, meta etc. I sent a second request to update the media.

/**
 * Let's update the image caption and other fields
 */
$fields = array(
    "date"          => date( 'Y-m-d H:i:s', $time ),
    "status"        => "publish",
    "title"         => "An interesting title for the image",
    "description"   => "Description of the image",
    "alt_text"      => "Images should have alt value",
    "caption"       => "Do not forget caption"
);
//Send the previously uploaded image ID in parameter
$updated_image = update_image_info( $uploaded_image->id, $fields );

The update_image_info function is pretty much the same as upload_image function.

function update_image_info( $id, $data = array() ) {
    $request_url = 'http://websiteurl.com/wp/v2/media/' . $id;

    $fields_string = json_encode( $data );

    $api = curl_init();

    //set the url, POST data
    curl_setopt( $api, CURLOPT_URL, $request_url );
    curl_setopt( $api, CURLOPT_POST, count($data) );
    curl_setopt( $api, CURLOPT_POSTFIELDS, $fields_string );
    curl_setopt( $api, CURLOPT_HTTPHEADER, array( 'Content-Type: application/json' ) );
    curl_setopt( $api, CURLOPT_RETURNTRANSFER, 1 );
    curl_setopt( $api, CURLOPT_HTTPAUTH, CURLAUTH_BASIC );
    curl_setopt( $api, CURLOPT_USERPWD, USERNAME . ':' . PASSWORD );

    //execute post
    $result = curl_exec( $api );

    //close connection
    curl_close( $api );

    return json_decode( $result );
}

Notice that the Content-Type is different this time. That’s it. The $updated_image variable has the detail of newly uploaded media.

I’ve used basic authentication in the example for simplicity. But there are several other authentication methods available. Here is an interesting read about implementing OAuth2 with WP API.