PHP Translation

How do you manage your multilanguage Symfony application?

This is something you know many companies do but nobody talks about how they do it. It might be because nobody is really proud of their solution. That is something we like to change. We want to share ideas, knowledge and tools with the PHP community.

This organization has some large building blocks that you should be aware of. First there is the Extractor that finds translation keys in any source file. Second we have the Symfony Bundle which is using the Extractor and puts a lot of great feature that will help your translation workflow. There are features like automatic translation, a Web UI, Edit-in-place that allows you to edit translations in the right context and there is also support for multiple local and remote storages.

Getting started

If you are using Symfony you should start by looking at the documentation for the Symfony bundle. If you are more hard core you may want to start by looking at the Organization overview.

Organization overview

There is quite a few repositories in this organisation. Here is a brief overview what all of them does.

Extractor

https://poser.pugx.org/php-translation/extractor/v/stable Total Downloads

This package include extractors that look at your source code and extract translation keys from it. We support extractor from PHP files, Twig files and Blade template files. Read more about the extractor.

Common

https://poser.pugx.org/php-translation/common/v/stable Total Downloads

Common interfaces and classes used by 2 or more packages. Read more about common.

Symfony Bundle

https://poser.pugx.org/php-translation/symfony-bundle/v/stable Total Downloads

The Symfony bundle integrates all these fancy features with Symfony. We have support for automatic translation, web UI, third party services and more. Read more about the bundle.

Translator

https://poser.pugx.org/php-translation/translator/v/stable Total Downloads

The translator package includes third party translation clients. Use this package if you want to translate a string with Google Translate, Yandex Translate or Bing Translate. Read more about translators.

Storage adapters

This organisation has plenty of storage adapters to support storing your translations on different third party services. All storages implement Translation\Common\Storage. The Symfony bundle allows you to use multiple storages.

Symfony storage
https://poser.pugx.org/php-translation/symfony-storage/v/stable Total Downloads

The Symfony storage stores translations on the local file system using Symfony’s writers and loaders. This storage is required by the Symfony bundle and should be considered as a “local cache”.

The Symfony storage also has a XliffConverter that converts a Catalogue to the contents of a Xliff file. It also supports the reverse action.

Flysystem
https://poser.pugx.org/php-translation/flysystem-adapter/v/stable Total Downloads

Do you use a remote filesystem for your translations? The Flysystem adapter is the adapter for you. It is a storage based on the excellent Flysystem by Frank de Jonge.

Loco
https://poser.pugx.org/php-translation/loco-adapter/v/stable Total Downloads

Use the power of Loco to manage your translations and give your translators access to a custom user interface. Loco is created by Tim Withlock.

Transifex
https://poser.pugx.org/php-translation/transifex-adapter/v/stable Total Downloads

An adapter for Transifex.

PhraseApp
https://poser.pugx.org/php-translation/phraseapp-adapter/v/stable Total Downloads

An adapter for PhraseApp.

How to use Loco Adapter

When your application has reached a certain number of languages and you can’t translate all of them yourself you need a translation platform to ease your work with external translators. This article shows how to set up a storage adapter using Loco. Loco is just an example here. All storage adapters have a similar way of being configured.

Installation

Assuming you have already installed the Symfony bundle, you need to find and install a storage adapter. See our list of storage adapters.

composer require php-translation/loco-adapter

The storage adapter does also contain a bundle which needs to be enabled.

<?php
// app/AppKernel.php

public function registerBundles()
{
    $bundles = array(
        // ...
        new Translation\PlatformAdapter\Loco\Bridge\Symfony\TranslationAdapterLocoBundle(),
    );
}

Configuration

Once the adapter bundle is installed you must configure it with your API keys. For Loco the configuration looks like this:

# /app/config/config.yml
translation_adapter_loco:
  projects:
    messages:
      api_key: 'foobar'
    navigation:
      api_key: 'bazbar'

When the storage adapter bundle is configured it will register a service with id php_translation.adapter.loco. Now we need to tell the TranslationBundle to use this adapter.

Note

The terminology for “adapters” in the context of the Symfony bundle is “storage”.

