How to make Gettext work with PHP on Windows 11

How to make Gettext work with PHP on Windows 11

Published at: 12/13/2022
Updated at: 01/16/2023
Categories
Tags

Gettext with PHP on Windows 11

Yes, I've made it, after spending only 8 hours between tests and debug, I have succesfully enabled gettext on Windows for my local PHP environment! If you are reading this, I guess you are also struggling with it like I did today.

Important premise: In my case I used Laragon and PHP 8.1.10 with Apache as a Web Server. It may work differently with other environments like XAMP or WAMP, or with Nginx and other versions of PHP.

Preparing the PHP environment

First of all, let's enable the gettext extension within PHP, by editing the php.ini

# php.ini
extension=gettext

Then we verify it has been enabled with phpinfo:

// index.php
phpinfo();

The output needs to show us that gettext is indeed enabled:

Gettext enabled with phpinfo
Gettext enabled with phpinfo

You might also be interested in: How to use Matomo's Server Side Tracking

Windows regional language format

I know it must sound unbelievable, but the usage of gettext depends on the Regional Format for the language we have on Windows. I spent hours to reach this conclusion, but so it is, at least for me.

Let's check our regional format then:

Formato regionale della lingua su Windows 11
Regional format for the language on Windows 11

We can also find the language codes from the Microsoft docs:

Generally, we just need the two-letter codes, "it" for Italian, "en" for English, and so on. You might need to use a more specific code, like "it-IT" or 'en-US". Be aware that Windows uses the dash while Unix uses the underscore. On Linux you have "fr_FR" while Windows has "fr-FR".

We will use this code with setlocale in php and, as we are going to see, for the textdomain.

How gettext works with PHP on Windows

In a nutshell: Suppose we have "Italian" as the Regional Language (see above). Gettext will look for MO files inside the "it" folder in the path that we will specify in PHP with the functions on the textdomain:

$language = 'fr'; // French, or I could also use fr-FR
$domain = 'mydomain_' . $language; // The domain will be mydomain_fr, thus PHP will look for the file mydomain_fr.mo
setlocale(LC_ALL, $language);
bindtextdomain($domain, 'C:\laragon\usr\locale'); // PHP will look in this path for the folder "it" based on the language code of the Regional Format on our System
textdomain($domain);
bind_textdomain_codeset($domain, 'UTF-8');
# All my .mo files must therefore be inside C:\laragon\usr\locale\it

Please note that on Windows we use LC_ALL, we do not have LC_MESSAGES nor LC_TIME which are used in Unix. LC_ALL is used for all the locales, as pointed out in the PHP documentation for setlocale.

After all, gettext is a GNU software.

Getting the user language from the browser

If we want to know the language our user is browsing in, we can do it like this:

$language = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);

This is just a header sent by the browser, it might not be set at all, so be sure to define a default locale like "en".

With this setup, we must have all the .mo files following this structure:

C:\laragon\
- usr
    - locale
        - it (language code for my regional format on Windows)
            .\mydomain_fr.mo ---> this is the file that is going to be loaded because I have setlocale() to "fr"
            .\mydomain_it.mo
            .\mydomain_es.mo

We can change the $domain to use a different MO file for another project (myotherdomain_fr.mo) and we can change the $language to use the MO file for another language (mydomain_es.mo).

Translating automatically the PHP files with xgettext and msgfmt

This step is optional, it is useful only if we need to compile and update automatically the PO files, for the new text strings we have added in our PHP website or application.

Even on Windows we can perform the same actions we do on Unix using xgettext and msgfmt.

All the strings used with the php gettext() function or with its alias _() will be configured in a PO file to be translated, this will be done automatically, which will save us a lot of time!

Example of translatable strings:

# index.php
_('Page title')
_('My text')
gettext('Some other text')

