Skip Navigation
Originally written
January 15, 2014
July 23, 2019


This tutorial explains how to build multilingual-ready listing and directory websites using WordPress and WPML.

In this tutorial, we will use our Toolset Classifieds reference theme. It’s a fully featured, multilingual-ready, directory website, built using Toolset plugins.

There are also a number of other directory themes that you can use including GeoPlaces5 which contains location-based features for city directories.

You can find out all the compatible themes with WPML in our list of multilingual-ready themes.

What does it mean to have a multilingual listings site?

Listing, directory and all sorts of “marketplace” websites offer visitors to create content from the site’s front-end. That content is the listings, which other visitors will be looking for.

When we talk about multilingual listing sites, we mean that:

  1. Visitors can switch the language, in which they are browsing the site.
  2. The site’s structure, navigation, and all ‘hard coded’ texts will appear in the correct language.
  3. Listings that are translated will appear in the current language.
  4. Untranslated listings will appear in their original language.

With these four ground rules, we can develop our multilingual listing sites!

English homepage French homepage

Everything in English
Everything in English

Structure in French, user content in original English
Structure in French, user content in original English

Structure of the site

As we are talking about listing sites, we will need to store the listing items and organize them. For this, we will use custom post types and custom taxonomy.

  • Listings – custom posts
  • Listing categories – custom taxonomy

We want to allow visitors to write their own listings, without going through the WordPress admin. The site admin will set up the listing categories. Visitors will write ‘listings’, which belong to these categories.

What gets translated and what gets duplicated

WPML’s way of displaying untranslated content is by duplicating it to all languages. This means that visitors see the same text, but from a different entry in the database, depending on the selected language.

Why is this a good idea? Because each piece of content belongs to just one language. The site will display everything in the correct language, even if the content is not really translated.

Since the categories are really part of the site’s structure, the site admins should translate them. Admins should translate everything that they create. This includes the pages, strings, and categories.

Visitors will enter their listings and the site will automatically duplicate them to all languages. Then, if the visitor wishes to translate a listing to a certain language, the duplicated copy will get overwritten with the proper translation.

Duplicating listings to all languages after front-end submission

CRED form for creating a new ad
CRED form for creating a new ad

Toolset Classifieds uses Forms to create listings in the front-end. Toolset Forms is a plugin that lets you build forms for creating and editing WordPress content from front-end pages. Toolset Forms comes with an extension called Toolset Forms Commerce, which also allows charging payments for creating content. We need this so that we can charge members for “premium” ads.

We use the Toolset Forms Commerce hook cred_commerce_after_notifications to duplicate the listings, right after they are created.

Here is the code that does this:

add_action('cred_commerce_after_send_notifications', 'classifieds_cred_commerce_after_send_notifications',10,1);

This hook allows us to check the status of the WooCommerce order associated to our listing and only when the status is completed, to trigger the duplication process.

This is the code that we run after listings are created:

