php-gettext / Gettext

PHP library to collect and manipulate gettext (.po, .mo, .php, .json, etc)
MIT License
687 stars 132 forks source link

I need help configuring a gettext project #249

Closed NathanBnm closed 4 years ago

NathanBnm commented 4 years ago

Hi! I trying to create a sample project in PHP with gettext integration for localization, but I can't manage to create the right architecture in order to use it.

I tried the different examples in the README file but I don't see how to link everything together. Is there a complete and concrete example somewhere? I searched the Web and everything I found is kind of outdated.

I only want to have the possibility to generate po files from php files and use them for localization changing the language from a variable.

For example if I have a index.php file:

<?php

echo _("Hello world!")

How should I use php-gettext to extract this string into a po file in a structured locale folder and use it back to translate the page according to the specified language?

Thanks in advance for your help :smile: I'm kind of lost.

NathanBnm commented 4 years ago

Here is what I tried in order to extract a PHP file:

I installed gettext/gettext and gettext/php-scanner via composer.

I have a index.php file with the following content:

<?php

echo /* i18n: Hello world */ _("Hello world!");

Then I created a extract.php file with the following content:

<?php

require 'vendor/autoload.php';

use Gettext\Scanner\PhpScanner;
use Gettext\Generator\PoGenerator;
use Gettext\Translations;

//Create a new scanner, adding a translation for each domain we want to get:
$phpScanner = new PhpScanner(
    Translations::create('messages')
);

//Set a default domain, so any translations with no domain specified, will be added to that domain
$phpScanner->setDefaultDomain('messages');

//Extract all comments starting with 'i18n:' and 'Translators:'
$phpScanner->extractCommentsStartingWith('i18n:', 'Translators:');

//Scan files
foreach (glob('*.php') as $file) {
    $phpScanner->scanFile($file);
}

//Save the translations in .po files
$generator = new PoGenerator();

foreach ($phpScanner->getTranslations() as $domain => $translations) {
    $generator->generateFile($translations, "locales/{$domain}.po");
}

When executing this I get the following messages.po file which is empty:

msgid ""
msgstr ""
"X-Domain: messages\n"
oscarotero commented 4 years ago

Hi. By default, the function _ is not scanned. You have two options:

$phpScanner = new PhpScanner(
    Translations::create('messages')
);

$phpScanner->setFunctions([
    '_' => 'gettext' //Scan the "_" functions with the "gettext" handler
    '_e' => 'gettext' //Scan the "_e" functions with the "gettext" handler
]);
NathanBnm commented 4 years ago

@oscarotero thanks for your help. I was wondering why don't you specify that in the doc?

NathanBnm commented 4 years ago

Now this is how I extract my translations in the extract.php script I created:

<?php

//Translation domain
$domain = 'messages';
//Supported languages
$languages = [
    'fr'
];
//Files to extract translations from
$files = glob('*.php');

require 'vendor/autoload.php';

use Gettext\Generator\PoGenerator;
use Gettext\Loader\PoLoader;
use Gettext\Merge;
use Gettext\Scanner\PhpScanner;
use Gettext\Translations;

$translations = Translations::create($domain);

$generator = new PoGenerator();

$loader = new PoLoader();

$phpScanner = new PhpScanner($translations);

$phpScanner->setFunctions([
    '_' => 'gettext' //Scan the "_" functions with the "gettext" handler
]);

//Set a default domain, so any translations with no domain specified, will be added to that domain
$phpScanner->setDefaultDomain('messages');

//Extract all comments starting with 'Translators:'
$phpScanner->extractCommentsStartingWith('Translators:');

//Scan files
foreach ($files as $file) {
    $phpScanner->scanFile($file);
}

foreach ($phpScanner->getTranslations() as $domain => $translations) {
    foreach ($languages as $lang) {

        $path = "locale/{$lang}/LC_MESSAGES";

        if (!file_exists($path)) {
            mkdir($path, 0777, true);
        }

        $path .= "/{$domain}.po";

        $translations->setLanguage($lang);

        if (file_exists($path)) {
            $translations = $translations->mergeWith($loader->loadFile($path), Merge::SCAN_AND_LOAD);
        }

        $generator->generateFile($translations, $path);
    }
}

Here is the fr.po file that I get:

msgid ""
msgstr ""
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Domain: messages\n"

#: index.php:15
msgid "Hello world!"
msgstr "Bonjour tout le monde !"

#. Translators: %d is a number
#: index.php:18
msgid "Number is %d"
msgstr "Le nombre est %d "

In my index.php page I have the following code:

<?php

require 'vendor/autoload.php';

use Gettext\Loader\PoLoader;

$loader = new PoLoader();

if(isset($_GET['lang'])) {
    $lang = $_GET['lang'];
    $translations = $loader->loadFile("locales/{$lang}.po");
}

echo _("Hello world!");

$n = 10;
echo sprintf( /* Translators: %d is a number */ _("Number is %d"), $n);

Now how do I make the translations apply?

Edit: I tried using Translator with the following example but it doesn't work:

<?php

require 'vendor/autoload.php';

use Gettext\GettextTranslator;
use Gettext\TranslatorFunctions;

$translator = new GettextTranslator();