The TranslationBundle supports multiple storages. You can even use them at the same time. There are local storages and remote storages. One should consider the local storage as a cache. Which means that the absolute truth is always the remote storage. By default there is a local file storage which would be suitable for most applications.

Lets configure the bundle to use Loco as a remote storage.

translation:
    locales: ["en", "fr", "sv"]
    configs:
        app:
            dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"]
            output_dir: "%kernel.root_dir%/Resources/translations"
            remote_storage: ["php_translation.adapter.loco"]

Usage

You may use the TranslationBundle as you normally do. When you add new translations in the Symfony Profiler they will automatically be added in Loco. You can also run Symfony commands to upload and download translations to your remote storages.

Configure HTTPlug

If you are using the Symfony bundle you want to use HTTPlug. It is a great tool to decouple from the HTTP client. If you are new to HTTPlug you may want to read their introduction. The very easiest way of using HTTPlug is to install the HTTPlugBundle.

The standard configuration (no configuration) works for a common setup but you may want to add some configuration. If you want to add logging for all the requests and responses for a client named acme you may do:

httplug:
    plugins:
        logger: ~
    clients:
        acme:
            factory: 'httplug.factory.guzzle6'
            plugins: ['httplug.plugin.logger']
            config:
                timeout: 2
                # Set verify to false if somehow you need to disable SSL certificate check.
                # Beware, this should always be true (default value) in production:
                # verify: false

translation:
    http_client: 'httplug.client.acme'

Configure caching

When you are using the auto translation features you may want to cache the responses from your paid third party translator services. To configure HTTPlug to be aggressive for those request you need the CachePlugin and a PSR-6 cache pool.

# Using PHP-cache.com for PSR-6 cache.
cache_adapter:
    providers:
        my_redis:
            factory: 'cache.factory.redis'

httplug:
    plugins:
        logger: ~
        cache:
            cache_pool: 'cache.provider.my_redis'
            config:
                default_ttl: 94608000 # three years
                respect_response_cache_directives: [] # We cache no matter what the server says
    clients:
        translator:
            factory: 'httplug.factory.guzzle6'
            plugins: ['httplug.plugin.cache', 'httplug.plugin.logger']

translation:
    # ...
    http_client: 'httplug.client.translator'
    fallback_translation:
        service: 'google' # 'yandex' is available as an alternative
        api_key: 'foobar'

Note

See PHP-cache.com for information about caching.

Adding extractors

The extractor library is very SOLID which means that you easily can add extractors without changing existing code. There are some concepts to be aware of

The Extractor object has a collection of FileExtractor that are executed on files with a file type they support. The PHPFileExtractor and TwigFileExtractor are using the visitor pattern. They have a collection of Translation\Extractor\Visitor that will be executed for each file the FileExtractor is running for. To add a custom extractor for a custom PHP class you may only add a visitor for the PHPFileExtractor.

Note

Read more about the architecture at the component description of Extractor.

Example

This is an example of how you would extract the “foobar” from the following PHP script:

$this->translateMe('google', 'foobar');

First you need to create your visitor. Since it is a PHP file we do not need to add another FileExtractor.

use PhpParser\Node;
use PhpParser\NodeVisitor;
use Translation\Extractor\Model\SourceLocation;
use Translation\Extractor\Visitor\Php\BasePHPVisitor;

class TranslateMeVisitor extends BasePHPVisitor implements NodeVisitor
{
    public function enterNode(Node $node)
    {
        if ($node instanceof Node\Expr\MethodCall) {
            if (!is_string($node->name)) {
                return;
            }
            $name = $node->name;

            if ('translateMe' === $name) {
                $label = $this->getStringArgument($node, 1);

                $source = new SourceLocation(
                    $label,
                    $this->getAbsoluteFilePath(),
                    $node->getAttribute('startLine'),
                    ['domain' => 'messages']
                );
                $this->collection->addLocation($source);
            }
        }
    }

    // ...
}

Note

Refer to the documentation of nikic/PHP-Parser for more examples of note types.

Tests

This will work, but we need tests. Each extractor must be properly tested. We use functional tests for each visitor. Add test resources with scripts that you will use to test your visitor. Reusing test resources should be avoided.

