Proposal for handling translatable strings in WordPress

Proposal for handling translatable strings in WordPress

Introduction

Custom post types and taxonomy

Custom fields and 'option' entries

How to set language information

Example - announcing translatable texts in WooCommerce

API

Filters

translated_string

translation_action

Actions

dynamic_string

Functions

Configuration

Static strings

post-meta

options

Sample JSON file for WooCommerce

Dynamic strings

Storing settings

Implementation

Caveats

Introduction

This proposal is intended to make easier to develop multilingual-ready themes and plugins. As 98% of WordPress sites are single-language, our goal is to enable multilingual functionality, while avoiding complexity for single-language sites.

The intention is to allow themes and plugins to indicate what elements, which they create, will require translation on multilingual sites. Different multilingual plugins can use that information and implement the multilingual operation in whatever way they choose.

Custom post types and taxonomy

When running a multilingual site, custom posts and taxonomy can be 'translatable' or not. If items of a custom type are translatable, the user expects to see different content per language. In some cases, even if the site is multilingual, certain CPTs should be language-indifferent. For example, if a site offers 'downloads', they will probably be the same, no matter in what language visitors browse the site.

Custom fields and 'option' entries

Custom fields and 'options' are a bit more complex than CPTs. Custom fields may require translation, may need to be the same for all languages or should be language-indifferent.

How to set language information

Developers will have two options to indicate if elements require translation:

JSON files allow to pre-compile the language information for elements. These files offer zero overhead during runtime for single-language sites. When running in a multilingual site, the multilingual plugin will read these JSON files and 'know' what elements require translation. JSON files are best for 'static' elements, which the theme or plugin always create. For example, a real estate theme may create CPTs for 'properties' and some fields for properties. A JSON file in the theme folder will indicate that these elements require translation.

Filters are best for dynamic elements, which cannot be determined when developing the code. They add minimal overhead during runtime (to go through filters that nothing uses). A plugin that creates field according to user inputs will need to use these filters to indicate language information, as these fields are not known when coding the plugin.

Example - announcing translatable texts in WooCommerce

The following example, shows a configuration file for translating post meta and ‘option’ entries in WooCommerce.

This would be the JSON file for WooCommerce:

{

  "post-meta": [

      {

        "action": "copy",

        "name": "_backorders"

      },

      {

        "action": "copy",

        "name": "_pricing_rules"

      }

      {

        "action": "translate",

        "name": "_crosssell_ids"

      },

      ...

    ]

  ,

  "options": [

      { "name": "woocommerce_shop_page_title" },

      {

        "name": "woocommerce_new_order_settings",

        "key": [

          { "name": "subject" },

          { "name": "heading" }

        ]

      },

      ...

      { "name": "woocommerce_demo_store_notice" },

    ]

}

You can see that the custom fields ‘_backorders’ and ‘_pricing_rules’ need to be copied between languages. The field ‘_crosssell_ids’ is user-translatable.

We can implement the same using API calls, like these:

// Get the theme option

$cross_sell_ids = get_option( 'my_theme_footer_text' );

$cross_sell_ids = get_translated_string( 'option', 'my_theme_footer_text', $cross_sell_ids );

// Or...

$cross_sell_ids = apply_filters( 'translated_string', get_option( 'my_theme_footer_text' ), 'option', 'my_theme_footer_text' );

You can read the complete technical proposal in the following Google Doc:

https://goo.gl/Ynp1ek

API

Filters

translated_string

This filter will accept the following arguments and will return the filtered string (by the multilingual plugin):

translation_action

This filter will accept the following argument and will return the translation action:

Actions

dynamic_string

This action expect the following arguments:

The purpose of this action is to dynamically add more items to the static strings, for all these cases where is not possible to define in advance which strings needs to be translated[5].

Functions

The following functions, except one, will be just wrappers of the above filters and actions:

Configuration

Static strings

Core will look for a json file on each plugin load (“plugins_loaded”, or more appropriate hook).

