Skip to content

Commit

Permalink
Site updated: 2024-08-23 23:00:15
Browse files Browse the repository at this point in the history
  • Loading branch information
StdioA committed Aug 23, 2024
1 parent 6ac2738 commit 93ea0b2
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 17 deletions.
35 changes: 22 additions & 13 deletions 2024/08/python-i18n-with-gettext/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://blog.stdioa.com/pics/python-i18n/i18n.png">
<meta property="article:published_time" content="2024-08-23T08:21:08.000Z">
<meta property="article:modified_time" content="2024-08-23T08:33:24.377Z">
<meta property="article:modified_time" content="2024-08-23T15:00:08.275Z">
<meta property="article:author" content="David Dai">
<meta property="article:tag" content="Python">
<meta property="article:tag" content="多语言">
Expand Down Expand Up @@ -262,7 +262,7 @@ <h3 class="widget-title">归档</h3>
<div class="slimContent">
<nav id="toc" class="article-toc">
<h3 class="toc-title">文章目录</h3>
<ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#%E8%83%8C%E6%99%AF"><span class="toc-text"> 背景</span></a></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E6%8E%A5%E5%85%A5%E6%B5%81%E7%A8%8B"><span class="toc-text"> 接入流程</span></a></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E8%BF%90%E8%A1%8C%E6%97%B6%E7%BF%BB%E8%AF%91"><span class="toc-text"> 运行时翻译</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%98%BE%E5%BC%8F%E6%8C%87%E5%AE%9A%E8%AF%AD%E8%A8%80"><span class="toc-text"> 显式指定语言</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3"><span class="toc-text"> 参考文档</span></a></li></ol>
<ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#%E8%83%8C%E6%99%AF"><span class="toc-text"> 背景</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E8%AF%AD%E8%A8%80language%E5%92%8C%E5%9C%B0%E5%8C%BAlocale"><span class="toc-text"> 语言(language)和地区(locale)</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E6%8E%A5%E5%85%A5%E6%B5%81%E7%A8%8B"><span class="toc-text"> 接入流程</span></a></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E8%BF%90%E8%A1%8C%E6%97%B6%E7%BF%BB%E8%AF%91"><span class="toc-text"> 运行时翻译</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%98%BE%E5%BC%8F%E6%8C%87%E5%AE%9A%E8%AF%AD%E8%A8%80"><span class="toc-text"> 显式指定语言</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3"><span class="toc-text"> 参考文档</span></a></li></ol>
</nav>
</div>
</aside>
Expand Down Expand Up @@ -314,7 +314,7 @@ <h1 class="article-title" itemprop="name">
<span class="post-comment"><i class="icon icon-comment"></i> <a href="/2024/08/python-i18n-with-gettext/#comments" class="article-comment-link">评论</a></span>


<span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计: 1.3k(字)</span>
<span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计: 2k(字)</span>