By using the BasePHPVisitorTest class you can easily write test will little or no overhead.

class TranslateMeVisitorTest extends BasePHPVisitorTest
{
    public function testExtract()
    {
        $collection = $this->getSourceLocations(new TranslateMeVisitor(), Resources\Php\Symfony\TranslateMeVisitor::class);

        $this->assertCount(1, $collection);
        $source = $collection->first();
        $this->assertEquals('foobar', $source->getMessage());
    }
}

Best practices

A goal of this organization is to show best practises and case studies how to do translations in PHP. The information here should not be considered an absolute truth but rather show examples of how other is doing translations and to be a place of shared knowledge.

Translation keys

The Symfony best practice document states that:

“Keys should always describe their purpose and not their location. For example, if a form has a field with the label ‘Username’, then a nice key would be label.username, not edit_form.label.username.”

That is a good start that we like to build on to. All reusable translations keys should describe their purpose. But non-reusable translation keys should describe their location. Example pricing_page.partner.paragraph0 or flash.user_signup.email_in_use.

The translation keys are also used to give translators some context about where they are used. The following table is a good rule of thumb.

Message key Description
label. foo For form form labels.
flash. foo For flash messages.
error. foo For error messages.
help. foo For help text used with forms.
foo .heading For a heading.
foo .paragraph0 For the first paragraph after a heading.
foo .paragraph1 For the second paragraph after a heading.
foo.paragraph2 .html A third paragraph where HTML is allowed inside the translation.
_foo Starting with underscore means the the translated string should start with a lowercase character.
foo For any common strings like “Show all”, “Next”, “Yes” etc.
vendor.bundle.controller.action. foo For any non-reusable translation.

One should also use different domains to give translators more context. Example of good domains are mail, messages, navigation, validators and admin.

Translate just a few languages

TODO

Using a translation service

TODO

Symfony Translation Bundle

The Symfony bundle is filled with cool features that will ease your translation workflow. You probably do not want all features enabled, just choose the ones you like. Some features requires you to install extra packages, but that is explained in the documentation for each feature.

Installation

Install the bundle with Composer

composer require php-translation/symfony-bundle

Then enable the bundle in AppKernel.php

class AppKernel extends Kernel
{
  public function registerBundles()
  {
    $bundles = array(
        // ...
        new Translation\Bundle\TranslationBundle(),
    }
  }
}

Configuration

The bundle has a very flexible configuration. It allows you do have different setups for different parts of your application. This might be overkill for most applications but it is possible by specifying more keys under translation.configs.

Below is an example of configuration that is great to start with.

translation:
  locales: ["en", "fr", "sv"]
  configs:
    app:
      dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"]
      output_dir: "%kernel.root_dir%/Resources/translations"
      excluded_names: ["*TestCase.php", "*Test.php"]
      excluded_dirs: [cache, data, logs]

With the configuration above you may extract all translation keys from your source code by running

php bin/console translation:extract app

Note

See page Extracting Translations from Source for more information.

Storages

By default we store all translations on the file system. This is highly configurable. Many developers keep a local copy of all translations but do also use a remote storage, like a translations platform. You may also create your own storage. A storage service must implement Translation\Common\Storage.

translation:
  locales: ["en", "fr", "sv"]
  configs:
    app:
      dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"]
      output_dir: "%kernel.root_dir%/Resources/translations"
      remote_storage: ["php_translation.adapter.loco"]
      local_storage: ["app.custom_local_storage"]
      output_format: "xlf"

The PHP Translation organisation provides some adapters to commonly used translation storages. See our all storage adapters or see an example on how to install an adapter.

Configuration reference

The full default configuration is:

translation:
    configs:

        # Prototype
        name:

            # Directories we should scan for translations
            dirs:                 []
            excluded_dirs:        []
            excluded_names:       []
            external_translations_dirs: []
            output_format:        xlf # One of "php"; "yml"; "xlf"; "po"
            blacklist_domains:    []
            whitelist_domains:    []

            # Service ids with to classes that supports remote storage of translations.
            remote_storage:       []

            # Service ids with to classes that supports local storage of translations.
            local_storage:

                # Default:
                - php_translation.local_file_storage.abstract
            output_dir:           '%kernel.root_dir%/Resources/translations'

            # The root dir of your project. By default this will be kernel_root's parent.
            project_root:         ~

            # The version of XLIFF XML you want to use (if dumping to this format).
            xliff_version:        '2.0'

            # Options passed to the local file storage's dumper.
            local_file_storage_options: []
    fallback_translation:
        enabled:              false
        service:              google # One of "google"; "yandex"
        api_key:              null
    edit_in_place:
        enabled:              false
        config_name:          default
        activator:            php_translation.edit_in_place.activator
        show_untranslatable:  true
    webui:
        enabled:              false
        allow_create:         true
        allow_delete:         true

        # Base path for SourceLocation's. Defaults to "%kernel.project_dir%".
        file_base_path:       null
    locales:              []

    # Your default language or fallback locale. Default will be kernel.default_locale
    default_locale:       ~

    # Extend the debug profiler with information about requests.
    symfony_profiler:

        # Turn the symfony profiler integration on or off. Defaults to kernel debug mode.
        enabled:              true
        formatter:            null

        # Limit long HTTP message bodies to x characters. If set to 0 we do not read the message body. Only available with the default formatter (FullHttpMessageFormatter).
        captured_body_length: 0
        allow_edit:           true
    auto_add_missing_translations:
        enabled:              false
        config_name:          default
    http_client:          httplug.client
    message_factory:      httplug.message_factory

You can also dump the default configuration yourself using Symfony command:

bin/console config:dump-reference translation

Extracting Translations from Source

Extracting translations from your project

translation:
  locales: ["en", "fr", "sv"]
  configs:
    app:
      dirs: ["%kernel.root_dir%/Resources/views", "%kernel.root_dir%/../src"]
      output_dir: "%kernel.root_dir%/Resources/translations"
      excluded_names: ["*TestCase.php", "*Test.php"]
      excluded_dirs: [cache, data, logs]

With the configuration above you may extract all translation keys from your source code by running

php bin/console translation:extract app

Extracting translations from a bundle

If you’re using a bundle shipped with custom translations, you can extract them using external_translations_dir.

For example, with FOSUserBundle<https://github.com/FriendsOfSymfony/FOSUserBundle/>:

translation:
  configs:
    app:
      external_translations_dir: ["%kernel.root_dir%/vendor/friendsofsymfony/user-bundle/Resources/translations"]

Creating custom extractor

Example of method whose argument we want to translate

$this->logger->addMessage("text");

Example of extractor class that we use to create translations

<?php

namespace App\Extractor;

use PhpParser\Node;
use PhpParser\NodeVisitor;
use Translation\Extractor\Visitor\Php\BasePHPVisitor;

final class MyCustomExtractor extends BasePHPVisitor implements NodeVisitor
{
    /**
     * {@inheritdoc}
     */
    public function beforeTraverse(array $nodes): ?Node
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function enterNode(Node $node): ?Node
    {
        if (!$node instanceof Node\Expr\MethodCall) {
            return null;
        }

        if (!is_string($node->name) && !$node->name instanceof Node\Identifier) {
            return null;
        }

        $name = (string) $node->name;

        //This "if" check that we have method which interests us
        if ($name !== "addMessage") {
            return null;
        }

        $caller = $node->var;
        $callerName = isset($caller->name) ? (string) $caller->name : '';

        //This "if" check that we have xxx->logger->addMessage()
        if ($callerName === 'logger' && $caller instanceof Node\Expr\MethodCall) {

            //This "if" chack that we have first argument in method as plain text ( not as variable )
            //xxx->logger->addMessage("custom-text") is acceptable
            if (null !== $label = $this->getStringArgument($node, 0)) {
                $this->addLocation($label, $node->getAttribute('startLine'), $node);
            }
        }

        return null;
    }


