There are many approaches for adding new languages to your application’s UI. Though some userland solutions like symfony/translation are arguably simpler to use, they’re slower than the good old native gettext by an order of several magnitudes.

有多种方法可以向应用程序的UI添加新语言。 尽管可以说某些用户界面解决方案(例如symfony / translation)使用起来更简单,但它们比旧的本机gettext慢了几个数量级。

In this tutorial, we’ll modify an English-only application to use gettext. Through this, we’ll demonstrate that getting internationalization up and running in an already existing app is not only possible, but relatively easy.

在本教程中,我们将修改仅英语的应用程序以使用gettext。 通过这一点,我们将证明在已经存在的应用程序中启动并运行国际化不仅可能,而且相对容易。

The application in question will be our own nofw – a ready-to-use skeleton app.

有问题的应用程序将是我们自己的nofw –即用型骨架应用程序

Do you speak English?

引导和基础 (Bootstrapping and Basics)

We’ll be using our trusty Homestead Improved as always as an environment – if you’d like to follow along, please fire it up. Our box already has gettext installed and activated. We’ll see how to manually install it for deployment purposes at the end of this tutorial.

我们将一如既往地在环境中使用值得信赖的Homestead改进型 –如果您想跟进,请启动它。 我们的盒子已经安装并激活了gettext。 在本教程的最后,我们将看到如何出于部署目的手动安装它。

Since nofw uses Twig, we’ll need the i18n extension. To start the project off right, here’s the full process:

由于nofw使用Twig ,因此我们需要i18n扩展名 。 要立即开始该项目,请执行以下完整过程:

git clone
cd nofw
git checkout tags/2.93 -b 2.93
composer require twig/extensions

Note: the above commands clone an older version of nofw – one without the internationalization features built in – so that readers can follow along with the tutorial.

注意:上面的命令克隆了nofw的较旧版本 (没有内置的国际化功能),因此读者可以随本教程一起学习。

This will install both Twig’s extensions, and all the project’s dependencies. Follow the procedure from the README to set up the rest of the nofw app (the database end), then return to this post.

这将安装Twig的扩展以及项目的所有依赖项。 按照自述文件中的过程设置其余的nofw应用程序(数据库端),然后返回本文。

The app should be up and running now.


Nofw working

The syntax for getting a translatable string is gettext("string") or its alias: _("string") – that is, _() is the function we call and "string" is the string we’re translating. If a translation for "string" isn’t found, then the original (which is considered a placeholder) value is returned. Placeholders are usually full strings in the most popular language for the site’s audience, so that if translation fails for some reason, readable text is still rendered.

获取可翻译字符串的语法是gettext("string")或其别名: _("string") –即_()是我们调用的函数,而"string"是我们正在翻译的字符串。 如果找不到"string"的翻译,则返回原始值(被视为占位符)。 对于网站的受众来说,占位符通常是使用最流行语言的完整字符串,因此,如果由于某种原因翻译失败,仍会呈现可读文本。

Let’s try and make this work on a bogus PHP file, one that isn’t being powered by Twig, just to make sure everything is in working order. We’ll use the example from the old gettext post series. In the root of the project, we’ll make a file called i18n.php and give it the contents:

让我们尝试在一个伪造PHP文件上运行此文件,该文件不是Twig所提供的,只是为了确保一切正常。 我们将使用旧的gettext帖子系列中的示例。 在项目的根目录中,我们将创建一个名为i18n.php的文件并i18n.php提供内容:


$language = "en_US.UTF-8";
putenv("LANGUAGE=" . $language); 
setlocale(LC_ALL, $language);

$domain = "messages"; // which language file to use
bindtextdomain($domain, "Locale"); 
bind_textdomain_codeset($domain, 'UTF-8');


echo _("HELLO_WORLD");

In the same folder, let’s create a folder structure like this one:


Folder structure

Describing the code above, we first set the OS environment’s language as US English, then save that as an environment variable. PHP’s setlocale function uses the LC_ALL constant to switch all contexts to the given locale – so PHP will try to convert dates, numeric formatting, even currency to the locale we give it. Naturally, LC_ALL includes our custom translated messages, too.

