Bash 脚本国际化

作者:Louis Iacona

我最早获得报酬开发的软件是一个 2 页的 shell 脚本,它提示用户输入十几个信息,然后启动一组协同工作的进程。这些进程构成了公共电话网络性能评估套件的核心——对于当时来说,这是一个相当庞大的系统,具有很高的知名度。

回顾这项任务和更广泛的应用,我可以完全肯定地说,它的任何利益相关者都没有考虑过[人类]语言的独立性——也就是说,如何在英语以外的语言中呈现提示、错误消息、进度诊断等。即使我们曾逐步考虑过这一点,开发语言/平台提供的便利程度也非常有限或根本不存在。

快进到 2010 年,语言独立性——或者现在众所周知的国际化——是商业级软件的期望之一。我在 1982 年自豪地编写的那个 shell 脚本是少数几个直接与用户交互或生成进度诊断信息的应用程序模块之一。这正是那种会促使我们考虑国际化的 shell 脚本。

我提供这个专栏的动机源于最近的一次经历。我们的团队被要求评估一个大型遗留系统的“国际化准备程度”——也就是说,识别出未国际化且需要国际化的模块,并估计应用所有必要更改的工作量。我们发现差距主要存在于用解释型语言(如 Rexx、TCL 和 bash shell)实现的模块中。我发现,虽然似乎有关于该应用程序中使用的大多数编程语言的国际化通用文档,但在 shell 脚本方面却找不到太多(至少没有提供带有代码示例的“操作指南”)。我发现的更完整的在线资源之一是一个 bash 脚本指南的附录,它以以下句子开头:“本地化是一个未记录的 Bash 功能。”。好吧,至少它提供了一些希望、基本信息和代码片段。本专栏继续提炼我认为完整但概要形式中缺少的内容。

大局观(在一个小框架内)。

首先,让我们就一个通用词汇达成一致——这些术语开始为后续提出的工作和代码示例奠定框架。

  • 消息目录:是一个国际化应用程序使用的自然语言消息的索引存储库。消息目录提供了[人类]语言内容和应用程序代码的解耦。当应用程序需要在运行时访问消息时,底层处理堆栈中的某些内容知道如何根据唯一键检索它。消息目录的格式和维护细节通常是特定于开发平台的,但目标始终相同——解耦和集中应用程序的自然语言文本。
  • 国际化:术语国际化(以下简称其常用缩写:I18N - “I - 十八个字母 - N”)适用于软件设计师/开发人员为使应用程序与语言无关而采取的步骤。在编码级别,用户可读文本永远不会编译到应用程序中或与标记语言混合。相反,应用程序代码通过唯一的消息目录键引用此类内容。
  • 本地化:(有时缩写为“L10N”)适用于使应用程序适应特定目标语言的过程。如果应用了 IN18,本地化不应涉及重新编码,而应侧重于语言翻译和重新部署。换句话说,本地化只是为新语言添加支持的过程——将消息目录内容从一种语言翻译成另一种语言。
  • 区域设置:是用户环境中定义位置、国家和文化信息的部分——最明显的是用户的语言偏好。区域设置通常作为底层操作系统或渲染应用程序(如浏览器)的一部分安装和配置。

让我们总结一下。我们进行国际化,以便我们可以进行本地化。I18N 是一项设计和编码时间工作,要求开发人员遵守某些设计和编码实践,其主要目标是——将语言敏感内容与源代码解耦。对于需要支持的每种语言,都要执行本地化工作——为该语言创建新的消息目录。

这里的好消息是,I18N 过程不必从第一原理开始。包括 Bash 在内的大多数现代开发语言都提供了促进基本操作的功能——开发人员的任务是决定如何将这些基本操作集成到生命周期过程和代码库中。

范围之内和之外

充分利用本文材料的唯一软性先决条件是对 I18N 的一般理解(独立于编程语言,如上所述)和对 shell 脚本的基本熟悉。

