gettext 国际化
by Anastasia
由Anastasia
In my previous tutorial, we discussed how to introduce support for I18n into Rails apps. Today we will continue covering back-end frameworks and talk about localization of Phoenix applications with the help of Gettext.
在我之前的教程中,我们讨论了如何将对I18n的支持引入Rails应用程序 。 今天,我们将继续介绍后端框架,并在Gettext的帮助下讨论Phoenix应用程序的本地化 。
You might not have heard about Phoenix before, so let me say a couple of words about it. This is a server-side MVC framework written in Elixir, which is a functional programming language working on Erlang virtual machine. The framework itself is quite young but still it is very promising thanks to Erlang’s and Elixir’s features. It is very fast, scalable, and concurrency-oriented which is really important for heavily loaded applications.
您可能以前没有听说过Phoenix ,所以让我说几句话。 这是用Elixir编写的服务器端MVC框架, Elixir是在Erlang虚拟机上工作的一种功能编程语言。 该框架本身还很年轻,但是由于Erlang和Elixir的功能,它仍然很有希望。 它非常快速,可伸缩且面向并发,这对于重载应用程序确实非常重要。
Gettext, in turn, is an I18n tool maintained by GNU which may be used for web, desktop applications, and even in operating systems.
反过来, Gettext是GNU维护的I18n工具,可用于Web,桌面应用程序甚至操作系统。
Throughout this article we will be localizing a Phoenix demo project and will see Gettext in action. Also, we will discuss how to introduce support for locale switching and persisting user preferences throughout the requests. Before proceeding to the main part of the tutorial, you also might want to learn common recommendations which are listed in our recent article.
在整个本文中,我们将本地化Phoenix演示项目,并将看到Gettext的实际应用。 另外,我们将讨论如何在整个请求中引入对区域设置切换的支持以及持久化用户偏好。 在继续学习本教程的主要部分之前,您可能还想学习我们最近的文章中列出的常见建议。
Alright, let’s dive straight into the code and observe the localization of Phoenix applications in practice. Create a new project without a default DBMS and change directory into the project:
好吧,让我们直接看一下代码,并在实践中观察Phoenix应用程序的本地化。 创建一个没有默认DBMS的新项目,并将目录更改为该项目:
mix phx.new lokalise_demo --no-ecto cd lokalise_demo
It appears that Phoenix has support for Gettext out of the box: you don’t need to install any third-party libraries. Moreover, if you navigate to the demo/lib/demo_web/templates/page/index.html.eex
file, you’ll notice the following line of code:
Phoenix似乎开箱即用地支持Gettext:您不需要安装任何第三方库。 此外,如果导航到demo/lib/demo_web/templates/page/index.html.eex
文件,则会注意到以下代码行:
<h2><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h2>
What is going on here? Well, gettext
is a function that tries to load translation for the string "Welcome to %{name}!"
. %{name}
here is a placeholder that will be replaced with a "Phoenix"
string as dictated by the second argument name: "Phoenix"
(this argument contains so-called bindings).
这里发生了什么? 好吧, gettext
是一个试图为字符串"Welcome to %{name}!"
加载转换的函数 "Welcome to %{name}!"
。 %{name}
是一个占位符,将由第二个参数name: "Phoenix"
(此参数包含所谓的bindings )指示的"Phoenix"
字符串替换 。
By default, Phoenix applications have English as the default locale set, and no other locales are supported. However, you may easily change that by adding a new line to the config/config.exs
file:
默认情况下,Phoenix应用程序将英语作为默认语言环境集,并且不支持其他语言环境。 但是,您可以通过在config/config.exs
文件中添加新行来轻松更改它:
config :lokalise_demo, LokaliseDemoWeb.Gettext, locales: ~w(en ru)
Now we are supporting both English and Russian locales.
现在,我们支持英语和俄语语言环境。
The next step is to provide translations for the string passed to the gettext
function inside the index.html.eex
file. The simplest way to do that is by extracting all translation strings into separate files automatically:
下一步是为传递到index.html.eex
文件中的gettext
函数的字符串提供翻译。 最简单的方法是将所有翻译字符串自动提取到单独的文件中:
mix gettext.extract mix gettext.merge priv/gettext mix gettext.merge priv/gettext --locale ru
These commands are going to create three new files inside the priv/gettext
folder. Therefore, let’s stop for a second and talk a bit more about these files.
这些命令将在priv/gettext
文件夹中创建三个新文件。 因此,让我们停下来稍谈一些有关这些文件的内容。
The first command above, mix gettext.extract
, searches for all Gettext messages that require translation and places them into the priv/gettext/default.pot
file. POT means “portable object template”, and such files serve as templates for language-specific translations. Our default.pot
has the following contents:
上面的第一个命令, mix gettext.extract
,搜索所有需要翻译的Gettext消息,并将它们放入priv/gettext/default.pot
文件。 POT的意思是“便携式对象模板”,这些文件充当特定于语言的翻译的模板。 我们的default.pot
具有以下内容:
## This file is a PO Template file. ## ## msgid here are often extracted from source code. ## Add new translations manually only if they're dynamic ## translations that can't be statically extracted. ## ## Run mix gettext.extract to bring this file up to ## date. Leave msgstr empty as changing them here as no ## effect: edit them in PO (.po) files instead. msgid "" msgstr "" #, elixir-format #: lib/lokalise_demo_web/templates/page/index.html.eex:2 msgid "Welcome to %{name}!" msgstr ""
The template conveniently shows the lines where the extracted messages are located. msgid
is the string to translate (some developers may refer to it as a “key”). msgstr
is, of course, the actual translation.
该模板方便地显示提取的消息所在的行。 msgid
是要翻译的字符串(某些开发人员可能将其称为“键”)。 msgstr
当然是实际的翻译。
The name of the POT file — default
— is also a domain name which serves as a namespace. Initially, there is only one namespace, but for larger sites with hundreds of translations it may be a good idea to create multiple domains and, consequently, separate translations into different files.
POT文件的名称( default
)也是一个用作名称空间的域名 。 最初,只有一个命名空间,但是对于具有数百个翻译的较大站点,创建多个域并因此将翻译分为不同的文件可能是一个好主意。
The mix gettext.merge priv/gettext --locale LOCALE_CODE_HERE
command creates translation files for the given language based on the template. These translation files have .po
extension (“portable object”) and live inside the priv/gettext/LOCALE_CODE_HERE/LC_MESSAGES
folder. Remember that in order to provide translations for the messages, you should edit these PO files, not the templates directly!
mix gettext.merge priv/gettext --locale LOCALE_CODE_HERE
命令可根据模板为给定语言创建翻译文件 。 这些翻译文件具有.po
扩展名(“便携式对象”),位于priv/gettext/LOCALE_CODE_HERE/LC_MESSAGES
文件夹中。 请记住,为了提供消息的翻译,您应该编辑这些PO文件,而不是直接编辑模板!
As already mentioned above, Gettext supports multiple domains or namespaces. When you are utilizing the gettext/4
function, you always assume a default
domain. If you would like to employ a different namespace, use the dgettext/6
function instead which accepts the domain, the message, optional bindings, and some other arguments:
如上所述,Gettext支持多个域或名称空间。 当使用gettext/4
函数时,始终假定一个default
域。 如果要使用其他名称空间,请改用dgettext/6
函数 ,该函数接受域,消息,可选绑定和其他一些参数:
<%= dgettext "custom_domain", "message is ${placeholder}", placeholder: "my binding" %>
Now the mix gettext.extract
command is going to create a new custom_domain.pot
file. Similarly, running mix gettext.merge
creates a custom_domain.po
file based on the template.
现在, mix gettext.extract
命令将创建一个新的custom_domain.pot
文件。 同样,运行mix gettext.merge
custom_domain.po
将基于模板创建一个custom_domain.po
文件。
Note once again that for smaller sites, using multiple domains is usually an overkill. Still, their usage for large resources is very much recommended because this way you don’t end up with hundreds of translations in a single file. Another reason is the ability to have the same translation keys under different namespaces.
再次注意,对于较小的站点,使用多个域通常是过大的。 尽管如此,还是强烈建议将其用于大型资源,因为这样一来,您就不会在单个文件中得到数百个翻译。 另一个原因是能够在不同的名称空间下具有相同的转换键。
So, having discussed some Gettext internals, we can now translate the Welcome to %{name}!
string into Russian (this message is already in English, so of course no translation is needed for this language). Modify the priv/gettext/ru/LC_MESSAGES/default.po
file like this:
因此,在讨论了一些Gettext内幕之后,我们现在可以将Welcome to %{name}!
转换Welcome to %{name}!
字符串转换成俄语(此消息已经是英文,因此,该语言当然不需要翻译)。 修改priv/gettext/ru/LC_MESSAGES/default.po
文件,如下所示:
# ... some other stuff goes here ... #, elixir-format #: lib/lokalise_demo_web/templates/page/index.html.eex:2 msgid "Welcome to %{name}!" msgstr "Вас приветствует %{name}"
This is it! Currently we do not have any mechanism to switch the language, so set Russian as a default locale:
就是这个! 当前,我们没有任何切换语言的机制,因此请将俄语设置为默认语言环境 :
# config/config.exs config :lokalise_demo, LokaliseDemoWeb.Gettext, locales: ~w(en ru), default_locale: "ru" # <== modify this line
Now start the server by running:
现在运行以下命令启动服务器:
mix phx.server
Open http://localhost:4000
page in your browser and make sure that the translated message is shown!
在浏览器中打开http://localhost:4000
页面,并确保显示翻译后的消息!
Another important feature that I would like to cover is the pluralization. Different languages have different pluralization rules, and Gettext supports many of them out of the box. Still, it is our job to provide proper translations for all potential cases.
我想介绍的另一个重要特征是多元化 。 不同的语言具有不同的复数规则,Gettext开箱即用地支持其中的许多规则。 尽管如此,为所有潜在案件提供适当的翻译仍然是我们的工作。
As a very simple example, let’s say how many apples the user has. Suppose we don’t know the exact amount, which means that the sentence may read as “1 apple” or “X apples”. To support pluralization, we have to stick with the ngettext/5
function:
举一个非常简单的例子,假设用户拥有多少个苹果。 假设我们不知道确切的数量,这意味着该句子可能显示为“ 1个苹果”或“ X个苹果”。 为了支持多元化,我们必须坚持使用ngettext/5
函数 :
ngettext "You have 1 apple", "You have %{count} apples", 2
This function accepts both singular and plural forms of the sentence, as well as the count
. Under the hood, Gettext takes this count and chooses the proper translation based on the pluralization rules.
此函数接受单数形式和复数形式的句子以及count
。 在引擎盖下,Gettext会进行此计数,并根据多元规则选择适当的翻译。
Next you may update the POT and PO files with the following commands:
接下来,您可以使用以下命令更新POT和PO文件:
mix gettext.extract --merge priv/gettext mix gettext.extract --merge priv/gettext --locale=ru
You’ll find a couple of new lines inside the Gettext files:
您将在Gettext文件中找到几行:
msgid "You have 1 apple" msgid_plural "You have %{count} apples" msgstr[0] "" msgstr[1] ""
msgstr[0]
and msgstr[1]
contain translations for singular and plural forms respectively. For English we don’t need to do anything else, but the Russian language requires some extra steps:
msgstr[0]
和msgstr[1]
包含单数和复数形式的翻译。 对于英语,我们不需要做任何其他事情,但是俄语需要一些额外的步骤:
msgid "You have one message" msgid_plural "You have %{count} messages" msgstr[0] "У вас одно яблоко" msgstr[1] "У вас %{count} яблока" msgstr[2] "У вас %{count} яблок"
The pluralization rules in this case are a bit more complex, therefore we must provide not two, but three possible options. You may find more information on the topic in the official docs.
在这种情况下,复数规则要复杂一些,因此我们必须提供的不是两个,而是三个可能的选项。 您可以在官方文档中找到有关该主题的更多信息。
As I already mentioned earlier, currently there is no way to actually switch between locales when browsing the app. This is an important feature, so let’s add it now!
如前所述,目前在浏览应用程序时无法在语言环境之间进行切换。 这是一项重要功能,所以现在就添加它!
All in all, we have two potential solutions:
总而言之,我们有两个潜在的解决方案:
Utilize a third-party solution, for example the set_locale plug (the easy way)
利用第三方解决方案,例如set_locale插件(简单方法)
If you choose to stick with the third-party plug, things will be very simple indeed. You need to perform only three quick steps:
如果您选择坚持使用第三方插件,那么事情确实会非常简单。 您只需要执行三个快速步骤 :
Add a new plug to the router.ex
file
将新插件添加到router.ex
文件中
Add a new :locale
routing scope
添加新的:locale
路由范围
After that the locale will be inferred from the URL, cookies, or the accept-language
request header. Simple.
之后,将从URL,cookie或accept-language
请求标头中推断出语言环境 。 简单。
However, in this tutorial I propose choosing a more complex way and writing this feature from scratch.
但是,在本教程中,我建议选择一种更复杂的方法并从头开始编写此功能。
The most common way of specifying the desired locale is via the URL. The language’s code may be a part of the domain name, or a part of the path:
指定所需语言环境的最常见方法是通过URL。 语言代码可以是域名的一部分,也可以是路径的一部分:
Let’s stick with the latter option and provide the locale as a GET parameter. To read the locale’s value and do something about it, we need a custom plug. Create a new lib/lokalise_demo_web/plugs/set_locale_plug.ex
file with the following contents:
让我们坚持使用后一个选项,并将语言环境作为GET参数提供。 要读取语言环境的值并对其进行处理,我们需要一个自定义的plugin 。 创建一个新的lib/lokalise_demo_web/plugs/set_locale_plug.ex
文件,其内容如下:
defmodule LokaliseDemoWeb.Plugs.SetLocale do import Plug.Conn # 1 @supported_locales Gettext.known_locales(LokaliseDemoWeb.Gettext) # 2 def init(_options), do: nil # 3 def call(%Plug.Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do # 4 end def call(conn, _options), do: conn # 5 end
Let’s discuss this code snippet:
让我们讨论以下代码片段:
This is the actual fulfillment of the contract: a callback that gets invoked automatically. It may return options passed to the call/2
function, or just nil
这是合同的实际履行:自动调用的回调。 它可能返回传递给call/2
函数的选项,或者只是nil
The call/2
is initialized with all the GET parameters of the request. We are only interested in the locale
part and fetch it using the pattern matching mechanism. Also on this line we have a guard clause that ensures the chosen language is actually supported
用请求的所有GET参数初始化call/2
。 我们只对locale
部分感兴趣,并使用模式匹配机制来获取它。 同样在这一行上,我们有一个保护子句,可确保实际上支持所选语言
The last thing we need to do is flesh out the first clause of the call/2
function. It simply has to set the chosen locale as the current one:
我们需要做的最后一件事是充实call/2
函数的第一个子句。 它只需要将选定的语言环境设置为当前语言环境即可:
def call(%Plug.Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do LokaliseDemoWeb.Gettext |> Gettext.put_locale(locale) conn end
Note that the conn
must be returned by the call/2
function!
请注意, conn
必须由call/2
函数返回!
The plug is ready, and you may place it inside the :browser
pipeline:
插件已准备就绪,您可以将其放在:browser
管道中:
# lib/router.ex # ... pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug LokaliseDemoWeb.Plugs.SetLocale end
Now reload the server and navigate to http://localhost:4000/?locale=en
. The welcoming message should be in English which means that the custom plug is working as expected!
现在,重新加载服务器并导航到http://localhost:4000/?locale=en
。 欢迎消息应为英文,这表示自定义插头正在按预期工作!
Our next task is persisting the chosen locale among requests so that the user does not need to provide it every time. The perfect candidate for such persistence would be cookies: small text files stored on the user’s PC. Phoenix indeed has support for cookies out of the box, so just utilize a put_resp_cookies/4
function inside your plug:
我们的下一个任务是在请求中保留所选的语言环境,以便用户无需每次都提供它。 这种持久性的最佳选择是cookie:存储在用户PC上的小型文本文件。 Phoenix确实开箱即put_resp_cookies/4
支持cookie,因此只需在插件内部使用put_resp_cookies/4
函数即可 :
def call(%Plug.Conn{params: %{"locale" => locale}} = conn, _options) when locale in @supported_locales do LokaliseDemoWeb.Gettext |> Gettext.put_locale(locale) conn |> put_resp_cookie "locale", locale, max_age: 365*24*60*60 end
We modify the connection by storing a cookie named "locale"
. It has a lifetime of 1 year which effectively means eternity in terms of the web.
我们通过存储名为"locale"
的cookie来修改连接。 它的生命周期为1年,这实际上意味着网络的永恒。
The last step here is reading the chosen locale from the cookie. Unfortunately, we cannot use a guard clause for this task anymore, so let’s replace two clauses of the call/2
function with only one:
这里的最后一步是从cookie中读取所选的语言环境。 不幸的是,我们不能再为此任务使用保护子句,因此让我们仅用一个替换call/2
函数的两个子句:
def call(conn, _options) do case fetch_locale_from(conn) do nil -> conn locale -> LokaliseDemoWeb.Gettext |> Gettext.put_locale(locale) conn |> put_resp_cookie "locale", locale, max_age: 365*24*60*60 end end
All in all, the logic remains the same: we fetch the locale, check it, and then either do nothing or store it as the current one.
总而言之,逻辑保持不变:我们获取语言环境,对其进行检查,然后不执行任何操作或将其存储为当前语言环境。
Add two private functions to finalize this feature:
添加两个私有函数以完成此功能:
defp fetch_locale_from(conn) do (conn.params["locale"] || conn.cookies["locale"]) |> check_locale end defp check_locale(locale) when locale in @supported_locales, do: locale defp check_locale(_), do: nil
Here we are reading the locale from the either the GET param or cookie, and then checking if the desired language is supported. Then either return this language’s code, or just nil
. Great job!
在这里,我们从GET参数或cookie中读取语言环境,然后检查是否支持所需的语言。 然后要么返回该语言的代码,要么返回nil
。 很好!
Another pretty common way of setting the locale is by using the Accept-Language
HTTP header. If you would like to implement this mechanism, try utilizing the code from the set_locale plug that already provides all the necessary RegExs and other fancy stuff.
设置语言环境的另一种非常常见的方法是使用Accept-Language
HTTP标头。 如果您想实现此机制,请尝试利用set_locale插件中的代码,该插件已经提供了所有必需的RegEx和其他奇特的东西。
So, the SetLocale
plug is ready, but we still have not provided any controls to choose the website’s language. Therefore, let’s render two links at the top of the page. Define a new helper inside the lib/views/layout_view.ex
file:
因此, SetLocale
插件已准备就绪,但我们仍未提供任何控件来选择网站的语言。 因此,让我们在页面顶部呈现两个链接。 在lib/views/layout_view.ex
文件中定义一个新的帮助器:
defmodule LokaliseDemoWeb.LayoutView do use LokaliseDemoWeb, :view def new_locale(conn, locale, language_title) do "<a href=\"#{page_path(conn, :index, locale: locale)}\">#{language_title}</a>" |> raw end end
Call this helper from the templates/layout/app.html.eex
template:
从templates/layout/app.html.eex
模板调用此帮助templates/layout/app.html.eex
:
<body> <div class="container"> <header class="header"> <%= new_locale @conn, :en, "English" %> <%= new_locale @conn, :ru, "Russian" %> </header> <!-- other stuff --> </div> </body>
Reload the page and try switching between locales. Everything should be working just fine, which means that the task is completed!
重新加载页面,然后尝试在语言环境之间切换。 一切都应该正常工作,这意味着任务已完成!
By now you are probably thinking that supporting multiple languages on a big website is probably a pain. And, honestly, you are right. Of course, the translations can be namespaced with the help of domains. But still you must make sure that all the keys are translated for each and every locale. Luckily, there is a solution to this problem: the Lokalise platform that makes working with the localization files much simpler. Let me guide you through the initial setup which is nothing complex really.
到现在为止,您可能会认为在一个大型网站上支持多种语言可能很痛苦。 而且,老实说,你是对的。 当然,可以借助域对翻译进行命名空间。 但是仍然必须确保为每个语言环境都翻译了所有键。 幸运的是,有一个解决此问题的方法:使用Lokalise平台可以更轻松地处理本地化文件 。 让我指导您完成初始设置,这实际上并不复杂。
To get started, grab your free trial
首先, 请免费试用
Next simply download your PO files back and replace them inside the priv/gettext
folder
接下来,只需将您的PO文件下载回去,并将其替换在priv/gettext
文件夹中
Lokalise has many more features including support for dozens of platforms and formats, and even the possibility to upload screenshots in order to read texts from them. So, stick with Lokalise and make your life easier!
Lokalise具有更多功能,包括对数十种平台和格式的支持,甚至可以上传屏幕截图以从中读取文本。 因此,坚持使用Lokalise,让您的生活更轻松!
In today’s tutorial we have seen how to perform localization of Phoenix applications with the help of Gettext. We have discussed what Gettext is and what goodies it has to offer. We have seen how to extract translations, generate templates, and create PO files based on these templates. You have also learned what domains are, and how to introduce support for pluralization. On top of that, we have successfully created our custom plug to fetch and persist the chosen locale based on the user’s preferences. Not bad for one article!
在今天的教程中,我们已经了解了如何在Gettext的帮助下对Phoenix应用程序进行本地化。 我们已经讨论了Gettext是什么,以及它提供了什么好处。 我们已经看到了如何提取翻译,生成模板以及基于这些模板创建PO文件。 您还了解了什么是域,以及如何引入对多元化的支持。 最重要的是,我们已经成功创建了自定义插件,可以根据用户的偏好来获取并保留所选的语言环境。 一篇文章不错!
To learn more about Phoenix I18n, I encourage you to check out the official guide that provides both general explanations, as well as documentation for individual functions. To learn about the Gettext and its features in more detail, refer to the GNU’s documentation. And, of course, if you have any questions feel free to post them in the comments!
要了解有关Phoenix I18n的更多信息,我建议您阅读官方指南 , 该指南同时提供了一般性说明以及各个功能的文档。 要详细了解Gettext及其功能,请参阅GNU文档 。 而且,当然,如果您有任何问题,请随时在评论中发表!
Originally published at blog.lokalise.co on September 27, 2018.
最初于2018年9月27日发布在blog.lokalise.co 。
gettext 国际化