Let's see how to do this:

  1. Let's download gettext for Windows from Michele Locali's Github repository, which we thank for this. If the download doesn't work, I've uploaded the latest gettext release here: gettext 0.21 Windows
  2. Alternatively, we can install Poedit, an open source software that offers a graphical interface to edit PO files and generate MO files
  3. In the Poedit installation folder we will find GettextTools\bin where we have the executable files for Windows (.exe.) for xgettext and msgfmt
  4. At this point, let's configure the path in the Windows system variables, below are the screenshots with the steps to follow:
    1. We search on Windows for Editing environment variables
    2. Then we click on Environment Variables
    3. In the system variables, at the bottom, we modify Path, or add it if absent
    4. We add a new variable with the path that leads to Poedit\GettextTools\bin

In practice, the Path variables are used to make certain commands such as xgettext and msgfmt executable from any point on the terminal, or for example cwebp if we use the WebP libraries, or ffmpeg if we want to convert a video.

Modifying the Environment System Variables on Windows

Using the search bar on Windows we can open the tool to edit the system variables:

How to find the tool for modying System environment variables on Windows
Modifying the Environment Variables on Windows
System properties: Environment variables
System properties: Environment variables

Modifying the PATH System Variable

Let's change the Path variable for our System, or eventually only for our user if we prefer:

System variables: Path
System variables: Path

Adding a new variable with the path of the gettext executables

Now we add a new variable for the Path, writing the path to the gettext folder with the executables:

Adding a new environment variable for gettext
Adding a new environment variable for gettext

I have to give credit to user Desintegr from StackOverflow, it is thanks to him that I discovered the Windows executables were in Poedit's GettextTools folder.

Running the commands to compile and update the PO files automatically

After doing these operations to globally set xgettext and msgfmt, let's move on to the next step. Actually we could also launch the executables directly from the Poedit folder, but it's much more convenient to set the environment variables.

If we have opted for PoEdit, the msgattrib executable is not present in the GettextTools. In order to use it, we need to download gettext for Windows from Michele Locali's Github repository as already mentioned. In this case, the environment variable for Path should point to the path where we put gettext, for example C:\Programs\gettext

REM Translating a single file, a PHP one in this case
xgettext -d it -p \path\to\project\locale\it -L PHP --from-code=UTF-8 --no-location -j \path\to\project\index.php

REM Compiling MO files
msgfmt -D \path\to\project\locale\it it.po -o \path\to\project\locale\it\LC_MESSAGES\mydomain_it.mo

Note: To be detected by gettext, strings must be used with the gettext() function or its _() alias. See the gettext documentation.

If you get a "No such file or directory" error for the PO file, it's probably because we need to create the PO file first, it's not created by gettext, it must already exist. We can create it as a simple text file or use PoEdit.

Translating all PHP files at once

We can modify this code with our needings and save it as a .bat file:

@echo off
REM here we can omit the count and the pause if not needed
SET count=1
REM asking the language to the user, like es or fr
SET /p language=Enter language: 
FOR /f "tokens=*" %%G IN ('dir *.php /b /s') DO (call :subroutine "%%G")
pause
GOTO :eof

:subroutine
REM here we can also omit the echo and set commands
echo %count%:%1
REM we add --no-location if we want to remove from the PO file the path where the translation has been found
xgettext.exe -j %1 -d %language% -p \path\to\locale\it -L PHP --from-code=UTF-8 
set /a count+=1
GOTO :eof

This BAT file will save inside \path\to\locale\it the it.po file with all the pre-compiled strings for every PHP found. We can avoid using "pause" if we want the terminal to be closed as soon as it has completed the task.

Note: this script searches for all the PHP files inside the same folder and the subfolders, starting from where the BAT file is located. We can amplify or reduce the range of folders by simply moving the BAT files in the folders tree.

I have to thank for this the documentation on the FOR loop on CMD from SS64 because I was having a hard time making the loop work wel,, then I found this page and I understood I had to use the subroutine.

The count is just something useful to have a better output in the command prompt.

We add --no-location if we want to remove from the PO file the path where the translation has been found.

The following error can be ignored, it is probably caused by the use of the -j attribute inside the for loop.

