Welcome! / 欢迎!

开发实践

基础知识

PHP 是一门庞大的语言,各个水平层次的开发者都可以利用它进行迅捷高效的开发。然而在对语言逐渐深入的学习过程中,我们往往会因为走捷径和/或不良习惯而忘记(或忽视掉)基础的知识。为了帮助彻底解决这个问题,这一章的目的就是提醒开发人员注意有关 PHP 的基础编程实践。

日期和时间

PHP 中 DateTime 类的作用是在你读、写、比较或者计算日期和时间时提供帮助。除了 DateTime 类之外,PHP 还有很多与日期和时间相关的函数,但 DateTime 类为大多数常规使用提供了优秀的面向对象接口。它还可以处理时区,不过这并不在这篇简短的介绍之内。

在使用 DateTime 之前,通过 createFromFormat() 工厂方法将原始的日期与时间字符串转换为对象或使用 new DateTime 来取得当前的日期和时间。使用 format() 将 DateTime 转换回字符串用于输出。

<?php
$raw = '22. 11. 1968';
$start = DateTime::createFromFormat('d. m. Y', $raw);
 
echo 'Start date: ' . $start->format('Y-m-d') . "\n";

对 DateTime 进行计算时可以使用 DateInterval 类。DateTime 类具有例如 add()sub() 等将 DateInterval 当作参数的方法。编写代码时注意不要认为每一天都是由相同的秒数构成的,不论是夏令时(DST)还是时区转换,使用时间戳计算都会遇到问题,应当选择日期间隔。使用 diff() 方法来计算日期之间的间隔,它会返回新的 DateInterval,非常容易进行展示。

<?php
// create a copy of $start and add one month and 6 days
$end = clone $start;
$end->add(new DateInterval('P1M6D'));
 
$diff = $end->diff($start);
echo 'Difference: ' . $diff->format('%m month, %d days (total: %a days)') . "\n";
// Difference: 1 month, 6 days (total: 37 days)

DateTime 对象之间可以直接进行比较:

<?php
if ($start < $end) {
    echo "Start is before end!\n";
}

最后一个例子来演示 DatePeriod 类。它用来对循环的事件进行迭代。向它传入开始时间、结束时间和间隔区间,会得到这其中所有的事件。

<?php
// output all thursdays between $start and $end
$periodInterval = DateInterval::createFromDateString('first thursday');
$periodIterator = new DatePeriod($start, $periodInterval, $end, DatePeriod::EXCLUDE_START_DATE);
foreach ($periodIterator as $date) {
    // output each date in the period
    echo $date->format('Y-m-d') . ' ';
}

一个有名的 API 扩展是 Carbon。Carbon 不仅继承了所有 DateTime 类提供的功能,还提供了更多的人性化功能,例如自然语言时间处理、国际化支持、对象之间执行增减算术。

  • [阅读 DateTime][datetime]
  • [阅读日期格式][dateformat] (支持的日期字符串格式)

设计模式

当你在编写自己的应用程序时,最好在项目的代码和整体架构中使用通用的设计模式,这将帮助你更轻松地对程序进行维护,也能够让其他的开发者更快地理解你的代码。

当你使用框架进行开发时,绝大部分的上层代码以及项目结构都会基于所使用的框架,因此很多关于设计模式的决定已经由框架帮你做好了。当然,你还是可以挑选你最喜欢的模式并在你的代码中进行应用。但如果你并没有使用框架的话,你就需要自己去寻找适合你的应用的最佳模式了。

使用 UTF-8 编码

本章是由 Alex Cabal 最初撰写在 PHP Best Practices 中的,我们使用它作为进行建议的基础

这不是在开玩笑。请小心、仔细并且前后一致地处理它。

目前,PHP 仍未在底层实现对 Unicode 的支持。虽然有很多途径可以确保 UTF-8 字符串能够被正确地处理,但这并不是很简单的事情,通常需要对 Web 应用进行全方面的检查,从 HTML 到 SQL 再到 PHP。我们将争取进行一个简洁实用的总结。

PHP 层面的 UTF-8

最基本的字符串操作,像是连结两个字符串或将字符串赋值给变量,并不需要对 UTF-8 做特别的处理。然而大多数字符串的函数,像 strpos()strlen(),确实需要特别的处理。这些函数名中通常包含 mb_*:比如,mb_strpos()mb_strlen()。这些 mb_* 字符串是由 [Multibyte String Extension] 提供支持的,它专门为操作 Unicode 字符串而特别进行了设计。