从宏观角度来看,I18N/L10N 超越了自然语言独立性。虽然不是本专栏的重点,但区域设置可以包括定义日期/时间格式、货币符号、时区、非工作日等偏好,所有这些都用于驱动处理和呈现的各个方面。此处介绍的过程、编码和测试示例仅关注语言偏好。还应注意的是,此处介绍了一个相当简单的本地化示例——英语到意大利语——这两种语言共享相同的字母表(或多或少)。这排除了涵盖扩展字符集和本地化 I/O 设备(如键盘)的作用等细节的需要。可以通过在线和其他资源研究 I18N 的其他更深入和更广泛的领域以进行进一步研究。以下是一些示例

Unicode 字符编码标准http://www.unicode.org
关于 I18n 的体面的 I18N 简介http://www.debian.org/doc/manuals/intro-i18n/
W3C 相关 I18N 材料http://www.w3.org/International/
高级 Bash 脚本指南http://www.tldp.org/LDP/abs/html//td>
Bash 中 I18N 的移动部件

在上面概述的基础知识的基础上,让我们继续看一个真实的例子。本节演示如何在 bash 环境中支持和应用 I18N 和本地化,使用一个简单的 bash 脚本来深入理解概念和细节。

首先,哪种 shell 脚本元素对自然语言支持敏感?简而言之,答案是人类用户在使用应用程序时视觉上查看的任何内容。因此,这将包括

  • 给用户的文本提示
  • 错误消息
  • 重定向到日志文件或在控制台上显示的进度或错误诊断信息
  • 帮助文本以及其他用法信息和交互式文档。

Bash 如何促进 I18N 和本地化?我们将通过展示一个不能被认为是国际化的 shell 脚本来开始回答这个问题。下面的简短脚本没有太大的商业价值,但这种“质量”将使我们能够专注于手头的任务——识别和应用对语言敏感区域的更改。此脚本生成并显示用户提供的范围内的随机数,并记录其活动。

- orig-rand.sh
#!/bin/bash

function random {
        typeset low=$1 high=$2
        echo $(( ($RANDOM % ($high - $low) ) + $low ))
}

# (1)
echo  "Hello, I can generate a random number between 2 numbers that you provide"
#(2)
echo -n "What is your low number? "
read low
#(3) 
echo -n "What is your high number? "
read high

if [[ $low -ge $high ]]
then
        #(4)
        echo "1st number should be lower than the second - leaving early." >&2
        exit 1
fi

rand=$(random $low $high )
#(5)
echo "from/to generated (by/at):  $low / $high $rand (${LOGNAME} / $(date))" >> /tmp/POC
#(6)
echo "Your Random Number Is: $rand "

exit 0

运行脚本会产生预期的输出。

$: orig-rand.sh
Hello, I can generate a random number between 2 numbers that you provide
What is your low number? 50
What is your high number? 125
Your Random Number Is: 95
	$: 

注释行 (1) 到 (6) 已被标记为需要更改——因为它们包含自然语言。识别出这些内容后,我们可以继续创建消息目录,供修改后的国际化脚本使用。为了介绍格式,这是一个消息目录示例。它包含 2 条消息——问候语和错误消息。该文件的通用格式由键/值行对组成。“msgid”部分命名一个键,“msgstr”部分关联一个自然语言值。每个消息目录仅支持一种语言——在本例中为英语。

文件:en.po

msgid "Main Greeting" 
msgstr "Welcome, what do you want to do today?" 
msgid "Missing File Error" 
msgstr "File Not Found"

可以手动构建、后处理这些消息目录,并将其安装在环境中以支持一个或多个应用程序。(这些消息目录驻留在文件中,这些文件也称为可移植对象文件,按照惯例,以 .po 后缀命名)。

现在让我们构建一个消息目录来维护示例脚本中找到的用户可见内容。请注意,有 6 条不同的消息与原始脚本中嵌入的内容一致。

