Localizing PHP application with FBT instead of standard i18n

Localizing PHP application with FBT instead of standard i18n

Written by Richard Dobroň on Jul 19th, 2022 Views Report Post

Since about 2010, I have been looking for a translation framework for PHP that can generate very complex phrases and at the same time combine options such as singular/plural and work with genders (male, female, unknown) and ideally also format numbers according to the standards of the given country or region — unsuccessfully. In 2018, Facebook released FBT — an open source localization framework that provides a more efficient way of defining content for flexible and high-quality localization. I didn’t hesitate at all. I gradually started to rewrite this JavaScript version into PHP, and even if a few small things were not entirely according to the original code, I succeeded. It was my challenge that I finished in about 40 days (better said, evenings 😊).

There are many reasons why the common i18n libraries are very inadequate. The main ones are:

  • misunderstanding of the native text by the translator (does not know the context),
  • insufficient/nonsensical code combinations for translation,
  • do not support features like enums or pronouns,
  • and many more...

Fortunately, FBT can solve all of this. However, it is not easy to use it for the first time when you are not familiar with it. But I will be happy to advise you on how to use it.

  1. step: install the FBT package
composer require richarddobron/fbt
  1. step: set your FBT configuration
<?php
require ("vendor/autoload.php");

\fbt\FbtConfig::set('author', 'your name/team');
\fbt\FbtConfig::set('project', 'project name');
\fbt\FbtConfig::set('path', '/path/to/storage/fbt');
  1. step: language and gender settings
  • If you just want to change the interface language, just use:
\fbt\FbtHooks::locale('sk_SK'); // app locale
  • If you have an application in which users log in, you can use the interface IntlViewerContextInterface:
<?php

namespace App;

use fbt\Transform\FbtTransform\Translate\IntlVariations;
use fbt\Lib\IntlViewerContextInterface;
use fbt\Runtime\Gender;

class UserDTO implements IntlViewerContextInterface
{
    public function getLocale(): ?string
    {
        return $this->locale;
    }

    public static function getGender(): int
    {
        if ($this->gender === 'male') {
            return IntlVariations::GENDER_MALE;
        }

        if ($this->gender === 'female') {
            return IntlVariations::GENDER_FEMALE;
        }

        return IntlVariations::GENDER_UNKNOWN;
    }
}

After implementation, set viewerContext:

$loggedUserDto = ...;

\fbt\FbtConfig::set('viewerContext', $loggedUserDto)
  1. step: prepare translations files
  • Facebook has devised their own system of labeling languages, you can find a list of them at this link.
  • From this list, choose the languages into which you want to translate your website or application.
  • I usually add them to the directory /storage/fbt/
  • File name will look like this: sk_SK.json
  1. step: add FBT scripts to composer.json
{
    ...,
    "scripts": {
        "translate-fbts": "php ./vendor/bin/fbt translate --path=./storage/fbt --translations=./storage/fbt/*.json",
        "generate-translations": "php ./vendor/bin/fbt generate-translations --source=./storage/fbt/.source_strings.json --translations=./storage/fbt/*.json"
    }
}
  1. step: add your texts
  • Native phrases are always expected in English, it might look something like this:
<?php
// simple text:
echo fbt('Save', 'Button: Save a form or settings');

// text with params:
$name = 'Patricia';
$gender = 2;

echo fbt(
  \fbt\fbt::name(
    'name',
     '<a href="#">' . $name . '</a>',
     $gender
   ) .
  ' shared a link.  Tell ' . \fbt\fbt::sameParam('name') . ' you liked it.',
  'Notification about sharing a link.'
);

// is same as:
?>

<?php fbtTransform(); ?>
	<fbt desc="param example">
	  <fbt:name name="name" gender="<?=$gender?>">
		<a href="#"><?=$name?></a>
	  </fbt:name>
	  shared a link.  Tell
	  <fbt:same-param name="name" />
	  you liked it.
	</fbt>
<?php endFbtTransform(); ?>
  1. step: translate collected texts

This is what the collected source strings look like in the .source_strings.json file after script execution:

{
    "phrases": [
        {
            "hashToText": {
                "77515026232eb24b14cc5e7cca878637": "Save"
            },
            "desc": "Button: Save a form or settings",
            "project": "tutorial app",
            "author": "richard",
            "type": "text",
            "jsfbt": "Save"
        },
        {
            "hashToText": {
                "11608ffd7ee5e79d727ab00631b2c164": "{name} shared a link. Tell you liked it."
            },
            "desc": "Notification about sharing a link.",
            "project": "tutorial app",
            "author": "richard",
            "type": "table",
            "jsfbt": {
                "t": {
                    "*": "{name} shared a link. Tell you liked it."
                },
                "m": [
                    {
                        "token": "name", // phrase token
                        "type": 1 // gender = 1, number = 2, pronoun = 3
                    }
                ]
            }
        }
    ],
    "childParentMappings": []
}

Now we are ready to generate a file in which we will write the translations for the collected source strings:

composer run generate-translations

The /storage/fbt/sk_SK.json file now contains the hash keys of the source phrases that we can now translate. I make translations into Slovak, so I can use the gender of the {name} token and adapt the variations of the given text precisely. This step is a bit difficult for some translations with variants, as you have to work with the variations key.

You can also find several types of translations at this link.

{
    "sk_SK": {
        "fb-locale": "sk_SK",
        "translations": {
            "77515026232eb24b14cc5e7cca878637": {
                "translations": [
                    {
                        "translation": "Uložiť",
                        "variations": []
                    }
                ],
                "tokens": [],
                "types": []
            },
            "11608ffd7ee5e79d727ab00631b2c164": {
                "translations": [
                    {
                        "translation": "{name} zdieľal odkaz. Dajte mu vedieť, že sa vám páči.",
                        "variations": [1] // 1 = male
                    },
                    {
                        "translation": "{name} zdieľala odkaz. Dajte jej vedieť, že sa vám páči.",
                        "variations": [2] // 2 = female
                    },
                    {
                        "translation": "Používateľ {name} zdieľal/a odkaz. Dajte mu vedieť, že sa vám páči.",
                        "variations": [3] // 3 = unknown/neutral gender
                    }
                ],
                "tokens": ["name"],
                "types": [3] // 3 = gender, 28 = number
            }
        }
    }
}

After completing the translations, we run the command that creates the translations for the application:

composer run translate-fbts

Finally, this command will generate a production file with translations using Jenkins hash.

{
    "sk_SK": {
        "2gTzp8": [
            "Uložiť",
            "77515026232eb24b14cc5e7cca878637" // phrase hash
        ],
        "16x4nE": {
            "1": [ // male
                "{name} zdieľal odkaz. Dajte mu vedieť, že sa vám páči.",
                "11608ffd7ee5e79d727ab00631b2c164"
            ],
            "2": [ // female
                "{name} zdieľal odkaz. Dajte jej vedieť, že sa vám páči.",
                "11608ffd7ee5e79d727ab00631b2c164"
            ],
            "*": [ // fallback
                "Používateľ {name} zdieľal odkaz. Dajte mu vedieť, že sa vám páči.",
                "11608ffd7ee5e79d727ab00631b2c164"
            ]
        }
    }
}

Your app is now translated! Hurray!

Thanks for reading 🙏.

Comments (1)