    /**
     * {@inheritdoc}
     */
    public function leaveNode(Node $node): ?Node
    {
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function afterTraverse(array $nodes): ?Node
    {
        return null;
    }
}

Necessary configuration for proper operation

# -- config/service.yml --

# ....

App\Extractor\MyCustomExtractor:
    tags:
        - { name: php_translation.visitor, type: php }

# ....

Symfony WebUI

The Symfony WebUI feature bring you a web interface to add, edit and remove translations.

_images/webui-dashboard.png _images/webui-page.png

Configuration

# config/config.yaml
translation:
  # ..
  webui:
    enabled: true
  # ..
# config/routing_dev.yaml
_translation_webui:
    resource: "@TranslationBundle/Resources/config/routing_webui.yaml"
    prefix:  /admin

Go to http://localhost.dev/app_dev.php/admin/_trans

Symfony Profiler UI

The Symfony profiler page for translation is great. You see all translations that were used in that request. But what if you could edit those translations as well? This is exactly what this feature does. It is way easier to edit and add new translations since the missing translations are highlighted.

Configuration

# config/config.yaml
translation:
  # ..
  symfony_profiler:
    enabled: true
# config/routing_dev.yaml
_translation_profiler:
    resource: '@TranslationBundle/Resources/config/routing_symfony_profiler.yaml'

See the updated Translation page in the Symfony profiler. There are some new buttons on the right hand side.

Symfony Edit In Place

The Symfony Edit In Place feature allow to edit translations directly in the context of a page, without altering the display styles and presentation. It provides an easy to use interface, even in production.

Users are able to change any translated text directly on the web page, and save them to the configured translation configurations.

Demonstration of the Edit In Place feature

Limitations and trade-off

  • Some translated string can’t be translated via this feature, like HTML Input placeholder or title tag for example. The JavaScript part is using ContentTools by Anthony Blackshaw, which use the HTML contenteditable attribute;
  • Upon saving, the Symfony translation cache is re-generated to allow the user to see the new content. This can be an issue on read-only deployments.

Configuration

# config/config.yaml
translation:
  # ..
  edit_in_place:
    enabled: true
    config_name: default # The configuration to use
    activator: php_translation.edit_in_place.activator # The activator service id
  # ..
# config/routing.yaml
_translation_edit_in_place:
  resource: '@TranslationBundle/Resources/config/routing_edit_in_place.yaml'
  prefix:  /admin

Note

When you include the routing_edit_in_place.yaml to expose the controller that saves the modifications you should be aware of the following:

  • The routes must be in a protected area of your application
  • The routes should be in the production routing file if you want allow real users to use the feature.

Note

Make sure the Bundle assets are installed via bin/console assets:install

Usage

To see the editor options on a page, the php_translation.edit_in_place.activator service needs to allow the Request. By default we provide a simple Activator based on a flag stored in the Symfony Session.

You can activate the editor by calling:

$container->get('php_translation.edit_in_place.activator')->activate();

Then browse your website and you should see the blue Edit button on the top left corner. If you change a translation and hit the Save button, the modifications are saved for the current locale. So if you want to edit a German translation you have to go on the German version of your website.

You can deactivate the editor by calling:

$container->get('php_translation.edit_in_place.activator')->deactivate();

Those calls have to be implemented by yourself.

Building your own Activator

You can change the way the editor is activated by building your own Activator service, all you have to do in implement the Translation\Bundle\EditInPlace\ActivatorInterface interface.

For example if you wish to display the editor based on a specific authorization role you could implement it that way:

<?php

namespace AppBundle;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Translation\Bundle\EditInPlace\ActivatorInterface;

class RoleActivator implements ActivatorInterface
{
    /**
     * @var AuthorizationCheckerInterface
     */
    private $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    /**
     * {@inheritdoc}
     */
    public function checkRequest(Request $request = null)
    {
        try {
            return $this->authorizationChecker->isGranted(['ROLE_ADMIN']);
        } catch (AuthenticationCredentialsNotFoundException $e) {
            return false;
        }
    }
}
# services.yaml
services:
  my_activator:
    class: AppBundle\RoleActivator
    arguments: ["@security.authorization_checker"]

And then use this new activator in the bundle configuration:

# config/config.yaml
translation:
  # ..
  edit_in_place:
    activator: my_activator
  # ..

The Editor toolbox for HTML

What is allowed inside the edited text is handled by our JavaScript. So if you follow the Best practices and finish your translation keys with .html when you want to allow HTML, the editor comes with full power:

HTML Editor options

Please refer to ContentTools documentation for more information.

Automatically Translate

When your application is in production and you request for a translation that happens to be missing, the default action is to check if the translation exists in the fallback locale. If we are “lucky” we show a string in the fallback language.

This could be done better. With the auto translate feature we can try to translate the string in the fallback language to the requested language using a translation service like Google Translate.

Installation

To use this feature you need to install php-translation/translator.

composer require php-translation/translator

Note

If you are having issues installing. See Configure HTTPlug.

Configuration

# config/config.yaml
translation:
  # ..
  fallback_translation:
    enabled: true
    service: 'google' # One of "google", "yandex", or "bing"
    api_key: 'foobar'
  # ..

Usage

That’s it. You do not have to do anything more. It is however a good idea to add some aggressive caching on the responses from the translation service in order to remove the need of paying for the same translation twice. See how you Configure HTTPlug.

Automatically Add Missing Translations

When you are visiting a page the Symfony TranslationDataCollector will record what translations are being used. The translations marked as “Missing” will be added to the storage.

Configuration

# config/config.yaml
translation:
  # ..
  auto_add_missing_translations:
    enabled: true
    config_name: 'default'
  # ..

Usage

That’s it. You do not have to do anything more. Translations will automatically pop up in your storage.

Production environment

Note: The TranslationDataCollector is not used in production environment (this file is linked with the profiler). For use in production, you need to decorate the translator :

translator.data_collector:
    class: Symfony\Component\Translation\DataCollectorTranslator
    decorates: translator
    arguments: ['@translator.data_collector.inner']

Application Delivery

If you decide to remove translations from your project repository, when you deliver your application, you have to run the translation:download command. To do so, you just have to add one line in your composer.json file.

Configuration

Update the section "scripts" of you composer.json file.

Example for Symfony 2.x :

{
  "scripts": {
    "symfony-scripts": [
      "@php app/console --env=prod translation:download --cache"
    ],
    "post-install-cmd": [
      "@symfony-scripts"
    ],
    "post-update-cmd": [
      "@symfony-scripts"
    ]
  }
}

Example for Symfony 3.x :

{
  "scripts": {
    "symfony-scripts": [
      "@php bin/console --env=prod translation:download --cache"
    ],
    "post-install-cmd": [
      "@symfony-scripts"
    ],
    "post-update-cmd": [
      "@symfony-scripts"
    ]
  }
}

Example for Symfony 4.x :

{
  "scripts": {
    "auto-scripts": {
      "translation:download --cache": "symfony-cmd"
    },
    "post-install-cmd": [
      "@auto-scripts"
    ],
    "post-update-cmd": [
      "@auto-scripts"
    ]
  }
}

Common

The Common component is the least exiting component. It contains common interfaces and classes that are used by many other packages in this organization. The most important ones are listed on this page.

Message

The Message class is a representation of a translation in a specific language. This class is commonly used as a parameter or a return value for functions in the organisation.

The message contains of an key, domain, locale and translation. There is also an array where meta data can be stored. Example of usage of meta data could be when a third party translation service has flagged the translation as “fuzzy”.

Storage

The Storage interface is an abstraction for places where you can store translations. There are many examples like on file system, in database or in any third party translation service. A Storage is very simple. It has methods for getting, updating and deleting a translation.

Exception

The Exception interface will decorate all the runtime exceptions in the organisation.

Extractor

The responsibility of the Extractor component is to get translation keys from the source code.

Installation and Usage

Install the extractor component with Composer

composer require php-translation/extractor

When the extractor is downloaded you may use it by doing the following:

require "vendor/autoload.php";

use Translation\Extractor\Visitor\Php\Symfony as Visitor;

// Create extractor for PHP files
$fileExtractor = new PHPFileExtractor()

// Add visitors
$fileExtractor->addVisitor(new Visitor\ContainerAwareTrans());
$fileExtractor->addVisitor(new Visitor\ContainerAwareTransChoice());
$fileExtractor->addVisitor(new Visitor\FlashMessage());
$fileExtractor->addVisitor(new Visitor\FormTypeChoices());

// Add the file extractor to Extractor
$extractor = new Extractor();
$extractor->addFileExtractor($this->getPHPFileExtractor());

//Start extracting files
$sourceCollection = $extractor->extractFromDirectory('/foo/bar');

// Print the result
foreach ($sourceCollection as $source) {
  echo sprintf('Key "%s" found in %s at line %d', $source->getMessage(), $source->getPath(), $source->getLine());
}

Architecture

There is a lot of things happening the the code example above. Everything is very SOLID so it is easy to add your own extractors if you have custom features that you need to translate.

The class that we interact with after when we want to extract translations is the Extractor class. It supports Extractor::extractFromDirectory(string) and the more flexible Extractor::extract(Finder). The Extractor looks at all files in the directory and checks the type/extension. The extractor then executes all FileExtractor for this file type.

There is a few FileExtractor that comes with this component. They are PHPFileExtractor, TwigFileExtractor and BladeExtractor. As you may guess they extract translations from PHP files, Twig files and Blade files respectively. The most interesting ones are PHPFileExtractor and TwigFileExtractor because they are using the Visitor pattern to parse all the nodes in the document.

Let’s focus on the PHPFileExtractor only for a moment. We are using the Nikic PHP Parser to split the PHP source into an abstract syntax tree which enables us to statically analyze the source. Read more about this in the nikic/php-parser documentation. When you add a visitor to the PHPFileExtractor it will be called for each node in the syntax tree.

The visitors is very specific with what they are looking for. The FlashMessage visitor is searching for a pattern like $this->addFlash(). If that string is found it will add a new SourceLocation to the SourceCollection model.

When all visitors and FileExtractor has been executed an instance of the SourceCollection will be returned.

Note

If you want to add functionality to the extractor you are most likely to add a new visitor. See Adding extractors for more information.

Special extractors

We have common extractors for Symfony, Twig and Blade. They all are doing static analysis on the source files to find translation strings. But in some situations you need to specify translation dynamically. You may achieve this by implementing TranslationSourceLocationContainer.

use Translation\Extractor\Model\SourceLocation;
use Translation\Extractor\TranslationSourceLocationContainer;
use Symfony\Component\Form\AbstractType;

class MyCustomFormType extends AbstractType implements TranslationSourceLocationContainer
{
    // ...
    public static function getTranslationSourceLocations()
    {
        $options = // Get options
        $data = [];
        foreach ($options as $option) {
            $data[] = SourceLocation::createHere('option.'.$option);
        }

        return $data;
    }
}

Translator

The Translator component provides an interface for translation services like Google Translate or Bing Translate.

Installation and Usage

Install the translator component with Composer

composer require php-translation/translator
require "vendor/autoload.php";

$translator = new Translator();
$translator->addTranslatorService(new GoogleTranslator('api_key'));

echo $translator->translate('apple', 'en', 'sv'); // "äpple"

Architecture

The Translator class could be considered a “chain translator” it asks the first translation service to translate the string. If the translation fails it asks the second service until a translation is found. If no translation is found a null value will be returned.

The Translator class is SOLID so you can easily add your custom translator into the chain.

Note

Since most translator services are paid services you probably want to add aggressive caching on the responses. See Configure HTTPlug for more information.

Command line interface

If you do not want to “pollute” your application with a lot of dependencies you may install our CLI tool. It is basically the Symfony Translation bundle packed down in a single PHAR.

The CLI support extracting, syncing and downloading translations. It does also run the WebUI so you can edit translations in a nice user interface.

Download

wget https://php-translation.github.io/cli/downloads/translation.phar
chmod +x translation.phar

Configuration

Every time you run the CLI it looks for a configuration file named “translation.yml” that should be located in the same directory that you execute the command. The configuration will be exact the same as for the TranslationBundle. Example:

# translation.yml
translation:
  locales: ["en", "sv"]
  configs:
    app:
      dirs: ["%kernel.project_dir%/app/Resources/views", "%kernel.project_dir%/src"]
      output_dir: "%kernel.project_dir%/app/Resources/translations"
      excluded_names: ["*TestCase.php", "*Test.php"]
      excluded_dirs: [cache, data, logs]

Other translation bundles installed

The CLI tool does also have a few other translation bundles installed. They are installed by default to give you the possibility to configure different kind of remote storages.