文件:en.po

msgid "Greeting"
msgstr "Hello, I can generate a random number between 2 numbers that you provide"
msgid "Low Number Prompt"
msgstr "What is your low number"
msgid "High Number Prompt"
msgstr "What is your high number"
msgid "Input Error"
msgstr "1st number should be lower than the second - leaving early."
msgid "Result Title"
msgstr "Your Random Number Is: "
msgid "Activity Log"
msgstr "from/to generated (by/at): "

好的,至少就消息目录而言,我们现在已经涵盖了英语内容。现在让我们为另一种语言——意大利语组装一个。

文件:it.po

msgid "Greeting"
msgstr "Ciao, posso generare un numero casuale fra il numero 2 che assicurate"
msgid "Low Number Prompt"
msgstr "Che cosa  il vostro numero basso"
msgid "High Number Prompt"
msgstr "Che cosa  il vostro alto numero"
msgid "Input Error"
msgstr "il primo numero dovrebbe essere pi basso del secondo - andando presto."
msgid "Result Title"
msgstr "Il vostro numero casuale :"
msgid "Activity Log"
msgstr "da/al generato a (da/a):"

请注意,“msgid”值是常量,并且没有更改。它们将由修改后的脚本(国际化脚本)使用。现在语言目录已存在,需要做些什么才能使国际化脚本可以访问它们?Linux 提供了一个名为“msgfmt”的实用程序,它可以从可移植对象文件 (*.po) 创建“消息对象文件”(*.mo),而无需更改可移植对象文件。有关完整的命令行用法详细信息,请参阅已安装或在线的手册页。执行以下命令将生成并安装英语和意大利语的消息对象文件。

        msgfmt -o rand.sh.mo it.po
        cp -p rand.sh.mo $HOME/locale/it/LC_MESSAGES/ 
        msgfmt -o rand.sh.mo en.po
        cp -p rand.sh.mo $HOME/locale/en/LC_MESSAGES/

现在,两种语言的消息目录都已安装,bash 脚本如何利用它们?对我们的示例至关重要的另一个 Linux 实用程序称为“gettext”。

给定消息目录的目录和文件命名组织,gettext 提供了对存储在目录中的消息的访问。首先,描述消息目录必须如何存储在文件系统中,请参见下面的列表。对于每个 2 个字母的语言代码(在我们的示例中为“en”和“it”),一些“文本域”消息对象文件存储在名为 LC_MESSAGES 的子目录下。按照惯例,文本域与单个应用程序相关,但这在本地化时要做出组织决策。

目录/文件列表

en
en/LC_MESSAGES
en/LC_MESSAGES/rand.sh.mo
it
it/LC_MESSAGES
it/LC_MESSAGES/rand.sh.mo

如上所示,我们选择将消息目录安装在用户主目录下的一个名为 locale 的子目录下。与 Linux 一起分发的系统消息目录通常位于 /usr/lib/locale 下。以下是我的发行版上的一些目录列表

aa_DJ
aa_DJ/LC_MESSAGES
aa_DJ.utf8
aa_DJ.utf8/LC_MESSAGES
aa_ER
aa_ER/LC_MESSAGES
aa_ER@saaho
... many others not shown 

检索存储在消息目录中的消息非常简单——以下两行演示了基本访问。有关完整的命令行用法,请参阅已安装或在线的手册页。需要将环境变量 TEXTDOMAINDIR 设置为消息目录目录的根目录。

	$: export TEXTDOMAINDIR=/home/lji/locale
$: gettext -s "Greeting"
Hello, I can generate a random number between 2 numbers that you provide
$:

请注意,上面的调用迫使“gettext”实用程序显示消息的英语副本。这是由分配给用户区域设置的语言首选项值驱动的。无需详细说明,'locale' Linux 实用程序显示以下值。当然,第一个值驱动语言偏好。

$: locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
 ?? other values not shown.