Charset "CHARSET" is not a portable encoding name. Message conversion to user's charset might not work.

Compiling the MO files automatically

If you do not wish to use PoEdit to create the MO file and want to do this automatically, you can save this code in a BAT file, after adjusting it to your needs:

@echo off
REM asking the language to the user, like es or fr, and the domain, for example "my_project"
SET /p language=Enter language: 
SET /p domain=Enter domain: 
msgfmt -D \path\to\locale\%language% %language%.po -o \path\to\locale\%language%\LC_MESSAGES\mydomain_%language%.mo

You can remove the language and domain inputs if you prefer. Thanks to Instantsoup from StackOverflow for the idea.

Remove obsolete strings that are not needed anymore

REM In order to update the PO files, removing the unused strings found in the PHP files:
REM 1) Create the PO
SET /p sourcelanguage=Enter the language to refer to: 
SET /p destinationlanguage=Enter the destination language: 
xgettext -d %sourcelanguage% -p \path\to\project\locale\%sourcelanguage% -L PHP --from-code=UTF-8 --no-location -j file.php
REM 2) Make a diff between it.po and en.po, then mark as obsolete the extra strings that are not inside it.po
msgattrib --set-obsolete --ignore-file=\path\to\project\locale\%sourcelanguage%.po -o \path\to\project\locale\%destinationlanguage%.po \path\to\project\locale\%destinationlanguage%.po
REM 3) Remove obsolete strings
msgattrib --no-obsolete -o \path\to\project\locale\%destinationlanguage%.po \path\to\project\locale\%destinationlanguage%.po

We save the code above as a .bat file, after adjusting it to our needs.

Basically, after creating the PO file, we run msgattrib which will find the strings that are still inside en.po but are not inside it.po anymore, will mark the extra ones as obsolete, and then remove them.

Please keep in mind that if msgattrib finds non translated strings, where msgstr is empty, it will remove those string.

How to manually translate with PoEdit

It is clear that sooner or later we will also have to translate the texts, or hand them over to translators.

PO (Portable Object) files are "human-readable", we can open them with any text editor and edit them. MO (Machine Object) files, on the other hand, are readable only by the computer, they are binary files. Also, Portable Object Template (POT) files are a starting template for creating PO files, but they are optional.

Here is a PO file example:

# it.po
msgid "This is a title"
msgstr "Questo è un titolo"

msgid "Some other text"
msgstr "Dell'altro testo"

So we have the msgid which indicates the string to be translated, and the msgstr which indicates the actual translation in the language of interest.

We can edit these POs via editor or with PoEdit: be aware that on PoEdit we can only edit the final translations, not the original strings:

Poedit interface
Poedit Interface

At the top we have the strings present in the PO, at the bottom we have the source text (msgid) and the translation (msgstr).

On the right we also have the suggested translations, which however are a paid feature.

We save the PO file and the MO file will automatically be generated in the same folder. We can also generate just the MO file by going to File > Compile MO

Modifying PO files with a text editor

As we saw, we can modify the PO files with any editor. I personally use VSCodium and I have installed the gettext extension for syntax highlighting. We could also use Notepad++ or the goold old Windows Notepad.

When we are done, we can save the PO file overwriting the previous one. To convert it in MO we will need to use PoEdit or run the msgfmt command.

Conclusions

What really matters is that all the MO files are inside the same folder, based on our Windows Regional Format!

If this article was helpful to you, follow me on Facebook and Youtube! Leave a comment below to let me know what you think, or if you had any difficulties setting up gettext!

If you can, support my work! Preview image credits: Unsplash

Leave a comment

All comments will be subject to approval after being sent. They might be published after several hours.

You can just use a random nickname, it is usefull to allow me to at least answer your comments. And if you choose to submit your email, you can receive a notification whenever I reply to your comment.

*
  • 2023-09-11 17:27:24

    Author: eartahhj

    @Isaac thank you for your feedback!

  • 2023-09-11 17:18:04

    Author: Isaac

    great and helpful post. thanks