  • Loco Adapter
  • Flysystem Adapter
  • Phraseapp Adapter

Development

Contributor Code of Conduct

As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.

We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.

Examples of unacceptable behavior by participants include:

  • The use of sexualized language or imagery
  • Personal attacks
  • Trolling or insulting/derogatory comments
  • Public or private harassment
  • Publishing other’s private information, such as physical or electronic addresses, without explicit permission
  • Other unethical or unprofessional conduct

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at tobias.nyholm@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.

This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/

Contributing

If you’re here, you would like to contribute to this project and you’re really welcome!

Bug Reports

If you find a bug or a documentation issue, please report it or even better: fix it :). If you report it, please be as precise as possible. Here is a little list of required information:

  • Precise description of the bug
  • Details of your environment (for example: OS, PHP version, installed extensions)
  • Backtrace which might help identifying the bug
Security Issues

If you discover any security related issues, please contact us at tobias.nyholm@gmail.com instead of submitting an issue on GitHub. This allows us to fix the issue and release a security hotfix without publicly disclosing the vulnerability.

Feature Requests

If you think a feature is missing, please report it or even better: implement it :). If you report it, describe the more precisely what you would like to see implemented and we will discuss what is the best approach for it. If you can do some research before submitting it and link the resources to your description, you’re awesome! It will allow us to more easily understood/implement it.