$: 

因此,如果您一直在关注,那么下一个自然要问的问题是如何更改语言偏好。我们如何测试对意大利语消息目录的访问?再次,无需详细说明,将环境变量 LC_ALL 设置为包含语言和国家/地区代码的值,将重置每个区域设置属性。请注意在将意大利语/意大利 (it/IT) 分配为语言/国家/地区后,'locale' 实用程序的更新输出。

$: export LC_ALL="it_IT.UTF-8"
$: locale
LANG=it_IT.UTF-8
LC_CTYPE="it_IT.UTF-8"
LC_NUMERIC="it_IT.UTF-8"
LC_TIME="it_IT.UTF-8"
?? other values not shown.
$:

现在,如果执行相同的 'gettext' 命令,我们将期望显示等效的意大利语内容,并且我们确实显示了,如下所示。

$: gettext -s "Greeting"
Ciao, posso generare un numero casuale fra il numero 2 che assicurate
$:

因此,如果 'msgfmt' 和 'gettext' 实用程序是 bash shell 中基本 I18N 和本地化的核心,那么国际化原始示例脚本和其他类似脚本的最佳方法是什么?我采取的第一步是构建一个精简的便利库,它提供了 4 个有用的函数。我选择这种通用方法有两个原因:它将最低级别的细节与应用程序代码隔离开来,并通过为开发人员提供处理这些常见的自然语言敏感操作的直接方法来促进代码重用

  • 向标准输出显示文本
  • 显示错误消息
  • 提示用户做出响应
  • 将消息记录到文件

下面的库代码设置了 TEXTDOMAINDIR 环境变量并实现了 4 个函数。

i18n-lib.sh 的源代码

#!/bin/bash
##
# Thin library around basic I18N facilitated function
#   basic text display, file logging, error display, and prompting
export TEXTDOMAINDIR=/home/lji/locale

###############################################
##
## Display some text to stderr
## $1 is assumed to be the Message Catalog key
function i18n_error {
        echo "$(gettext -s "$1")" >&2
}

###############################################
##
## Display some text to sdtout
## $1 is assumed to be the Message Catalog key
## rest of args are used as misc information
function i18n_display {
        typeset key="$1"
        shift
        echo "$(gettext -s "$key") $@"
}

