本地化是指让你的应用能在不同区域设置(或国家/地区)中良好运行的过程。
你可能知道,并不是所有人都用同样的字符来表示小数点、千位分隔符……或者货币符号。
这些区域设置都有名字,你可以选择使用其中一个。例如,美国的区域设置可能会把数字写成这样:
100,000.00
而在巴西,同样的数字可能会把逗号和小数点对调过来写:
100.000,00
这样写代码,移植到其他国家或地区时就轻松多了!
呃,差不多吧。事实证明 C 只有一个内建的区域设置,而且功能有限。规范在这里确实留下了很多含糊之处,想做到完全可移植很难。
不过我们会尽力而为!
调用这些函数时,要包含 <locale.h>。
就声明一个特定的区域设置而言,可移植的做法基本上只有一种。如果你打算跟区域设置打任何交道,这大概就是你想做的:
setlocale(LC_ALL, ""); // 用本环境的区域设置来处理一切你会希望调用它,好让程序以你当前的区域设置来初始化。
再深入一点,还有一件事可以做,而且仍然保持可移植:
setlocale(LC_ALL, "C"); // 使用默认的 C 区域设置不过每次程序启动时都会默认调用它,所以没太大必要自己再调一遍。
在第二个字符串里,你可以指定系统所支持的任何区域设置。这完全取决于系统,所以会各不相同。在我的系统上,我可以指定这个:
setlocale(LC_ALL, "en_US.UTF-8"); // 不可移植!这样能行。但它只能移植到那些对同一个区域设置使用完全相同名称的系统上,而你没法保证这一点。
给第二个实参传一个空字符串(""),就等于告诉 C:“嘿,你自己去弄清楚这台系统上当前的区域设置是什么吧,省得我来告诉你。”
既然倒腾绿色纸片被说成是通往幸福的关键155,那我们就来聊聊货币区域设置吧。写可移植代码时,你总得知道表示钱该怎么写,对吧?不管是“$”、“€”、“¥”还是“£”。
怎么写这段代码才不至于把人逼疯?幸运的是,只要调用过 setlocale(LC_ALL, ""),你就可以用 localeconv() 查出这些信息:
struct lconv *x = localeconv();这个函数返回指向一个静态分配的 struct lconv 的指针,里面装着你想要的那些丰富信息。
下面是 struct lconv 的各字段及其含义。
先约定几件事。_p_ 表示“正”,_n_ 表示“负”,而 int_ 表示“国际”。虽然其中很多是 char 或 char* 类型,但大多数(或者说它们指向的字符串)实际上都被当作整数来处理156。
在继续之前,请先知道:CHAR_MAX(来自 <limits.h>)是 char 所能容纳的最大值。下面很多 char 值用它来表示该值在给定的区域设置中不可用。
| 字段 | 说明 |
|---|---|
char *mon_decimal_point |
货币的小数点字符,例如 "."。 |
char *mon_thousands_sep |
货币的千位分隔符字符,例如 ","。 |
char *mon_grouping |
货币的分组说明(见下文)。 |
char *positive_sign |
货币的正号,例如 "+" 或 ""。 |
char *negative_sign |
货币的负号,例如 "-"。 |
char *currency_symbol |
货币符号,例如 "$"。 |
char frac_digits |
打印货币金额时,小数点后要打印多少位,例如 2。 |
char p_cs_precedes |
1 表示非负金额中 currency_symbol 位于数值之前,0 表示位于之后。 |
char n_cs_precedes |
1 表示负金额中 currency_symbol 位于数值之前,0 表示位于之后。 |
char p_sep_by_space |
决定非负金额中 currency symbol 与数值之间的分隔方式(见下文)。 |
char n_sep_by_space |
决定负金额中 currency symbol 与数值之间的分隔方式(见下文)。 |
char p_sign_posn |
决定非负数值中 positive_sign 的位置。 |
char n_sign_posn |
决定负数值中 positive_sign 的位置。 |
char *int_curr_symbol |
国际货币符号,例如 "USD "。 |
char int_frac_digits |
frac_digits 的国际值。 |
char int_p_cs_precedes |
p_cs_precedes 的国际值。 |
char int_n_cs_precedes |
n_cs_precedes 的国际值。 |
char int_p_sep_by_space |
p_sep_by_space 的国际值。 |
char int_n_sep_by_space |
n_sep_by_space 的国际值。 |
char int_p_sign_posn |
p_sign_posn 的国际值。 |
char int_n_sign_posn |
n_sign_posn 的国际值。 |
好,这个有点绕。mon_grouping 是 char*,所以你可能会以为它是个字符串。但这回不是,它其实是一个 char 数组。它的结尾应当总是 0 或 CHAR_MAX。
这些值描述的是货币数字在小数点左侧(整数部分)如何分组。
例如,我们可能有:
2 1 0
--- --- ---
$100,000,000.00这些都是三位一组。组 0(紧挨小数点左边)有 3 位数字。组 1(再往左一组)有 3 位,最后一组也是 3 位。
所以我们可以从右(小数点)往左,用一组表示各组大小的整数值来描述这些分组:
3 3 3这样就能处理最大到 $100,000,000 的数值。
那如果要处理的数更大呢?我们可以一直往上加 3……
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3但那就太疯狂了。好在我们可以指定 0,表示沿用前一组的大小并重复:
3 0意思就是每 3 位重复一次。这样就能应付 $100、$1,000、$10,000、$10,000,000、$100,000,000,000 等等。
你完全可以用这些值玩出各种花样,表示某些稀奇古怪的分组。
例如:
4 3 2 1 0表示的是:
$1,0,0,0,0,00,000,0000.00还可能出现的另一个值是 CHAR_MAX。它表示不再进行分组,并且可以出现在数组中的任何位置,包括第一个值。
3 2 CHAR_MAX表示的是:
100000000,00,000.00例如。
而只要数组第一个位置是 CHAR_MAX,就说明根本不做分组。
所有 sep_by_space 变体都处理货币符号周围的空格。合法取值有:
| 值 | 说明 |
|---|---|
0 |
货币符号与数值之间不加空格。 |
1 |
用空格把货币符号(以及正负号,如果有)与数值隔开。 |
2 |
若正负号符号与货币符号相邻,则用空格把两者隔开;否则用空格把正负号符号与数值隔开。 |
sign_posn 的各变体由下面这些值决定:
| 值 | 说明 |
|---|---|
0 |
在数值和货币符号外面加圆括号。 |
1 |
把正负号字符串放在货币符号和数值之前。 |
2 |
把正负号字符串放在货币符号和数值之后。 |
3 |
把正负号字符串直接放在货币符号之前。 |
4 |
把正负号字符串直接放在货币符号之后。 |
在我的系统上取这些值时,我看到的是这样(分组字符串按单个字节值显示):
mon_decimal_point = "."
mon_thousands_sep = ","
mon_grouping = 3 3 0
positive_sign = ""
negative_sign = "-"
currency_symbol = "$"
frac_digits = 2
p_cs_precedes = 1
n_cs_precedes = 1
p_sep_by_space = 0
n_sep_by_space = 0
p_sign_posn = 1
n_sign_posn = 1
int_curr_symbol = "USD "
int_frac_digits = 2
int_p_cs_precedes = 1
int_n_cs_precedes = 1
int_p_sep_by_space = 1
int_n_sep_by_space = 1
int_p_sign_posn = 1
int_n_sign_posn = 1注意我们前面传给 setlocale() 的是宏 LC_ALL……这暗示可能还有别的变体,让你能更精确地指定要设置区域设置的哪些部分。
我们来看看这些可用的值:
| 宏 | 说明 |
|---|---|
LC_ALL |
把下面所有项都设为给定的区域设置。 |
LC_COLLATE |
控制 strcoll() 和 strxfrm() 函数的行为。 |
LC_CTYPE |
控制字符处理函数的行为157。 |
LC_MONETARY |
控制 localeconv() 返回的值。 |
LC_NUMERIC |
控制 printf() 系列函数的小数点。 |
LC_TIME |
控制 strftime() 和 wcsftime() 这些时间和日期打印函数的时间格式。 |
设置 LC_ALL 是很常见的做法,不过嘿,至少你还有得选。
另外我还得指出,LC_CTYPE 是其中的大头之一,因为它牵扯到宽字符——这可是个大麻烦,我们后面再聊。