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¶
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.
Symfony Bundle¶
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¶
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¶
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¶
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.
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 belabel.username
, notedit_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.
Features¶
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.
Configuration¶
# config/config.yaml
translation:
# ..
webui:
enabled: true
# ..
# config/routing_dev.yaml
_translation_webui:
resource: "@TranslationBundle/Resources/config/routing_webui.yaml"
prefix: /admin
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.
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:
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.