在操作 Unicode 字符串时,请你务必使用 mb_* 函数。例如,如果你对一个 UTF-8 字符串使用 substr(),那返回的结果中有很大可能会包含一些乱码。正确的方式是使用 mb_substr()

最难的地方在于每次都要记得使用 mb_* 函数。如果你哪怕只有一次忘记了使用,你的 Unicode 字符串就有在接下来的过程中变成乱码的风险。

不是所有的字符串函数都有一个对应的 mb_* 函数。如果你想要的功能没有对应的 mb_* 函数的话,那只能说你运气不佳了。

你应该在你所有的 PHP 脚本(或全局包含的脚本)的开头使用 mb_internal_encoding() 函数,然后紧接着在会对浏览器进行输出的脚本中使用 mb_http_output()。在每一个脚本当中明确声明字符串的编码可以免去很多日后的烦恼。

另外,许多对字符串进行操作的函数都有一个可选的参数用来指定字符串编码。当可以设定这类参数时,你应该始终明确指定使用 UTF-8。例如,htmlentities() 有一个字符编码的选项,你应该始终将其设为 UTF-8。从 PHP 5.4.0 开始, htmlentities()htmlspecialchars() 的编码都已经被默认设为了 UTF-8。

最后,如果你所编写的是分布式的应用程序并且不能确定 mbstring 扩展一定开启的话,可以考虑使用 [patchwork/utf8] Composer 包。它会在 mbstring 可用时自动使用,否则自动切换回非 UTF-8 函数。

数据库层面的 UTF-8

如果你使用 PHP 来操作到 MySQL,有些时候即使你做到了上面的每一点,你的字符串仍可能面临在数据库中以非 UTF-8 的格式进行存储的问题。

为了确保你的字符串从 PHP 到 MySQL都使用 UTF-8,请检查确认你的数据库和数据表都设定为 utf8mb4 字符集和整理,并且确保你的 PDO 连接请求也使用了 utf8mb4 字符集。请看下方的示例代码,这是 非常重要 的。

请注意为了完整的 UTF-8 支持,你必须使用 utf8mb4 而不是 utf8!你会在进一步阅读中找到原因。

浏览器层面的 UTF-8

使用 mb_http_output() 函数来确保 PHP 向浏览器输出 UTF-8 格式的字符串。

随后浏览器需要接收 HTTP 应答来指定页面是由 UTF-8 进行编码的。以前这一步是通过在页面 <head> 标签下包含字符集 `<meta>` 标签实现的,这是一种可行的方式。但更好的做法是在 Content-Type 响应头中进行设置,因为这样做的速度会更快

<?php
// Tell PHP that we're using UTF-8 strings until the end of the script
mb_internal_encoding('UTF-8');
 
// Tell PHP that we'll be outputting UTF-8 to the browser
mb_http_output('UTF-8');
 
// Our UTF-8 test string
$string = 'Êl síla erin lû e-govaned vîn.';
 
// Transform the string in some way with a multibyte function
// Note how we cut the string at a non-Ascii character for demonstration purposes
$string = mb_substr($string, 0, 15);
 
// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note the `charset=utf8mb4` in the Data Source Name (DSN)
$link = new PDO(
    'mysql:host=your-hostname;dbname=your-db;charset=utf8mb4',
    'your-username',
    'your-password',
    array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_PERSISTENT => false
    )
);
 
// Store our transformed string as UTF-8 in our database
// Your DB and tables are in the utf8mb4 character set and collation, right?
$handle = $link->prepare('insert into ElvishSentences (Id, Body) values (?, ?)');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
 