if(isset($_GET['lang'])) {
    $lang = $_GET['lang'];

    $translator->setLanguage($lang);
    $translator->loadDomain('messages', 'locale');
    TranslatorFunctions::register($translator);
}

echo _("Hello world!");

$n = 10;
echo sprintf( /* Translators: %d is a number */ _("Number is %d"), $n);
oscarotero commented 4 years ago

Maybe, the php-scanner should include the _ function by default, because it's the functions of the php extension. I'll fix that.

In your example, you're loading a translator but then use the _ function instead the translator itself:

$translator = new GettextTranslator();

//Configure the translator...

//Use it
echo sprintf($translator->gettext('Number is %d'), $n);

// Use the "__" functions to get the translations globally
// and apply format (sprintf, for example)
Gettext\TranslatorFunctions::register($translator);

//Now it's easier to apply the translations:
echo __('Number is %d', $n);
NathanBnm commented 4 years ago

@oscarotero is the given example supposed to work? I can't manage to make it work properly.

oscarotero commented 4 years ago

To use the php gettext extension, you have to export the translations to .mo (Po is the format to edit the translations but Mo is the format used by gettext to consume these translations. And you have to save the mo files in specific folders. See the PHP docs here: https://www.php.net/manual/en/book.gettext.php and the GettextTranslator docs: https://github.com/php-gettext/Translator#gettexttranslator

NathanBnm commented 4 years ago

I have the following project structure:

| locale
   | - fr
      | - LC_MESSAGES
         | - messages.mo
         | - messages.po
| extract.php
| index.php

Here is the extract.php script:

<?php

//Translation domain
$domain = 'messages';
//Supported languages
$languages = [
    'fr'
];
//Files to extract translations from
$files = glob('*.php');

require 'vendor/autoload.php';

use Gettext\Generator\MoGenerator;
use Gettext\Generator\PoGenerator;
use Gettext\Loader\PoLoader;
use Gettext\Merge;
use Gettext\Scanner\PhpScanner;
use Gettext\Translations;

$translations = Translations::create($domain);

$moGenerator = new MoGenerator();
$poGenerator = new PoGenerator();
$poLoader = new PoLoader();

$phpScanner = new PhpScanner($translations);

/*
$phpScanner->setFunctions([
    '_' => 'gettext' //Scan the "_" functions with the "gettext" handler
]);
*/

//Set a default domain, so any translations with no domain specified, will be added to that domain
$phpScanner->setDefaultDomain('messages');

//Extract all comments starting with 'Translators:'
$phpScanner->extractCommentsStartingWith('Translators:');

//Scan files
foreach ($files as $file) {
    $phpScanner->scanFile($file);
}

foreach ($phpScanner->getTranslations() as $domain => $translations) {
    foreach ($languages as $lang) {

        $path = "locale/{$lang}/LC_MESSAGES";

        if (!file_exists($path)) {
            mkdir($path, 0777, true);
        }

        $path .= "/{$domain}";

        $translations->setLanguage($lang);

        if (file_exists($path . '.po')) {
            $translations = $translations->mergeWith($poLoader->loadFile($path . '.po'), Merge::SCAN_AND_LOAD);
        }

        $poGenerator->generateFile($translations, $path . '.po');
        $moGenerator->generateFile($translations, $path . '.mo');
    }
}

It gives me the following messages.po file:

msgid ""
msgstr ""
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Domain: messages\n"

#: index.php:14
msgid "Hello world!"
msgstr "Bonjour tout le monde !"

And here is my index.php file:

<?php

require 'vendor/autoload.php';

use Gettext\GettextTranslator;
use Gettext\TranslatorFunctions;

$translator = new GettextTranslator();

$translator->setLanguage('fr');
$translator->loadDomain('messages', 'locale');
TranslatorFunctions::register($translator);

echo __('Hello world!');

But at the end I can't manage to make the translation work. I don't see what I do wrong.

oscarotero commented 4 years ago

I just copy-pasted your exact code and file structure and works fine. Perhaps is something related with the locales installed in your computer (I remember gettexts requires to have installed the locales in the system). Other possible issue may be cache: if you are using a web server like apache, I remember that I needed to restart the server to refresh the gettext cache, because the mo file is loaded once and saved in the cache. Note that Gettext is not a php sofware but a system service. The php extension is only to use it (like a mysql database or imagick, for example).

This is one of the reasons why I created this library, because working with gettext (at least in dev environment) is a pain in the ass. The easy way is by using Translator class (instead GettextTranslator) because it is pure php and you won't have these issues. Your code is the same, but instead of (or in addition to) export to mo, you need to export to php array, and load this array file with Translator class.

use Gettext\Generator\ArrayGenerator;
use Gettext\Translator;

// Your php code here...

$arrayGenerator = new ArrayGenerator();
$arrayGenerator->generateFile($translations, $path . '.php');

$translator = new Translator();
$translator->loadTranslations($path . '.php');

TranslatorFunctions::register($translator);

echo __('Hello world!');
NathanBnm commented 4 years ago

Thanks for your help! I finally managed to make something that works using PO and PHP files for translation.

You were right, using native gettext with MO files is annoying 👍 I had to refresh my Apache server to make it work.

I just created a repo with the template I created here. I would be pleased if you could take a look 😄 Do not hesitate to give me some feedback if needed!