Let's suppose this file is called wp-strings-config.json.

This file will contain this type of data:

post-meta

This part is pretty straight forwards: the array will tell WordPress what to do with a given post meta.

It's up to the multilingual plugin to handle the translation action:

options

Here I'm considering the case of options stored as serialized arrays or objects.

In order to allow translating only part of the array/object, we need to handle this setting in a recursive way.

Let's consider WooCommerce.

Among its options, there is “woocommerce_customer_invoice_settings”.

This is a serialized array and we want to translate the following keys:

To do so, we must tell WordPress to handle the “woocommerce_customer_invoice_settings” option, but only the above keys of it.

For the same reason, we may need to specify nested keys.

Sample JSON file for WooCommerce

{

  "post-meta": [

      {

        "action": "copy",

        "name": "_backorders"

      },

      {

        "action": "translate",

        "name": "_crosssell_ids"

      },

      {

        "action": "copy",

        "name": "_default_attributes"

      },

      {

        "action": "copy",

        "name": "_download_limit"

      },

      ...

      {

        "action": "copy",

        "name": "_pricing_rules"

      }

    ]

  ,

  "options": [

      { "name": "woocommerce_shop_page_title" },

      { "name": "woocommerce_email_footer_text" },

      {

        "name": "woocommerce_new_order_settings",

        "key": [

          { "name": "subject" },

          { "name": "heading" }

        ]

      },

      ...

      { "name": "woocommerce_demo_store_notice" },

      { "name": "woocommerce_price_thousand_sep" },

      { "name": "woocommerce_price_decimal_sep" },

      { "name": "woocommerce_price_display_suffix" },

      { "name": "woocommerce_email_from_name" },

      { "name": "woocommerce_email_from_address" }

    ]

}

This is a way for plugins to add the information, so multilingual plugins won't need to hard-code each translatable string through the dynamic strings approach.

Dynamic strings

Dynamic strings have the same attributes as static strings defined in the JSON file, but they are defined at runtime.

The 'dynamic_string' action (and it's wrapper 'add_translation_action' function) allows to do so.

This approach is the only possibility for handling “known” strings, when a json file is not provided.

Storing settings

WordPress will parse all the json files during plugin loading and store the data in a global $wp_strings_config variable (transient, or option).

It will update the information stored in $wp_strings_config every time we add new elements.

Implementation

The multilingual plugin will need to know if a given string is set to be translatable and then either render the translation, or create/update the translation.

The multilingual plugin can call get_all_translation_actions to know what can be translated and provide his own interface to handle these translation.

In order to render the translated post metas and options, the plugin must hook to the existing WP-Core API (filters) and, from there, call the new API, if needed.

For instance, when reading an option, plugins can:

  1. create a hook a function to each translatable option (by using get_all_translation_actions), so to hook to apply_filters( 'option_' . $option, maybe_unserialize( $value ) )
  2. The hooked function will then call the plugin's logic to translate the string[8]

Caveats

WordPress will need to handle duplicated configurations (either they are coming from different json files, or from dynamic strings).

I suggest two options:


[1] In case of an option, since it might be a serialized array/object, we may need to specify the subkey.

In order to so do, we can define a standard, such as option_name[key][subkey][...].

[2] See #1

[3] See #1

[4] “never-translate” might be redundant, but suppose the multilingual plugin wants to actually block any translation action for the element, “never-translate” would allow to do that. Omitted elements, instead, could be still set as translatable through the user interface (see Dynamic strings).

[5] e.g. a Custom Post Types plugin which allow to define post types, hence a bunch of strings such as labels, slug, etc.

[6] See Storing settings.

[7] See #4

[8] Please note that currently WP-Core does not provide a filter containing the information about which option is being filtered, therefore the only way to get this information is to write something like that:
$option_name = substr( current_filter(), 7 );
A change to
'option_' . $option filter, which add the option name as second argument is needed.