Expand All @@ -327,20 +327,29 @@ <h1 class="article-title" itemprop="name">
<p>在很久很久以前,我曾经在 Django 中使用过多语言支持,但还未尝试过使用底层框架为任意项目提供多语言支持。正巧昨天想将最近开源的 <a target="_blank" rel="noopener" href="https://github.com/StdioA/beancount-bot"><code>beancount-bot</code></a> 推荐给 <a target="_blank" rel="noopener" href="https://github.com/siddhantgoel/awesome-beancount"><code>awesome-beancount</code></a> 项目,而之前的所有文本几乎都是用中文写的。于是,我打算为它提供多语言支持,顺便学习一下 <code>gettext</code>.</p>
<h1 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h1>
<p>在企业中,我们通常将涉及到多语言的工作称为“国际化”工作,但提到相关领域,我们通常绕不开两个意思相近的词:<strong>国际化</strong>(internationalization,缩写为 i18n)和<strong>本地化</strong>(localization,缩写为 l10n)。<br />
按照我的理解,国际化属于更偏向框架层面的工作,旨在为程序提供<strong>支持多语言的能力</strong>而本地化是细节层面的工作,目标是在已有的国际化框架中,通过翻译等手段来提供得体的、符合当地文化语境的内容<br />
按照我的理解,国际化工作更偏向框架层面,旨在为程序提供<strong>支持多语言的能力</strong>而本地化工作更偏向是细节层面,其目标是在已有的国际化框架中,通过翻译等手段来提供得体的、符合当地文化环境的内容<br />
<a target="_blank" rel="noopener" href="https://www.gnu.org/software/gettext/manual/html_node/Concepts.html">GNU gettext 的文档</a> 更详细地介绍了这两个概念的区别。</p>
<p>除了文本翻译以外,货币、日期、数字表示法甚至 RTL 也属于国际化的工作范畴,<a target="_blank" rel="noopener" href="https://www.gnu.org/software/gettext/manual/html_node/Aspects.html">这篇文档</a>中详细介绍了更多国际化的工作内容。</p>
<p>在 Python 和 C 语言的程序中,我们通常会使用 GNU <code>gettext</code> 工具包来完成多语言支持工作。它提供了简洁的流程,可以让开发者以近乎为 0 的成本为程序来提供国际化支持<br />
<p>除了我们熟悉的文本翻译以外,货币、日期、数字表示法甚至 RTL 也属于国际化的工作范畴,<a target="_blank" rel="noopener" href="https://www.gnu.org/software/gettext/manual/html_node/Aspects.html">这篇文档</a>中详细介绍了更多国际化的工作内容。</p>
<p>在 Python 和 C 语言的程序中,我们通常会使用 GNU <code>gettext</code> 工具包来完成多语言支持工作。它提供了简洁且易于使用的框架,可以让开发者以极其微小的成本为程序来提供国际化支持<br />
而 Python 也提供了<a target="_blank" rel="noopener" href="https://docs.python.org/3.12/library/gettext.html">对应的 <code>gettext</code></a>来支持相关工作。</p>
<h2 id="语言language和地区locale"><a class="markdownIt-Anchor" href="#语言language和地区locale"></a> 语言(language)和地区(locale)</h2>
<p>在国际化工作中,“语言”(language)和“地区”(locale)是两个核心概念,它们在定义应用程序或内容如何适应不同市场和用户需求时扮演着关键角色。</p>
<p>语言指的是人们用于交流的符号系统,如英语、汉语、西班牙语等。它主要关注文本的翻译和语言习惯的适应,确保内容在不同语言环境下的可理解性和自然性。<br />
地区则是一个更广泛的概念,它不仅包括语言,还涵盖了与特定地理区域相关的所有文化、法律和格式规范。这包括日期和时间的显示格式、货币符号、数字格式、排序规则等。地区设置确保了应用程序在不同地区的用户界面和功能能够符合当地的文化和习惯。</p>
<p>比如,我们在安装系统时,通常会有一个提示界面让我们去选择“语言和地区”。如果用户选择了“英语(美国)”作为他们的地区设置,那么应用程序应该显示美式英语的文本,使用美元符号($)作为货币单位,并按照美国习惯格式化日期(如 MM/DD/YYYY);而如果用户选择了“英语(英国)”,虽然语言同样是英语,但日期格式(如 DD/MM/YYYY)和货币单位(£)将会有所不同,以适应英国地区的规范。<br />
在 POSIX 系统中,我们通常会使用 <code>语言代码_地区代码</code> 的格式来表示 locale. 比如上面的两个 locale 的代码分别为 <code>en_US</code><code>en_GB</code>.</p>
<p>通过精确区分和应用这两个概念,国际化工作能够确保软件产品和内容在全球范围内的有效性和用户满意度。</p>
<h1 id="接入流程"><a class="markdownIt-Anchor" href="#接入流程"></a> 接入流程</h1>
<p>通常情况下,一个 Python 程序接入多语言的工作流程如下图:<br />
<img src="/pics/python-i18n/i18n.png" alt="多语言接入流程" /></p>
<p>为了隔离不同场景,<code>gettext</code> 创建了与 <code>namespace</code> 类似的“域”的概念。此处假设我们的域为 <code>mydomain</code></p>
<p>为了隔离不同的使用场景,<code>gettext</code> 创建了与 <code>namespace</code> 类似的“域”的概念,并通过文件来将它们隔离开来。此处假设我们使用的域为 <code>mydomain</code><br />
<code>gettext</code> 中,我们会通过 <code>msgid</code> 来对文本做唯一标注,而这个 <code>msgid</code> 的值就来自于作为 <code>_</code> 函数参数的字符串。然而,在不同的语境中,同一个单词会具有不同的含义,如 <code>position</code> 一词可以表示“位置”,也可以表示“头寸”。</p>
<p>具体操作步骤如下:</p>
<ol>
<li>在 Python 代码中先通过 <code>gettext.gettext</code> 函数(通常会使用 <code>_</code> 的别名)来标记所有需要翻译的字符串。注意,需要翻译的字符串必须是”静态“字符串,不能是 <code>f-string</code> 这种内容不确定的字符串;</li>
<li>在 Python 代码中先通过 <code>gettext.gettext</code> 函数(通常会使用 <code>_</code> 做别名)来标记所有需要翻译的字符串。<br />
需要注意的是,需要翻译的字符串必须是“静态”字符串,而不能是 <code>f-string</code> 这种内容不确定的字符串。如果需要动态生成,可以考虑用 <code>format</code><code>%</code> 函数来渲染翻译后的字符串。</li>
<li>标记好后,通过 <code>xgettext -d mydomain -o locale/mydomain.pot **/*.py</code> 扫描源代码中的字符串,并生成 <code>.pot</code> 本地化模板;</li>
<li>选择你希望翻译的语言(严格来讲是 locale,假设为 <code>zh_CN</code>,并根据翻译模板生成 <code>.po</code> 本地化文件:
<li>选择你期望翻译的 locale,假设为 <code>zh_CN</code>,并根据翻译模板生成 <code>.po</code> 本地化文件:
<ol>
<li>如果是初次生成,则运行 <code>msginit -i locale/mydomain.pot -o locale/zh_CN/LC_MESSAGES/mydomain.po -l zh_CN</code></li>
<li>如果要更新现有 <code>.po</code> 的内容,并保留原有翻译,则运行 <code>msgmerge --update locale/zh_CN/LC_MESSAGES/mydomain.po locale/mydomain.pot</code></li>
Expand All @@ -352,16 +361,16 @@ <h1 id="接入流程"><a class="markdownIt-Anchor" href="#接入流程"></a> 接
<p>我将以上流程整理成了一个 <code>Makefile</code>,这样只需要 <code>make all</code> 即可完成增量构建。</p>
<figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">LANGUAGES := en zh_CN zh_TW fr_FR ja_JP ko_KR de_DE es_ES</span><br><span class="line"></span><br><span class="line">DOMAIN := mydomain</span><br><span class="line">POT_FILE := locale/<span class="variable">$(DOMAIN)</span>.pot</span><br><span class="line">PO_FILES := <span class="variable">$(<span class="built_in">foreach</span> lang,<span class="variable">$(LANGUAGES)</span>,locale/<span class="variable">$(lang)</span>/LC_MESSAGES/<span class="variable">$(DOMAIN)</span>.po)</span></span><br><span class="line">MO_FILES := <span class="variable">$(<span class="built_in">foreach</span> lang,<span class="variable">$(LANGUAGES)</span>,locale/<span class="variable">$(lang)</span>/LC_MESSAGES/<span class="variable">$(DOMAIN)</span>.mo)</span></span><br><span class="line"></span><br><span class="line"><span class="meta"><span class="keyword">.PHONY</span>: all gentranslations compiletranslations clean</span></span><br><span class="line"></span><br><span class="line"><span class="section">all: gentranslations compiletranslations</span></span><br><span class="line"></span><br><span class="line"><span class="section">gentranslations: <span class="variable">$(PO_FILES)</span></span></span><br><span class="line"></span><br><span class="line"><span class="section">compiletranslations: <span class="variable">$(MO_FILES)</span></span></span><br><span class="line"></span><br><span class="line"><span class="variable">$(POT_FILE)</span>: **/*.py</span><br><span class="line">spacexgettext -d <span class="variable">$(DOMAIN)</span> -o <span class="variable">$@</span> <span class="variable">$^</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">define</span> po_rule</span><br><span class="line"><span class="section">locale/$(1)/LC_MESSAGES/$(DOMAIN).po: <span class="variable">$(POT_FILE)</span></span></span><br><span class="line">space@mkdir -p $<span class="variable">$(<span class="built_in">dir</span> $<span class="variable">$@</span>)</span></span><br><span class="line">space@if [ ! -f $<span class="variable">$@</span> ]; then \</span><br><span class="line">spacespacemsginit -i $<span class="variable">$&lt;</span> -o $<span class="variable">$@</span> -l $(1); \</span><br><span class="line">space<span class="keyword">else</span> \</span><br><span class="line">spacespacemsgmerge --update $<span class="variable">$@</span> $<span class="variable">$&lt;</span>; \</span><br><span class="line">spacefi</span><br><span class="line"><span class="keyword">endef</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$(<span class="built_in">foreach</span> lang,<span class="variable">$(LANGUAGES)</span>,$(<span class="built_in">eval</span> $(<span class="built_in">call</span> po_rule,<span class="variable">$(lang)</span>)</span>))</span><br><span class="line"></span><br><span class="line"><span class="section">%.mo: %.po</span></span><br><span class="line">spacemsgfmt -o <span class="variable">$@</span> <span class="variable">$^</span></span><br><span class="line"></span><br><span class="line"><span class="section">clean:</span></span><br><span class="line">spacerm -f <span class="variable">$(POT_FILE)</span> <span class="variable">$(PO_FILES)</span> <span class="variable">$(MO_FILES)</span></span><br></pre></td></tr></table></figure>
<h1 id="运行时翻译"><a class="markdownIt-Anchor" href="#运行时翻译"></a> 运行时翻译</h1>
<p>在完成上面的国际化流程后,我们就可以运行我们的程序来查看效果了</p>
<p>在完成上面的国际化流程后,我们就可以运行我们的程序来对代码内的文本进行翻译了</p>
<p>我们可以使用以下代码来初始化多语言环境:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">locale_dir = pathlib.Path(__file__).parent / <span class="string">&#x27;locale&#x27;</span></span><br><span class="line">gettext.bindtextdomain(<span class="string">&#x27;mydomain&#x27;</span>, locale_dir)</span><br><span class="line">gettext.textdomain(<span class="string">&#x27;mydomain&#x27;</span>)</span><br></pre></td></tr></table></figure>
<p>注意,此处本地化文件的目录传递了绝对路径。如果只写 <code>locale</code>,则 <code>gettext</code> 会以工作目录为基准去查找本地化文件,进而很可能导致翻译功能失效</p>
<p>在默认情况下,<code>gettext</code> 包会按顺序读取环境变量(<code>LANGUAGE</code>, <code>LC_ALL</code>, <code>LC_MESSAGES</code>, <code>LANG</code>),并从中找到用户的偏好 locale<br />
<p>注意,此处本地化文件的目录传递了绝对路径。如果只写 <code>locale</code> 作为目录,则 <code>gettext</code> 会以当前的工作目录为基准去查找本地化文件,而这很可能导致翻译功能失效</p>
<p>在默认情况下,<code>gettext</code> 包会按顺序读取环境变量(<code>LANGUAGE</code>, <code>LC_ALL</code>, <code>LC_MESSAGES</code>, <code>LANG</code>,并从中找到用户的偏好 locale;若这些变量均为空,则会降级到 <a target="_blank" rel="noopener" href="https://docs.oracle.com/cd/E19253-01/817-2521/overview-1002/index.html"><code>C</code> locale</a>.<br />
确认 locale 后,我们在调用 <code>_</code> 函数时,它就可以将源字符串转换为翻译后的字符串。<br />
这几个环境变量的关系说来复杂,如果读者有兴趣,可以阅读 GNU gettext 文档中的 <a target="_blank" rel="noopener" href="https://www.gnu.org/software/gettext/manual/html_node/Setting-the-POSIX-Locale.html">《设置 POSIX locale》</a> 部分。</p>
<h2 id="显式指定语言"><a class="markdownIt-Anchor" href="#显式指定语言"></a> 显式指定语言</h2>
<p>虽然我们的默认 shell 环境中都包含了 locale 相关的环境变量,但在某些环境(如容器)里,这些环境变量是不会设置的。<br />
除了通过 <code>-e</code> 参数注入环境变量外,或许我们还可以考虑通过配置文件等方式为程序显式指定所用语言</p>
除了通过 <code>-e</code> 参数注入环境变量外,或许我们还可以考虑通过配置文件等方式为程序显式指定所用的 locale</p>
<p>此处有两种方法:</p>
<ol>
<li>环境变量覆盖:通过 <code>os.environ['LANGUAGE'] = &quot;ll_CC&quot;</code> 的方式,来为 gettext 指定语言;</li>
Expand Down
4 changes: 2 additions & 2 deletions atom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link href="https://blog.stdioa.com/atom.xml" rel="self"/>

<link href="https://blog.stdioa.com/"/>
<updated>2024-08-23T08:33:24.377Z</updated>
<updated>2024-08-23T15:00:08.275Z</updated>
<id>https://blog.stdioa.com/</id>

<author>
Expand All @@ -21,7 +21,7 @@
<link href="https://blog.stdioa.com/2024/08/python-i18n-with-gettext/"/>
<id>https://blog.stdioa.com/2024/08/python-i18n-with-gettext/</id>
<published>2024-08-23T08:21:08.000Z</published>
<updated>2024-08-23T08:33:24.377Z</updated>
<updated>2024-08-23T15:00:08.275Z</updated>


<summary type="html">&lt;p&gt;突发奇想,给自己的 &lt;code&gt;beancount-bot&lt;/code&gt; 接入了多语言支持。本文简单记录了接入和使用的流程。&lt;/p&gt;</summary>
Expand Down
2 changes: 1 addition & 1 deletion content.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ <h1 itemprop="name">
<span class="post-comment"><i class="icon icon-comment"></i> <a href="/2024/08/python-i18n-with-gettext/#comments" class="article-comment-link">评论</a></span>


<span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计: 1.3k(字)</span>
<span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计: 2k(字)</span>



Expand Down

0 comments on commit 93ea0b2

Please sign in to comment.