//duplicate posts created with CRED
function classifieds_cred_commerce_after_send_notifications($data){
    if (isset($data['new_status']) && 'completed' == $data['new_status']) {
        if (isset($data['cred_meta'][0]['cred_post_id']) && isset($data['cred_meta'][0]['cred_form_id'])) {
            $cred_form_id = get_post($data['cred_meta'][0]['cred_form_id']);
            // if a specific form
            $evaluate_cred_form_slug = $cred_form_id->post_name;
            switch ($evaluate_cred_form_slug) {
                case 'add-new-free-ad':
                case 'add-new-premium-ad':

This function does the following:

  1. Check that the order is completed (we only want to duplicate listings that are ready to go live)
  2. Check that we have all needed arguments (if something is missing, avoid a PHP error)
  3. Load the post (get_post…).
  4. Check that we are submitting a new ad.
  5. If so, duplicate the ad to all languages – the classifieds_duplicate_on_publish function does this.

This action hook will be executed after the notification has been sent and on order status change. It accepts one parameter $data. $data is an associative array that holds the form/post/product combinations and information about the WooCommerce order associated.

   ‘order_id’ => WooCommerce Order ID associated to the buying process
   ‘previous_status’=> the initial status of the order, usually ‘pending’
   ‘new_status’=> the new status of the order direct consequence of the payment. It expects one of the following options ('pending', 'failed', 'processing', 'completed', 'on-hold', 'cancelled', 'refunded' )
   'cred_meta'=>array( array(
      'cred_product_id'=> Product ID that was associated with the form and payment,
      'cred_form_id'=> ID of CRED Commerce form that customer used,
      'cred_post_id'=> Post ID that was created/edited with the form

When we use $data[‘new_status’] we are checking for the status of the WooCommerce order and will call the duplication function if the order status is ’completed’.

Full details about this filter are available in the Toolset Forms Commerce API documentation.

The actual duplication is managed by WPML, and we are using a built-in function that we can call upon to build the duplicated posts. If you are writing your custom directory sites, you can use this function, in WPML, to duplicate content to different languages.

function icl_makes_duplicates( $master_post_id ) {
    global $sitepress, $iclTranslationManagement;
    if ( !isset( $iclTranslationManagement ) ) {
        $iclTranslationManagement = new TranslationManagement;
    $post_type = get_post_type( $master_post_id );
    if ( $sitepress->is_translated_post_type( $post_type ) ) {
        $iclTranslationManagement->make_duplicates_all( $master_post_id );

This function is available from the WPML Coding API. The $master_post_id is the ID of the post to duplicate. If duplicates already exist, they will be updated. Otherwise, if duplicates don’t exist, they will be created. The master post can be in any language and will be duplicated to the other languages in the site.

Our directory website should work with and without WPML active, so before we call icl_makes_duplicates, we check if the function exists.

//duplicate a post/page/custom post on publish or update
function classifieds_duplicate_on_publish ($post_id) {

Translating ads (replacing the duplicates)

Of course, the duplicate ads that we’ve created are separate database entries. This makes it easy to translate some ads to some languages. When we do that, we just need to unset the flag which tells WPML that this content is duplicate. Unsetting this flag is very important. If we don’t do it, WPML will overwrite our translations, every time the original ad is updated.

This is what we do:

  1. Use a Toolset form to edit the ads in different languages.
  2. Set up another Toolset Forms hook to unset the duplicate flag, after manually translating ads.

WPML’s icl_makes_duplicates API function verifies that the translations are duplicates. If so, it will overwrite them when the original changes. Otherwise, the translations are left unchanged.

We use another Toolset Forms hook, cred_save_data, to unset the duplicate status, after manually translating ads. We wrote the function cred_save_data_message_action() and connected it to that hook.

add_action('cred_save_data', 'cred_save_data_message_action',10,2);

function cred_save_data_message_action($post_id, $form_data){
    global $post;    
    $cred_form_id = get_post($form_data['id']);
    // if a specific form
    switch ($evaluate_cred_form_slug) {
        case 'edit-product':

This function does the following:

  1. Check that this is indeed a product-edit form.
  2. Call classifieds_reset_duplicate_flag to reset the duplicate flag for the post that’s been edited.

This hook is also detailed in the Toolset Forms API documentation.

The classifieds_reset_duplicate_flag calls TranslationManagement::reset_duplicate_flag, which will unset the duplicate status for the translated post, and allow it to run independently of the original ad.

 function classifieds_reset_duplicate_flag($post_id) {
    global $sitepress, $iclTranslationManagement;
    if (is_object($sitepress)) {
        if ('TranslationManagement' !== get_class($iclTranslationManagement)) {
            require_once( ICL_PLUGIN_PATH . '/lib/icl_api.php' );
            require_once( ICL_PLUGIN_PATH . '/lib/xml2array.php' );
            require_once( ICL_PLUGIN_PATH . '/inc/translation-management/translation-management.class.php' );
            $iclTranslationManagement = new TranslationManagement();

This function does:

  1. Make sure that $sitepress object and the TranslationManagement class are available.
  2. Load the needed libraries.
  3. Calls the reset_duplicate_flag function.

Note: this is a temporary solution. The above code will go into WPML soon and we will use an API function for this process.