Wednesday, October 16, 2019

Translation in PHP using class constants

Translation in PHP frameworks is done in several different ways, but in all of them (?) the source text is a string. The translations are stored in CSV files, .mo files or in PHP arrays.

I propose to store them in class constants. This makes loading translations faster and creates a better integration with the IDE.

Translation classes

In this proposal a translation source file looks like this:

class CatalogTranslator extends Translator
{
    const price = "Price";
    const name = "Name";
    const nTimes = "%s times";
}

Each module has its own source file. This one belongs to the module Catalog. Note that we now have a source (i.e. nTimes) and a default translation ("%s times"in English, in this application).

The translation file (for the main German locale) is like this:
class CatalogTranslator_de_DE extends CatalogTranslator
{
    const price = "Preis";
    const nTimes = "%s Mal";
}
Note that the translation file extends the source file. This translation here has two translations, but lacks a translation for "name". That's ok; the value is inherited from its base class.

Translation calls

A function that needs translation functionality starts by creating an instance of a translation class for this module:
$t = CatalogTranslator::resolve();
And here is an example of an actual translation call:
$name->setLabel($t::name);
There it is: $t::name. It is a constant from a class that was resolved by the CatalogTranslator::resolve() call. When the request's current locale is de_DE, the class will be CatalogTranslator_de_DE; when the locale is fr_FR it will be CatalogTranslator_fr_FR. Which of the classes is selected is determined by the resolution function resolve.

Translator class resolution

Did you notice that CatalogTranslator extends the class Translator? This base class contains the static resolve function that determines the runtime translation class:
class Translator
{
    /**
     * @return $this
     */
    public static function resolve() {
        return $xyz->getTranslator(static::class);
    }
}
The function returns $this, which tells the IDE that an object of class CatalogTranslator is returned. A subclass of it, in fact.

$xyz stands for some object in your framework that defines the getTranslator function. Important is only that the runtime class (static::class) is passed to determine the name of the active translation class.

The function getTranslator looks like this:
class $Xyz
{
    protected $translatorClasses = [];

    public function getTranslator(string $class): Translator
    {
        if (!array_key_exists($class, $this->translatorClasses)) {
            $locale = $xyz->getLocale();
            $translatorClass = $class . '_' . $locale;
            $this->translatorClasses[$class] = new $translatorClass();
        }
        return $this->translatorClasses[$class];
    }
}
As you can see the resolver fetches the active locale from the request (again your framework will have a different approach) and simply append it to the runtime classname of the translation:

CatalogTranslator + de_DE = CatalogTranslator_de_DE

Advantages and disadvantages

An advantage of this approach is that the translations do not need to be loaded in memory each new request. They already reside in the opcache. Another is that translations are clearly scoped to the module they are defined in. No naming conflicts with other modules. Further, this approach is IDE friendly. Just control-click on the constant and you are in the source file. In this file you can find out where this source text is used (Find Usages) and which translations there are (Is overridden) using standard IDE functionality.

Disadvantages: the use of PHP constants to store translations will not be familiar to developers and translation agencies. It is easier to just type a source text directly in source code than to have to define a constant. Also, module-scoped translations mean that you may have to translate the same text several times in different modules. But if this is your only concern, you can adapt the approach into a single set of system-wide translation classes.



On SQLAlchemy

I've been using SQLAlchemy and reading about it for a few months now, and I don't get it. I don't mean I don't get SQLAlchem...