###############################################
## Append a log message to a file.
## use $1 as target file to append to
## use $2 as catalog key
## rest of args are used as misc information
function i18n_fileout {
        [[ $# -lt 2 ]] && return 1
        typeset file="$1"
        typeset key="$2"
        shift 2
        echo "$(gettext -s "$key") $@" >> ${file}
}

## Prompt the user with a message and echo back the response.
## $1 is assumed to be the Message Catalog key
function i18n_prompt {
        typeset rv
        [[ $# -lt 1 ]] && return 1
        read -p "$(gettext "$1"): " rv
        echo $rv
}

那么我们如何转换原始示例脚本以利用这个库——也就是说,对其进行国际化?请参见下面重新实现的脚本。有 4 个明显的更改

  1. TEXTDOMAIN 环境变量设置为基本应用程序值
  2. 我们的 I18N 库文件被引入。
  3. 用户有机会选择意大利语作为首选语言。
  4. 所有定向自然语言内容的“echo”语句都被对 I18N 库提供的函数的调用所取代。

文件:i18n-rand.sh

#!/bin/bash
##
# POC around i18n/Localization in a bash script
#(1)
export TEXTDOMAIN=rand.sh
I18NLIB=i18n-lib.sh
#(2)
# source in I18N library - shown above
if [[ -f $I18NLIB ]]
then
        . $I18NLIB
else
        echo "ERROR - $I18NLIB NOT FOUND"
        exit 1
fi

## Start of example script 
function random {
        typeset low=$1 high=$2
        echo $(( ($RANDOM % ($high - $low) ) + $low ))
}
#(3)
## ALLOW USER TO SET LANG PREFERENCE
## assume lang and country code follows
if [[ "$1" = "-lang" ]]
then
        export LC_ALL="$2_$3.UTF-8"
fi

#(4) 
# Display initial greeting
i18n_display "Greeting"
# ask for input 
low=$(i18n_prompt "Low Number Prompt" )
high=$(i18n_prompt "High Number Prompt" )
# check for error condition and display error if found 
if [[ $low -ge $high ]]
then
        i18n_error "Input Error"
        exit 1
fi
rand=$(random $low $high )
# Log what was just done 
i18n_fileout "/tmp/POC" "Activity Log" "$low / $high $rand (${LOGNAME} / $(date))"
# Display Results 
i18n_display "Result Title" $rand
exit 0

现在我们可以证明它一切正常。下面显示了两个测试运行——一个使用英语内容,另一个使用意大利语内容。

$: i18n-rand.sh
Hello, I can generate a random number between 2 numbers that you provide
What is your low number?  100
What is your high number?  1000
Your Random Number Is:  615
## now specify Italian as language preference
$:  i18n-rand.sh  -lang it IT
Ciao, posso generare un numero casuale fra il numero 2 che assicurate
Che cosa  il vostro numero basso?  500
Che cosa  il vostro alto numero?  1000
Il vostro numero casuale : 601
$: 

日志文件的内容符合预期。请注意,此脚本不是唯一受更改区域设置影响的处理。'date' 命令的输出显示了星期日 (dom) 和六月 (giu) 的意大利语缩写。是的,Linux 及其所有实用程序都被认为是国际化的。

from/to generated (by/at):  50 / 125 95 (lji / Sun Jun 10 12:57:38 EDT 2010)
from/to generated (by/at):  100 / 1000 615 (lji / Sun Jun 10 12:57:59 EDT 2010)
da/al generato a (da/a): 500 / 1000 601 (lji / dom giu 10 12:58:48 EDT 2010)
总结/结论

正如 XML 等信息交换标准使系统更具互操作性一样,I18N 的核心是使应用程序对更广泛、更全球的用户群更可用。我并不是说每个微不足道的 shell 脚本都一定需要 I18N,但由于所有商业软件都可能成为全球商品,因此需要考虑语言独立性——并且在设计/开发过程的早期就要考虑。在 2010 年,缺乏此类计划将是相当短视的。与所有核心应用程序服务一样,在项目开始时解决 I18N 问题比在产品生命周期深处强行塞入解决方案要便宜得多(而且优势非常明显)。

每种现代开发语言都以独特的方式支持 I18N/本地化。但是,无论您的应用程序是主要的网站还是 2 页的 shell 脚本,相同的通用概念始终适用。理想情况下,架构师和设计师通过为开发人员提供一种方便的方式来利用现有的 I18N 和本地化工具/API 来定下基调。首席开发人员可以并且应该在从消息目录获取内容的低级细节周围实现一个精简的便利包装器。在这个级别提供功能对于鼓励开发人员在所有应用程序中应用通用解决方案并防止代码膨胀大有帮助。

Linux 及其 bash shell 中对创建和使用消息目录提供了真正的支持,尽管文档可能很少。作为大型应用程序中相对较小的一部分,shell 脚本(它们呈现文本界面或控制进度和错误日志记录)经常在大量浏览器可访问的内容中被遗忘。很容易忘记 shell 脚本。我希望投入时间和精力来整理本文材料的少量投资可以用于包含 shell 脚本的开发工作。

杂项说明
  • 此处使用的代码示例是在 Suse Linux 10 上构建和测试的。
  • google 翻译器 (http://www.google.com/translate_t) 用于将基本英语消息目录翻译成意大利语,因此它们可能不是最合适的上下文翻译。通常,Locaization 的语言翻译由熟悉应用程序及其客户群的人工翻译执行。

图片来源:© asharkyu/Shutterstock

加载 Disqus 评论