在上面的代码描述中,我们首先将OS环境的语言设置为US English,然后将其保存为环境变量。 PHP的setlocale函数使用LC_ALL常量将所有上下文切换到给定的语言环境-因此PHP将尝试将日期,数字格式甚至货币转换为我们指定的语言环境。 当然, LC_ALL包括我们的自定义翻译消息。

The $domain field is there to tell PHP which language file to use – the language file will be called messages.po in its raw, editable form, and in its compiled, machine readable form. bindtextdomain merely sets the path of the language file, which as we know is inside the Locale folder, and bind_textdomain_codeset will set the language character set. UTF-8 is a pretty universally safe bet here.

$domain场有告诉PHP要使用的语言文件-语言文件将被称为messages.po在其原始的,可编辑的形式,并messages.mo在其编译,机器可读的形式。 bindtextdomain仅设置语言文件的路径,众所周知,该文件位于Locale文件夹中, bind_textdomain_codeset将设置语言字符集。 UTF-8是一个非常普遍的选择。

Finally, textdomain sets the active domain to be used.

最后, textdomain设置要使用的活动域。

Running this test script in the command line would echo the placeholder: HELLO_WORLD. Obviously, it’s missing the actual language file. It’s time to create it.

在命令行中运行此测试脚本将回显占位符: HELLO_WORLD 。 显然,它缺少实际的语言文件。 是时候创建它了。

萃取 (Extraction)

Gettext comes with a handy tool for extracting placeholder strings from files. In the root of the project, we’ll execute:

Gettext附带了一个方便的工具,用于从文件中提取占位符字符串。 在项目的根目录中,我们将执行:

xgettext --from-code=UTF-8 -o Locale/messages.pot public/i18n.php

Above, xgettext will use the UTF-8 encoding to output (-o) harvested strings from public/i18n.php into the given file. Inspecting the resulting messages.pot file now gives:

上面的xgettext将使用UTF-8编码将public/i18n.php到的字符串( -o )输出到给定文件中。 现在检查生成的messages.pot文件可以得到:

# This file is distributed under the same license as the PACKAGE package.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-04-10 10:44+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: public/i18n.php:13
msgstr ""

.pot stands for portable object template. These template files are used to build other language files. If we decide to add Japanese to our app later on, the .pot file will be used to generate a Locale/ja_JP/LC_MESSAGES/messages.po which will, in turn, be used to generate the respective file. Let’s use this approach to generate the en_US messages file now:

.pot代表可portable object template 。 这些模板文件用于构建其他语言文件。 如果以后我们决定将日语添加到我们的应用程序中,则.pot文件将用于生成Locale/ja_JP/LC_MESSAGES/messages.po ,而该Locale/ja_JP/LC_MESSAGES/messages.po又将用于生成相应的messages.mo文件。 让我们现在使用这种方法来生成en_US消息文件:

msginit --locale=en_US --output-file=Locale/en_US/LC_MESSAGES/messages.po --input=Locale/messages.pot

This process needs to be repeated for every new language we want added to the app.


The .po file is very similar to the .pot file from before, only it contains actual translation strings we can edit:


# English translations for PACKAGE package.
# This file is distributed under the same license as the PACKAGE package.
# vagrant <vagrant@homestead>, 2016.
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-04-10 10:44+0000\n"
"PO-Revision-Date: 2016-04-10 10:58+0000\n"
"Last-Translator: vagrant <vagrant@homestead>\n"
"Language-Team: English\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: public/i18n.php:13
msgstr "HELLO_WORLD"

After replacing the msgstr value of HELLO_WORLD with Howdy, we should compile the .po file into a .mo file Gettext can read:

HELLO_WORLDmsgstr值替换为Howdy ,我们应该将.po文件编译成.mo文件,Gettext可以读取:

msgfmt -c -o Locale/en_US/LC_MESSAGES/ Locale/en_US/LC_MESSAGES/messages.po