// Retrieve the string we just stored to prove it was stored correctly
$handle = $link->prepare('select * from ElvishSentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
 
// Store the result into an object that we'll output later in our HTML
$result = $handle->fetchAll(\PDO::FETCH_OBJ);
 
header('Content-Type: text/html; charset=UTF-8');
?><!doctype html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>UTF-8 test page</title>
    </head>
    <body>
        <?php
        foreach($result as $row){
            print($row->Body);  // This should correctly output our transformed UTF-8 string to the browser
        }
        ?>
    </body>
</html>

Further reading

国际化(i18n)和本地化(l10n)

_前置声明:i18n 和 l10n 是使用数字简略拼写方式来实现缩写,在我们的例子里:internationalization 是 i18n,而 localization 简写为 l10n。

首先,我们需要定义这两个相似的概念,还有相关的概念:

  • Internationalization 国际化:指的是一开始设计一个支持多语言的架构。很多时候这个事情只需要做一次,并且是在项目初始时,不然的话,你可能面临一个项目的重大修改。
  • Localization 本地化:指的是新语言的添加。基于 i18n 的架构设计,在每一次新支持一门语言时,我们都需要一点点的去增加翻译的语言。
  • Pluralization 复数形式:不同语言复数规则不一样,即使是相同语言里也会出现不同复数规则,例如大部分英文名词后面加 s 为复数,有一些单词如 knowledge 就没有复数形式。俄语和塞尔威亚语有两种复数的形式,甚至有一些语言,如斯洛维尼亚语、爱尔兰语和阿拉伯语会存在 4、5 或者是 6 种复数形式。

一般实现的方法

最简便的方式是使用数组键值对应的方式如 <h1><?=$TRANS['title_about_page']?></h1>,不过在比较正经的项目中,不建议这么做。因为会随着项目代码慢慢变多,维护的难度将会增加,尤其会阻碍后续本地化实施。

The most classic way and often taken as reference for i18n and l10n is a [Unix tool called `gettext`][gettext]. It dates back to 1995 and is still a complete implementation for translating software. It is pretty easy to get running, while it still sports powerful supporting tools. It's about Gettext we will be talking here. Also, to help you not get messy over the command-line, we will be presenting a great GUI application that can be used to easily update your l10n source files.

Other tools

There are common libraries used that support Gettext and other implementations of i18n. Some of them may seem easier to install or sport additional features or i18n file formats. In this document, we focus on the tools provided with the PHP core, but here we list others for completion:

  • [oscarotero/Gettext][oscarotero]: Gettext support with an OO interface; includes improved helper functions, powerful extractors for several file formats (some of them not supported natively by the gettext command), and can also export to other formats besides .mo/.po files. Can be useful if you need to integrate your translation files into other parts of the system, like a JavaScript interface.
  • [symfony/translation][symfony]: supports a lot of different formats, but recommends using verbose XLIFF's. Doesn't include helper functions nor a built-in extractor, but supports placeholders using strtr() internally.
  • [zend/i18n][zend]: supports array and INI files, or Gettext formats. Implements a caching layer to save you from reading the filesystem every time. It also includes view helpers, and locale-aware input filters and validators. However, it has no message extractor.

Other frameworks also include i18n modules, but those are not available outside of their codebases: - [Laravel] supports basic array files, has no automatic extractor but includes a @lang helper for template files. - [Yii] supports array, Gettext, and database-based translation, and includes a messages extractor. It is backed by the [`Intl`][intl] extension, available since PHP 5.3, and based on the [ICU project]; this enables Yii to run powerful replacements, like spelling out numbers, formatting dates, times, intervals, currency, and ordinals.

If you decide to go for one of the libraries that provide no extractors, you may want to use the gettext formats, so you can use the original gettext toolchain (including Poedit) as described in the rest of the chapter.

Gettext

Installation

You might need to install Gettext and the related PHP library by using your package manager, like apt-get or yum. After installed, enable it by adding extension=gettext.so (Linux/Unix) or extension=php_gettext.dll (Windows) to your php.ini.

Here we will also be using [Poedit] to create translation files. You will probably find it in your system's package manager; it's available for Unix, Mac, and Windows, and can be [downloaded for free on their website][poedit_download] as well.

Structure

Types of files

There are three files you usually deal with while working with gettext. The main ones are PO (Portable Object) and MO (Machine Object) files, the first being a list of readable “translated objects” and the second, the corresponding binary to be interpreted by gettext when doing localization. There's also a POT (Template) file, that simply contains all existing keys from your source files, and can be used as a guide to generate and update all PO files. Those template files are not mandatory: depending on the tool you're using to do l10n, you can go just fine with only PO/MO files. You'll always have one pair of PO/MO files per language and region, but only one POT per domain.

Domains

There are some cases, in big projects, where you might need to separate translations when the same words convey different meaning given a context. In those cases, you split them into different domains. They're basically named groups of POT/PO/MO files, where the filename is the said translation domain. Small and medium-sized projects usually, for simplicity, use only one domain; its name is arbitrary, but we will be using “main” for our code samples.
In [Symfony] projects, for example, domains are used to separate the translation for validation messages.

Locale code

A locale is simply a code that identifies one version of a language. It's defined following the [ISO 639-1][639-1] and [ISO 3166-1 alpha-2][3166-1] specs: two lower-case letters for the language, optionally followed by an underline and two upper-case letters identifying the country or regional code. For [rare languages][rare], three letters are used.

For some speakers, the country part may seem redundant. In fact, some languages have dialects in different countries, such as Austrian German (de_AT) or Brazilian Portuguese (pt_BR). The second part is used to distinguish between those dialects - when it's not present, it's taken as a “generic” or “hybrid” version of the language.

Directory structure

To use Gettext, we will need to adhere to a specific structure of folders. First, you'll need to select an arbitrary root for your l10n files in your source repository. Inside it, you'll have a folder for each needed locale, and a fixed LC_MESSAGES folder that will contain all your PO/MO pairs. Example:

{% highlight console %} <project root> ├─ src/ ├─ templates/ └─ locales/

  ├─ forum.pot
  ├─ site.pot
  ├─ de/
  │  └─ LC_MESSAGES/
  │     ├─ forum.mo
  │     ├─ forum.po
  │     ├─ site.mo
  │     └─ site.po
  ├─ es_ES/
  │  └─ LC_MESSAGES/
  │     └─ ...
  ├─ fr/
  │  └─ ...
  ├─ pt_BR/
  │  └─ ...
  └─ pt_PT/
     └─ ...

</code>

Plural forms

As we said in the introduction, different languages might sport different plural rules. However, gettext saves us from this trouble once again. When creating a new .po file, you'll have to declare the [plural rules][plural] for that language, and translated pieces that are plural-sensitive will have a different form for each of those rules. When calling Gettext in code, you'll have to specify the number related to the sentence, and it will work out the correct form to use - even using string substitution if needed.

Plural rules include the number of plurals available and a boolean test with n that would define in which rule the given number falls (starting the count with 0). For example:

  • Japanese: nplurals=1; plural=0 - only one rule
  • English: nplurals=2; plural=(n != 1); - two rules, first if N is one, second rule otherwise
  • Brazilian Portuguese: nplurals=2; plural=(n > 1); - two rules, second if N is bigger than one, first otherwise

Now that you understood the basis of how plural rules works - and if you didn't, please look at a deeper explanation on the [LingoHub tutorial][lingohub_plurals] -, you might want to copy the ones you need from a [list][plural] instead of writing them by hand.

When calling out Gettext to do localization on sentences with counters, you'll have to give him the related number as well. Gettext will work out what rule should be in effect and use the correct localized version. You will need to include in the .po file a different sentence for each plural rule defined.

Sample implementation

After all that theory, let's get a little practical. Here's an excerpt of a .po file - don't mind with its format, but instead the overall content, you'll learn how to edit it easily later:

{% highlight po %} msgid “” msgstr “” “Language: pt_BR\n” “Content-Type: text/plain; charset=UTF-8\n” “Plural-Forms: nplurals=2; plural=(n > 1);\n”

msgid “We're now translating some strings” msgstr “Nós estamos traduzindo algumas strings agora”

msgid “Hello %1$s! Your last visit was on %2$s” msgstr “Olá %1$s! Sua última visita foi em %2$s”

msgid “Only one unread message” msgid_plural “%d unread messages” msgstr[0] “Só uma mensagem não lida” msgstr[1] “%d mensagens não lidas” </code>

The first section works like a header, having the msgid and msgstr especially empty. It describes the file encoding, plural forms and other things that are less relevant. The second section translates a simple string from English to Brazilian Portuguese, and the third does the same, but leveraging string replacement from [`sprintf`][sprintf] so the translation may contain the user name and visit date.
The last section is a sample of pluralization forms, displaying the singular and plural version as msgid in English and their corresponding translations as msgstr 0 and 1 (following the number given by the plural rule). There, string replacement is used as well so the number can be seen directly in the sentence, by using %d. The plural forms always have two msgid (singular and plural), so it's advised to not use a complex language as the source of translation.

Discussion on l10n keys

As you might have noticed, we're using as source ID the actual sentence in English. That msgid is the same used throughout all your .po files, meaning other languages will have the same format and the same msgid fields but translated msgstr lines.

Talking about translation keys, there are two main “schools” here:

  1. msgid as a real sentence.
    The main advantages are: - if there are pieces of the software untranslated in any given language, the key displayed will still maintain some meaning. Example: if you happen to translate by heart from English to Spanish but need help to translate to French, you might publish the new page with missing French sentences, and parts of the website would be displayed in English instead; - it's much easier for the translator to understand what's going on and make a proper translation based on the msgid; - it gives you “free” l10n for one language - the source one; - The only disadvantage: if you need to change the actual text, you would need to replace the same msgid across several language files.
  2. msgid as a unique, structured key.
    It would describe the sentence role in the application in a structured way, including the template or part where the string is located instead of its content. - it's a great way to have the code organized, separating the text content from the template logic. - however, that could bring problems to the translator that would miss the context. A source language file would be needed as a basis for other translations. Example: the developer would ideally have an en.po file, that translators would read to understand what to write in fr.po for instance. - missing translations would display meaningless keys on screen (top_menu.welcome instead of Hello there, User! on the said untranslated French page). That's good it as would force translation to be complete before publishing - but bad as translation issues would be really awful in the interface. Some libraries, though, include an option to specify a given language as “fallback”, having a similar behavior as the other approach.

The [Gettext manual][manual] favors the first approach as, in general, it's easier for translators and users in case of trouble. That's how we will be working here as well. However, the [Symfony documentation][symfony-keys] favors keyword-based translation, to allow for independent changes of all translations without affecting templates as well.

Everyday usage

In a common application, you would use some Gettext functions while writing static text in your pages. Those sentences would then appear in .po files, get translated, compiled into .mo files and then, used by Gettext when rendering the actual interface. Given that, let's tie together what we have discussed so far in a step-by-step example:

1. A sample template file, including some different gettext calls

<?php include 'i18n_setup.php' ?>
<div id="header">
    <h1><?=sprintf(gettext('Welcome, %s!'), $name)?></h1>
    <!-- code indented this way only for legibility -->
    <?php if ($unread): ?>
        <h2><?=sprintf(
            ngettext('Only one unread message',
                     '%d unread messages',
                     $unread),
            $unread)?>
        </h2>
    <?php endif ?>
</div>
 
<h1><?=gettext('Introduction')?></h1>
<p><?=gettext('We\'re now translating some strings')?></p>
  • [`gettext()`][func] simply translates a msgid into its corresponding msgstr for a given language. There's also the shorthand function _() that works the same way;
  • [`ngettext()`][n_func] does the same but with plural rules;
  • there's also [`dgettext()`][d_func] and [`dngettext()`][dn_func], that allows you to override the domain for a single call. More on domain configuration in the next example.

2. A sample setup file (`i18n_setup.php` as used above), selecting the correct locale and configuring Gettext

<?php
/**
 * Verifies if the given $locale is supported in the project
 * @param string $locale
 * @return bool
 */
function valid($locale) {
   return in_array($locale, ['en_US', 'en', 'pt_BR', 'pt', 'es_ES', 'es');
}
 
//setting the source/default locale, for informational purposes
$lang = 'en_US';
 
if (isset($_GET['lang']) && valid($_GET['lang'])) {
    // the locale can be changed through the query-string
    $lang = $_GET['lang'];    //you should sanitize this!
    setcookie('lang', $lang); //it's stored in a cookie so it can be reused
} elseif (isset($_COOKIE['lang']) && valid($_COOKIE['lang'])) {
    // if the cookie is present instead, let's just keep it
    $lang = $_COOKIE['lang']; //you should sanitize this!
} elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
    // default: look for the languages the browser says the user accepts
    $langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
    array_walk($langs, function (&$lang) { $lang = strtr(strtok($lang, ';'), ['-' => '_']); });
    foreach ($langs as $browser_lang) {
        if (valid($browser_lang)) {
            $lang = $browser_lang;
            break;
        }
    }
}
 
// here we define the global system locale given the found language
putenv("LANG=$lang");
 
// this might be useful for date functions (LC_TIME) or money formatting (LC_MONETARY), for instance
setlocale(LC_ALL, $lang);
 
// this will make Gettext look for ../locales/<lang>/LC_MESSAGES/main.mo
bindtextdomain('main', '../locales');
 
// indicates in what encoding the file should be read
bind_textdomain_codeset('main', 'UTF-8');
 
// if your application has additional domains, as cited before, you should bind them here as well
bindtextdomain('forum', '../locales');
bind_textdomain_codeset('forum', 'UTF-8');
 
// here we indicate the default domain the gettext() calls will respond to
textdomain('main');
 
// this would look for the string in forum.mo instead of main.mo
// echo dgettext('forum', 'Welcome back!');
?>

3. Preparing translation for the first run

To make matters easier - and one of the powerful advantages Gettext has over custom framework i18n packages - is its custom file type. “Oh man, that's quite hard to understand and edit by hand, a simple array would be easier!” Make no mistake, applications like [Poedit] are here to help - a lot. You can get the program from [their website][poedit_download], it's free and available for all platforms. It's a pretty easy tool to get used to, and a very powerful one at the same time - using all powerful features Gettext has available.

In the first run, you should select “File > New Catalog” from the menu. There you'll have a small screen where we will set the terrain so everything else runs smoothly. You'll be able to find those settings later through “Catalog > Properties”:

  • Project name and version, Translation Team and email address: useful information that goes in the .po file header;
  • Language: here you should use that format we mentioned before, such as en_US or pt_BR;
  • Charsets: UTF-8, preferably;
  • Source charset: set here the charset used by your PHP files - probably UTF-8 as well, right?
  • plural forms: here go those rules we mentioned before - there's a link in there with samples as well;
  • Source paths: here you must include all folders from the project where gettext() (and siblings) will happen - this is usually your templates folder(s)
  • Source keywords: this last part is filled by default, but you might need to alter it later - and is one of the powerful points of Gettext. The underlying software knows how the gettext() calls look like in several programming languages, but you might as well create your own translation forms. This will be discussed later in the “Tips” section.

After setting those points you'll be prompted to save the file - using that directory structure we mentioned as well, and then it will run a scan through your source files to find the localization calls. They'll be fed empty into the translation table, and you'll start typing in the localized versions of those strings. Save it and a .mo file will be (re)compiled into the same folder and ta-dah: your project is internationalized.

4. Translating strings

As you may have noticed before, there are two main types of localized strings: simple ones and the ones with plural forms. The first ones have simply two boxes: source and localized string. The source string can't be modified as Gettext/Poedit do not include the powers to alter your source files - you should change the source itself and rescan the files. Tip: you may right-click a translation line and it will hint you with the source files and lines where that string is being used.
On the other hand, plural form strings include two boxes to show the two source strings, and tabs so you can configure the different final forms.

Whenever you change your sources and need to update the translations, just hit Refresh and Poedit will rescan the code, removing non-existent entries, merging the ones that changed and adding new ones. It may also try to guess some translations, based on other ones you did. Those guesses and the changed entries will receive a “Fuzzy” marker, indicating it needs review, being highlighted in the list. It's also useful if you have a translation team and someone tries to write something they're not sure about: just mark Fuzzy and someone else will review later.

Finally, it's advised to leave “View > Untranslated entries first” marked, as it will help you a lot to not forget any entry. From that menu, you can also open parts of the UI that allow you to leave contextual information for translators if needed.

Tips & Tricks

Possible caching issues

If you're running PHP as a module on Apache (mod_php), you might face issues with the .mo file being cached. It happens the first time it's read, and then, to update it, you might need to restart the server. On Nginx and PHP5 it usually takes only a couple of page refreshes to refresh the translation cache, and on PHP7 it is rarely needed.

Additional helper functions

As preferred by many people, it's easier to use _() instead of gettext(). Many custom i18n libraries from frameworks use something similar to t() as well, to make translated code shorter. However, that's the only function that sports a shortcut. You might want to add in your project some others, such as __() or _n() for ngettext(), or maybe a fancy _r() that would join gettext() and sprintf() calls. Other libraries, such as [oscarotero's Gettext][oscarotero] also provide helper functions like these.

In those cases, you'll need to instruct the Gettext utility on how to extract the strings from those new functions. Don't be afraid, it's very easy. It's just a field in the .po file, or a Settings screen on Poedit. In the editor, that option is inside “Catalog > Properties > Source keywords”. You need to include there the specifications of those new functions, following [a specific format][func_format]:

  • if you create something like t() that simply returns the translation for a string, you can specify it as t. Gettext will know the only function argument is the string to be translated;
  • if the function has more than one argument, you can specify in which one the first string is - and if needed, the plural form as well. For instance, if we call our function like this: __('one user', '%d users', $number), the specification would be __:1,2, meaning the first form is the first argument, and the second form is the second argument. If your number comes as the first argument instead, the spec would be __:2,3, indicating the first form is the second argument, and so on.

After including those new rules in the .po file, a new scan will bring in your new strings just as easy as before.

References

打印/导出