Sending a Pull Request

If you’re here, you are going to fix a bug or implement a feature and you’re the best! To do it, first fork the repository, clone it and create a new branch with the following commands:

$ git clone git@github.com:your-name/repo-name.git
$ git checkout -b feature-or-bug-fix-description

Then install the dependencies through Composer:

$ composer install

Write code and tests. When you are ready, run the tests. (This is usually PHPUnit)

$ composer test

When you are ready with the code, tested it and documented it, you can commit and push it with the following commands:

$ git commit -m "Feature or bug fix description"
$ git push origin feature-or-bug-fix-description

Note

Please write your commit messages in the imperative and follow the guidelines for clear and concise messages.

Then create a pull request on GitHub.

Please make sure that each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting with the following commands (here, we assume you would like to squash 3 commits in a single one):

$ git rebase -i HEAD~3

If your branch conflicts with the master branch, you will need to rebase and re-push it with the following commands:

$ git remote add upstream git@github.com:orga/repo-name.git
$ git pull --rebase upstream master
$ git push -f origin feature-or-bug-fix-description
Coding Standard

This repository follows the PSR-2 standard and so, if you want to contribute, you must follow these rules.

Semver

We are trying to follow semver. When you are making BC breaking changes, please let us know why you think it is important. In this case, your patch can only be included in the next major version.

Contributor Code of Conduct

This project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

License

All of our packages are licensed under the MIT license.

Building the Documentation

We build the documentation with Sphinx. You could install it on your system or use Docker.

Install Sphinx
Install on Local Machine

The installation for Sphinx differs between system. See Sphinx installation page for details. When Sphinx is installed you need to install enchant (e.g. sudo apt-get install enchant).

Using Docker

If you are using docker. Run the following commands from the repository root.

$ docker run --rm -t -v "$PWD":/doc webplates/readthedocs build
$ docker run --rm -t -v "$PWD":/doc webplates/readthedocs check

Alternatively you can run the make commands as well:

$ docker run --rm -t -v "$PWD":/doc webplates/readthedocs make html
$ docker run --rm -t -v "$PWD":/doc webplates/readthedocs make spelling

To automatically rebuild the documentation upon change run:

$ docker run --rm -t -v "$PWD":/doc webplates/readthedocs watch

For more details see the readthedocs image documentation.

Build Documentation

Before building the documentation make sure to install all requirements.

$ pip install -r requirements.txt

To build the docs:

$ make html
$ make spelling

License

Copyright (c) PHP Translation <tobias.nyholm@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.