新增语言 (Adding a new language)

To be sure things work, let’s add a new language – hr_HR (Croatian).


  1. First, we install the new locale onto the OS with:


    sudo locale-gen hr_HR hr_HR.UTF-8
    sudo update-locale
    sudo dpkg-reconfigure locales
  2. We then generate new .po files from the .pot files:


    mkdir -p Locale/hr_HR/LC_MESSAGES
    msginit --locale=hr_HR --output-file=Locale/hr_HR/LC_MESSAGES/messages.po --input=Locale/messages.pot
  3. Next, we change the HELLO_WORLD value into Zdravo, then generate the .mo file:

    接下来,我们将HELLO_WORLD值更改为Zdravo ,然后生成.mo文件:

    msgfmt -c -o Locale/hr_HR/LC_MESSAGES/ Locale/hr_HR/LC_MESSAGES/messages.po
  4. Finally, we change the locale setting in the PHP file to hr_HR.UTF-8 and test.


Everything should be working fine.


Note: a restart of the web server and / or PHP-FPM might be necessary to clear the gettext cache.


枝条 (Twig)

Now that we know that gettext works fine and we’re able to add new languages on a whim, let’s see how it behaves in conjunction with Twig. First, let’s add the following into app/config/config_web.php, at the very top:

既然我们知道gettext可以正常工作,并且我们能够一时兴起地添加新语言,接下来让我们看看它与Twig一起如何工作。 首先,让我们将以下内容添加到app/config/config_web.php的最顶部:

$language = "hr_HR.UTF-8";
putenv("LANGUAGE=" . $language);
setlocale(LC_ALL, $language);

$domain = "messages"; // which language file to use
bindtextdomain($domain, __DIR__."/../../Locale");
bind_textdomain_codeset($domain, 'UTF-8');


For Twig to work with translatable strings, it needs the i18n extension we installed during the bootstrapping section. Then, in the templates, we use the trans block:

为了使Twig使用可翻译字符串,它需要我们在引导部分中安装的i18n扩展名。 然后,在模板中,我们使用trans块:

{% trans %}
    Hello {{ name }}!
{% endtrans %}

Of course, gettext has no idea what {{name}} is supposed to mean so Twig’s extension automatically compiles this into the gettext-friendly Hello %name%!. One caveat is that xgettext isn’t equipped to extract twig strings, so we need an alternative as per the docs.

当然,gettext不知道{{name}}含义,因此Twig的扩展名自动将其编译为对gettext友好的Hello %name%! 。 需要注意的是xgettext不能提取枝条字符串,因此根据docs我们需要一种替代方法。

We’ll compile our view templates into the system’s temporary folder, and then xgettext those, like regular PHP files!


First, let’s add a translatable message to one of the files. For example, somewhere into Standard/Views/home.twig, we can put:

首先,让我们向其中一个文件添加可翻译的消息。 例如,在Standard/Views/home.twig ,我们可以放置:

{% trans %}
        This is translatable
    {% endtrans %}

Then, in app/bin, we’ll create a new file: twigcache.php:

然后,在app/bin ,我们将创建一个新文件: twigcache.php


require __DIR__.'/../../vendor/autoload.php';
$shared = require __DIR__.'/../config/shared/root.php';

$tplDir = dirname(__FILE__) . '/templates';
$tmpDir = '/tmp/cache/';
$loader = new Twig_Loader_Filesystem($shared['site']['viewsFolders']);

