Skip to content
Anton Ukhanev edited this page Jun 9, 2020 · 16 revisions

WP i18n

Overview

The purpose of this package is to provide a concrete implementation of a translator, which uses the functionality of WordPress and wraps it with a standards-compliant OOP interface. This package does not aim to provide some alternative means of translation; instead, it aims to make translation more useful and usable.

Standards

The base standard used in this package is of dhii/i18n-interface, and classes in this package implement interfaces of the standard package. The fact that the classes in this package comply with dhii/i18n-interface means that they can be used anywhere, where the interfaces are required; see further below for explanation on de-coupling.

Benefits

WordPress provides a set of functions that can be used for i18n, and for many this seems like enough. This package brings certain benefits on top of the WordPress functionality.

  1. Standards-compliant. See "Standards" above.
  2. Avoid anti-patterns and code duplication. Centralize the text domain and other settings
  3. De-couple your code from WordPress and gettext.
  4. Use dependency injection, and other OOP patterns. Make your code SOLID.

In More Depth

One of the greatest advantages of programming in general, and OOP in particular, is the ability to re-use functionality. To be able to do that, functionality must be broken down into units. The smaller the units - the more granular the re-usability. The more they are de-coupled - the more possible the re-usability. This is Separation of Concerns, SoC, and is the S in SOLID.

Now, let's imagine that you are developing a module that does something very specific, as it should, and unrelated to WordPress: an OOP wrapper for an API. Making requests, processing of responses, and formatting them in a standard fashion - these are the things that have nothing to do with WordPress; and yet, there's nothing that should stop a developer from using it within WordPress. Nothing should stop the developer from taking those classes, and using them in their plugin to consume an API.

Now, let's also imagine a super-realistic scenario: you need to throw an exception of a specific type. Whether it is the generic Exception, a more specific OutOfRangeException, or your custom exception interface (preferable, anyway), it needs a message which would explain what went wrong, and this message must be useful. It has been established that text must be understandable in order to be useful, and this makes i18n a requirement. But how do you internationalize, if your module couldn't give two ships about where and how it is being used? How can it know whether it's in a plugin, and which plugin, or a theme, or if it's even in WordPress? If the module is helpful for consuming an API, it is just as helpful in Magento or Joomla as it is in WordPress. And ideally, your modules should always be that way. Great effort is being made by some members of the PHP community, including PHP-FIG members, to make this possible - such as Puli. But internationalization is not available to you in your module, because it would couple your module to WordPress, and to gettext. Solution? Depend on a standard via an interface, and dependency-inject a standard-compliant instance in your class; then use that instance to translate.

This would allow you to build platform-independent modules, write tests with ease and run them with confidence - something impossible with WordPress traditionally.

Usage

In your code that needs to internationalize, consume the appropriate interface, e.g. FormatTranslatorInterface.

Concrete Implementation

Call FormatTranslatorInterface#translate() directly on the concrete class.

$translator = new \Dhii\Wp\I18n\FormatTranslator(MY_TEXT_DOMAIN);
$string = 'Hello, %1$s!';
$name = 'Use Of Standards';
$translatedString = $translator->translate($string, [$name]);

Decoupled From Instance

Depending on how you acquire the $translator instance, this can couple your code to dhii/wp-i18n. Here's how you can use real OOP to solve that problem:

abstract class AbstractMyClass
{
    // ...

    /**
     * ...
     * @return \Dhii\I18n\FormatTranslatorInterface
     */
    abstract protected function _getTranslator();

    /**
     * @return string
     */
    public function getGreeting()
    {
        $translator = $this->_getTranslator();
        $string = 'Hello, %1$s!';
        $name = 'Correct Use Of OO Patterns';
        $translatedString = $translator->translate($string, [$name]);

        return $translatedString;
    }
}

De-coupled from interface

The above de-couples your abstract code from a specific implementation. You could go further and de-couple the abstract code from the interface as well:

abstract class AbstractMyClass
{
    // ...

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = array(), $context = null)

    /**
     * @return string
     */
    public function getGreeting()
    {
        $string = 'Hello, %1$s!';
        $name = 'Correct Use Of OO Patterns';
        $translatedString = $this->__($string, [$name]);

        return $translatedString;
    }
}

class MyClass extends AbstractMyClass
{
    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see sprintf()
     * @see _translate()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    function __($string, $args = array(), $context = null)
    {
        return $this->_getTranslator()->translate($string, $args, $context);
    }

    /**
     * Retrieves the translator associated with this instance.
     *
     * @since [*next-version*]
     *
     * @return FormatTranslatorInterface The translator.
     */
    protected function _getTranslator()
    {
        return $this->translator;
    }
}

Of course, it can get very tedious. At the very least, a large part of your classes will need internationalization, and declaring a getter/setter for translation, along with implementing a __() method, can get very tedious, not to mention that it increases code duplication. Specifically to remedy this situation, you can use the traits in dhii/i18n-helper-base.

abstract class AbstractMyClass
{
    // ...

    /**
     * Translates a string, and replaces placeholders.
     *
     * @since [*next-version*]
     * @see sprintf()
     *
     * @param string $string  The format string to translate.
     * @param array  $args    Placeholder values to replace in the string.
     * @param mixed  $context The context for translation.
     *
     * @return string The translated string.
     */
    abstract protected function __($string, $args = array(), $context = null)

    /**
     * @return string
     */
    public function getGreeting()
    {
        $string = 'Hello, %1$s!';
        $name = 'Correct Use Of OO Patterns';
        $translatedString = $this->__($string, [$name]);

        return $translatedString;
    }
}

class MyClass extends AbstractMyClass
{

    // ...

    use \Dhii\I18n\StringTranslatingTrait;
    use \Dhii\I18n\StringTranslatorConsumingTrait;

    public function __construct(\Dhii\I18n\FormatTranslatorInterface $translator)
    {
        // ...
        // Saving the translator instance for later use, to be returned by `_getTranslator()`
    }
}

Examples

By far, the most common real life example where this kind of abstraction is really necessary is when internationalizing exception messages in the abstract layer.

For example, AbstractRecursiveIteratorLocator uses the kind of abstraction described above to internationalize the message of the exception which occurs if the module config is not iterable. It does not care how the message is internationalized, with which mechanism, and in which environment; it only makes a contract that there is a __() method which promises to return a string with parameters interpolated.
The RecursiveIteratorLocator uses the StringTranslatorConsumingTrait -> StringTranslatingTrait, which implements the __() method, interpolating parameters, and delegating the translation itself to a _translate() method. The StringTranslatorConsumingTrait delegates translation to a translator returned by _getTranslator() . RecursiveIteratorLocator also uses the StringTranslatorAwareTrait, which implements the translator setter and getter, _getTranslator().
Finally, RecursiveIteratorLocator in its constructor accepts a StringTranslatorInterface, of which FormatTranslatorInterface is a subtype.

Thus, we have a class, which accepts a translator, and its inherited methods use this translator without knowing about that. The abstract implementation knows nothing about the translator; and the concrete implementation knows nothing about the translation method; yet the class is able to perform translation due to being injected with a dependency, thus achieving IoC. QED.