PHP internationalization

<?php
$foo = 'Hello world';

$foo = trans('hello_world');
// messages.php: 'hello_world' => 'Hello world'

$foo = "Hello world, $user!";
$foo = 'Hello world, ' . $user . '!';
$foo = sprintf('Hello world, %s!', $user);

$foo = trans('hello_world', ['user' => $user]);
// messages.php: 'hello_world' => 'Hello world, %user%!'

Features supported

Configure hardcoded strings extraction from PHP source

The plugin should automatically configure itself for Laravel, Symfony, CodeIgniter, CakePHP, Zend and Laminas projects, but adjustments could be needed for custom setup and other frameworks.

PHP Source Code Preferences screenshot

Scope

i18n Ally is applying inspections for files that have .php extension and are included into a PhpStorm’s scope.

Create a new scope or adjust existing by clicking on button and handpicking only the meaningful directories and files.

Select Project files to include all .php files in your project.

Important! This source would only looks for hardcoded strings within PHP source codeHTML and outside of PHP snippets. To extract hardcoded strings from HTML tags configure an HTML with PHP source.

Replacement template

The “Replacement template” reflects the result of the hardcoded string extraction.function name and arguments template.

Recommended value for gettext, CodeIgniter, CakePHP and Zend/Laminas: _('%key%') with sprintf mode enabled.
Recommended value for Yii v2: _('%namespace%', '%key%', %map%).
Recommended value for Yii v3: _('%key%', %map%, '%namespace%').

It could be any callable PHP structure that wraps arguments into parentheses:

  • function: _(…), __(…),
  • object method: $this->trans(…), $translator->trans(…),
  • static method: \Yii:app(…).

%key%

Short key or a natural language string that defines a translation.

%namespace%

Namespace (called ‘domain’ in Symfony) usually means a part of language file path from where translations would be searched for. The default namespace is usually messages, but could be changed by putting a namespace in first position in “Namespaces” field.

%map%

If there are no variables in the string, then nothing would be added.

Map will be replaced with an associative short syntax array if there are any placeholders detected: trans('key', ['foo' => $foo, 'bar' => $bar]).

Placeholder names will be determined automatically based on a respective variable, function or method name.

In language files placeholder syntax will be determined based on the Placeholder format setting of the language file.

%list%

If there are no variables in the string, then nothing would be added.

List will be replaced with an array if there are any placeholders detected: trans('key', [$foo, $bar]).

In language files the ordered placeholder syntax {0}, {1} will be enforced.

%varargs%

If there are no variables in the string, then nothing would be added.

Varargs will be replaced with placeholder passed directly to the translation function if there are any placeholders detected: trans('key', $foo, $bar).

In language files the ordered placeholder syntax {0}, {1} will be enforced.

Supported language constructs

i18n Ally finds hardcoded user-facing strings within callable context and supports multiple cases:

"Welcome, John"     // trans('welcome') simple strings
"Welcome, {$name}"  // trans('welcome', ['name' => $name]) interpolated strings
"Welcome, " . $name // trans('welcome', ['name' => $name]) concatenated strings
sprintf("Welcome, %s", $name) // trans('welcome', ['name' => $name]) // sprintf templates

Placeholder names are determined automatically.

What’s not supported

  • Using an array for message retrieval (common approach in PHP legacy codebases, for example $lang['key']).

What strings are skipped

  • All arguments passed to functions or methods (except constructors),
  • HEREDOC and NOWDOC strings,
  • Array keys,
  • Class property definitions,
  • Default paramenter values,
  • Constant name specified in define first argument,
  • Strings assigned to constants,
  • Default argument values
  • Full SQL queries and most of SQL parts,
  • Strings that looks like code: without letters, multiple words without spaces or camelCased ones.

Limitation: dependencies should be wired manually

When extracting a translation you should still wire dependencies manually if they are not global like Laravel’s helper __(…) or static method in Yii 2.0 \Yii::t(…).

Given a Symfony controller with configured autowiring:

class BlogController extends BaseController
{
    public function commentNew(Request $request)
    {
        $message = 'Comment saved!';
    }
}

i18n Ally would help to extract a hardcoded string:

class BlogController extends BaseController
{
    public function commentNew(Request $request)
    {
        $message = $translator->translate('comment_saved'); # CHANGED by i18n Ally
    }
}

Then developer should manually specify dependencies:

use Symfony\Contracts\Translation\TranslatorInterface; # CHANGED manually

class BlogController extends BaseController
{
    public function commentNew(Request $request, TranslatorInterface $translator) # CHANGED manually
    {
        $message = $translator->translate('Comment saved!');
    }
}

Best practice: dealing with branching in messages

It’s common to have small and simple branching for presentation purposes:

$foo = 'Webhook <strong>' . ($success ? 'succeeded' : 'failed') . '</strong>.';

The best practice it to separate this message into two different ones so translators would have a full context and would be able to adjust word order according the target language grammar.

1st step: manually extract the condition out of the message to get two messages without condition

if ($success) {
    $foo = 'Webhook <strong>succeeded</strong>.';
} else {
    $foo = 'Webhook <strong>failed</strong>.';
}

2nd step: replace simple messages with i18n Ally

if ($success) {
    $foo = trans('webhook_succeeded');
} else {
    $foo = trans('webhook_failed');
}