// force auto-reload to always have the latest version of the template
$twig = new Twig_Environment(
    $loader, [
    'cache' => $tmpDir,
    'auto_reload' => true,
$twig->addExtension(new Twig_Extensions_Extension_I18n());
// configure Twig the way you want

// iterate over all your templates
foreach ($shared['site']['viewsFolders'] as $tplDir) {
    foreach (new RecursiveIteratorIterator(
                 new RecursiveDirectoryIterator($tplDir),
             ) as $file) {
        // force compilation
        if ($file->isFile()) {
            $twig->loadTemplate(str_replace($tplDir . '/', '', $file));

This file pulls in the common root.php configuration file in which view folders are defined, and as such we only need to update them in one place. Executing the script with php app/bin/twigcache.php now produces a directory tree with PHP cache files:

该文件提取定义了视图文件夹的通用root.php配置文件,因此,我们只需要在一个地方更新它们即可。 现在,使用php app/bin/twigcache.php执行脚本会生成包含PHP缓存文件的目录树:

├── 1a
│   └── 1ad38dfd106734cda72279c3bbd83dd4c64d93ff9c713afb1e74904144018347.php
├── 1c
│   └── 1ca70331199383cea2ce308ab09447cebd7e5e81f2a7f5caa319d577f3a66682.php
├── df
│   └── df75e14ad2cb55315ab205872c8b8590ffde333912ec5c89e44c365479bfe457.php
└── f4
    └── f444ff725954cd5a9ec29ceb56a9cbf7eda8a273cea96c542c35a271e0f57c7e.php

We can unleash xgettext on this collection now:


xgettext -o Locale/messages.pot --from-code=UTF-8 -n --omit-header /tmp/cache/*/*.ph

Inspecting Locale/message.pot now reveals entirely new contents:


#: /tmp/cache/d0/d006e63c5a4c4e6a700d9273d4523dd0cf419105fa4b00cf6b89918c67df4b2b.php:56
msgid "This is translatable"
msgstr ""

As before, we can now create the .po files for our two pre-installed languages.


msgmerge -U Locale/en_US/LC_MESSAGES/messages.po Locale/messages.pot
msgmerge -U Locale/hr_HR/LC_MESSAGES/messages.po Locale/messages.pot

The msgmerge command merges the changes from messages.pot into the defined messages.po file. We use msgmerge instead of msginit here for convenience, but we could have also used msginit to start a new language file. Merge has an added bonus, though: seeing as xgettext no longer looked for translatable strings in the i18n.php from the example above, the newly updated .po files actually have the previously used string-value pair commented out:

msgmerge命令将来自messages.pot的更改合并到已定义的messages.po文件中。 为了方便起见,我们在这里使用msgmerge而不是msginit ,但是我们也可以使用msginit来启动新的语言文件。 但是,合并还有一个额外的好处:鉴于xgettext在上面的示例中不再在i18n.php寻找可翻译的字符串,因此新更新的.po文件实际上已注释掉了以前使用的字符串值对:

#: /tmp/cache/d0/d006e63c5a4c4e6a700d9273d4523dd0cf419105fa4b00cf6b89918c67df4b2b.php:56
msgid "This is translatable"
msgstr "Yes, this is totally translatable"

#~ msgid "HELLO_WORLD"
#~ msgstr "Howdy"

This makes it easy to track deprecated translations without actually losing the effort it took to make them.


Assuming we changed some translation values, let’s compile to .mo now and test:


msgfmt -c -o Locale/hr_HR/LC_MESSAGES/ Locale/hr_HR/LC_MESSAGES/messages.po
msgfmt -c -o Locale/en_US/LC_MESSAGES/ Locale/en_US/LC_MESSAGES/messages.po
Working Croatian translation

Notice our translated string at the bottom there – everything works as expected!


Granted, the configuration we pasted to the top of config_web.php could use some work – like detecting the desired language through routes etc, but for brevity, this works fine.


Now all that’s left is hunting down all the strings in all the views and turning them into {% trans %} blocks!

现在剩下的就是在所有视图中搜寻所有字符串并将它们变成{% trans %}块!

奖励:脚本! (Bonus: Scripts!)

While the process above isn’t exactly complicated, it’d be simpler not to have to type out those long commands for every little thing. With more languages, things get more convoluted and confusing rather quickly, and it becomes ever easier to make a typo when punching in those shell commands. That’s why we can put together some shortcut bash scripts to help us out.

尽管上面的过程并不十分复杂,但不必为每个小事情都键入那些长命令会更简单。 使用更多的语言,事情变得更加令人费解和混乱,并且在打入这些shell命令时打错字变得越来越容易。 这就是为什么我们可以组合一些快捷的bash脚本来帮助我们的原因。

Note: if you’re not using nofw and don’t intend to, feel free to skip this section and/or just harvest what you think is useful from it. Likewise, note that all these scripts are meant to be run from the root folder of the project.

注意:如果您不打算使用nofw且不想这样做,请随时跳过本节和/或仅收获您认为有用的内容。 同样,请注意,所有这些脚本均应从项目的根文件夹中运行。

We’ll put all these scripts into app/bin/i18n/ and make them executable on the command line:


touch app/bin/i18n/{,,,}
chmod +x app/bin/i18n/*.sh

设定档 (Config)


[[ -f app/bin/i18n/ ]] && source app/bin/i18n/

This script will be included by other scripts, which allows users to change the desired folder for the locales. Likewise, it contains the name of the non-sudo user. As it’s generally a bad idea to have many sudo commands inside a bash script, and we’ll certainly need to execute a lot of them with root privileges, we’ll opt to execute the whole script with sudo and then just drop privileges to the regular user on those commands that don’t need sudo. The user defaults to “forge” because that’s the user Laravel Forge sets up.

该脚本将包含在其他脚本中,这些脚本允许用户更改所需的语言环境文件夹。 同样,它包含非sudo用户的名称。 由于在bash脚本中包含许多sudo命令通常是一个坏主意 ,并且我们当然需要使用root特权执行许多命令,因此,我们将选择使用sudo执行整个脚本,然后将特权删除那些不需要sudo命令的普通用户。 用户默认为“伪造”,因为那是用户Laravel Forge设置的

This script also includes another config script if it exists (because it’s in .gitignore and won’t exist on live servers) in which the username can be overridden. This is useful for local development. For example, when using Homestead Improved, everything will be run from the vagrant user’s perspective and the forge user doesn’t exist.

该脚本还包括另一个配置脚本( 如果存在) (因为它位于.gitignore并且在实时服务器上将不存在),在其中可以覆盖用户名。 这对于本地发展很有用。 例如,当使用Homestead Improvementd时 ,所有内容都将从vagrant的角度运行,并且forge用户不存在。

新语言/刷新语言脚本 (New language / refresh languages script)

#!/usr/bin/env bash


source app/bin/i18n/

if [[ $EUID -ne 0 ]]; then
   echo "This script must be run as root" 1>&2
   exit 1

if [ -z "$1" ]; then
for folder in $(find ${LOCALE_FOLDER} -maxdepth 1 -type d | awk -F/ '{print $NF}')
    if [ "${folder}" != ${LOCALE_FOLDER} ]; then
        echo "Executing locale-gen ${folder} ${folder}.UTF-8"
        locale-gen ${folder} ${folder}.UTF-8
echo "Executing updates..."
dpkg-reconfigure locales

if [ -n "$1" ]; then
echo "Executing locale-gen $1 $1.UTF-8"
locale-gen $1 $1.UTF-8

echo "Executing updates..."
dpkg-reconfigure locales

echo "Creating folder: ${LOCALE_FOLDER}/$1/LC_MESSAGES"

This will immediately install any locale passed in as the first argument, like so:


sudo app/bin/i18n/ ja_JP

It will also create the appropriate language folder in the Locale folder.


If no parameter was passed in, then this script will look for expected locales by traversing the Locale folder, and auto-installing each of the locales as per the subfolder name.


I.e., if there are folders Locale/en_US and Locale/hr_HR, it will be as if we had run sudo app/bin/i18n/ en_US and sudo app/bin/i18n/ hr_HR. This helps auto-install locales during deployment.

即,如果有文件夹Locale/en_USLocale/hr_HR ,就好像我们已经运行了sudo app/bin/i18n/ en_USsudo app/bin/i18n/ hr_HR 。 这有助于在部署过程中自动安装语言环境。

This script needs to be run as root because the locale-related commands require elevated privileges.


刷新锅脚本 (Refresh pot script)

#!/usr/bin/env bash


source app/bin/i18n/

echo "Regenerating cache"
php app/bin/twigcache.php

echo "Running xgettext on the cached files"
xgettext -o ${LOCALE_FOLDER}/messages.pot --from-code=UTF-8 -n --omit-header /tmp/cache/*/*.php

for folder in $(find ${LOCALE_FOLDER} -maxdepth 1 -type d | awk -F/ '{print $NF}')
    if [ "${folder}" != ${LOCALE_FOLDER} ]; then
        if [[ -f ${LOCALE_FOLDER}/${folder}/LC_MESSAGES/messages.po ]]; then
            echo "Merging for ${folder}"
            msgmerge -U Locale/${folder}/LC_MESSAGES/messages.po ${LOCALE_FOLDER}/messages.pot
            echo "Initializing for ${folder}"
            msginit --locale=${folder} --output-file=${LOCALE_FOLDER}/${folder}/LC_MESSAGES/messages.po --input=${LOCALE_FOLDER}/messages.pot

This regenerates the view cache, unleashes xgettext on it, and merges the result with the current .pot file, if any. It then uses the refreshed .pot file to update the .po files. Notice it uses msginit if the language hasn’t been initialized yet, and msgmerge otherwise.

这将重新生成视图缓存,在其上释放xgettext ,并将结果与​​当前.pot文件合并(如果有)。 然后,它使用刷新的.pot文件来更新.po文件。 请注意,如果尚未初始化语言,它将使用msginit否则将使用msgmerge

重新编译脚本 (Recompile script)

#!/usr/bin/env bash


source app/bin/i18n/

for folder in $(find ${LOCALE_FOLDER} -maxdepth 1 -type d | awk -F/ '{print $NF}')
    if [ "${folder}" != ${LOCALE_FOLDER} ]; then
        echo "Compiling .mo for ${folder}"
        msgfmt -c -o Locale/${folder}/LC_MESSAGES/ ${LOCALE_FOLDER}/${folder}/LC_MESSAGES/messages.po

The recompilation script is supposed to be run after edits to .po files have been made. It makes the edits ready for use, and allows the translations to appear on the site.

重新编译脚本应该在对.po文件进行编辑后运行。 它使编辑可以使用,并允许翻译显示在网站上。

部署中 (Deploying)

Deployment of these language-specific upgrades will depend on the deployment approach applied to the app. We could be using Deployer, we could be using Forge, or something else entirely. What ever the case, before we even try out our scripts above we’ll need to make sure that:

这些特定于语言的升级的部署将取决于应用于该应用程序的部署方法。 我们可能正在使用Deployer ,我们可能正在使用Forge或其他完全使用的东西。 无论如何,在我们尝试上面的脚本之前,我们需要确保:

  1. gettext is installed and activated

  2. the necessary locales have been generated on the OS


On Ubuntu, this is easily skipped by making sure the following commands run at the end of the deployment process:


sudo apt-get install gettext
sudo app/bin/i18n/

The rest is automatic, seeing as .pot, .po and .mo files are meant to be committed alongside the application’s source code.


Note that you’ll need to modify both the installation command and the shell scripts above if you’re using something other than Ubuntu


结论 (Conclusion)

In this tutorial, we looked at adding internationalization features to an existing application powered by Twig. We demonstrated the use of gettext on a mock no-Twig file, made sure everything worked, and then went through a step by step integration with Twig. Finally, we wrote some shortcut scripts that can help tremendously when sharing the project or deploying it to production.

在本教程中,我们研究了如何在Twig支持的现有应用程序中添加国际化功能。 我们演示了在模拟的非Twig文件上使用gettext,确保一切正常,然后与Twig进行了逐步集成。 最后,我们编写了一些快捷脚本,这些脚本在共享项目或将其部署到生产环境时可以提供极大帮助。

Do you use gettext? Or do your apps take a different approach? Let us know in the comments!

您使用gettext吗? 还是您的应用采用了不同的方法? 让我们在评论中知道!



  • 0
  • 0
  • 0
  • 一键三连
  • 扫一扫,分享海报

评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
钱包余额 0