0%

多语言版本软件设计

alt text

为了可以使 windows 桌面应用可以支持西班牙语,并且在无法使用 QT 等现代具有多语言翻译模块的背景下,尝试设计软件显示多种语言。

背景

为了可以使 windows 桌面应用可以支持西班牙语,并且在无法使用 QT 等现代具有多语言翻译模块的背景下,尝试设计软件显示多种语言。

西班牙语中的字符

西班牙语字母表共有27个字母,包括:

  • 26个基础拉丁字母(与英语相同),
  • 再加上一个特殊字母 “ñ”。

除此之外,西班牙语中还有以下常见的带附加符号的字符:

  • 带重音符号(á, é, í, ó, ú):用于表示重读音节的位置。
  • 分音符(ü):用于特殊情况下标示发音,例如 pingüino(企鹅),表示字母 “u” 应发音。

虽然这些带附加符号的字母并未单独计入字母表,但在西班牙语书写和阅读中仍然非常重要。

特殊标点符号的使用:

句首和句尾分别使用 ¿?¡! 表示疑问句和感叹句:

  • 如:“¿Quieres salir del cliente de administración de red?” —— 是否退出网管客户端?
  • 如:“¡Contraseña incorrecta!” —— 密码错误!

ANSI 编码

  1. “ANSI 编码”不是正式标准名:

    • 虽然名字中有“ANSI”,但“ANSI 编码”不是指 ANSI 官方制定的某个明确的字符编码标准。
    • 在微软文档中,“ANSI 编码”常指 Windows 的默认“本地代码页”(ACP,Active Code Page)。
  2. “ANSI 编码”会随系统语言/区域设置变化:

    • 简体中文环境:ANSI 编码 = Code Page 936 = GBK。
    • 英文(美国)/西欧环境:ANSI 编码 = Code Page 1252 = Windows-1252。
    • 其他国家/地区的 Windows 系统可能采用不同的代码页(如 1251、932 等)。
  3. ANSI ≠ ASCII:

    • 有时初学者会将“ANSI 编码”与“ASCII”混淆,实际 ANSI 编码一般是基于 ASCII 的扩展,但字符集远大于 ASCII。
  4. “ANSI 编码”≠ Unicode:

    • Unicode(如 UTF-8、UTF-16)是一种跨平台、跨语言的统一编码方式。
    • ANSI 编码不具备这种兼容性,容易出现乱码。

ASCII 编码

ASCII(American Standard Code for Information Interchange),中文通常译作“美国信息交换标准代码”,是一种基于拉丁字母的 7 位字符编码方案。它主要包括:

  1. 可显示字符:

    • 英文字母(大小写)
    • 阿拉伯数字(0–9)
    • 标点符号及其他常用符号
  2. 控制字符:

    • 用于控制设备(如打印机、终端等)行为的一些特殊字符(如换行、回车、制表符等)

主要特点

  • 范围: ASCII 编码使用 7 位二进制数表示字符,编码值从 0 到 127。(Extended ASCII 使用 8 位,但不是标准 ASCII 的一部分。)
  • 用途广泛: 几乎所有现代字符编码方案都兼容 ASCII,或将其作为子集。例如:UTF-8ISO-8859-1GBK 等编码在 ASCII 范围内的字符通常与 ASCII 保持一致。
  • 历史地位: ASCII 在 20 世纪 60 年代就已标准化,是最早的一批计算机字符编码标准之一,对后续的字符编码体系产生了深远影响。

容易与 ANSI 的混淆

ANSI”是 美国国家标准学会(American National Standards Institute) 的缩写,也常被用来泛指 Windows 系统默认使用的某些本地编码(如 Windows-1252)。然而它:

  • 并不是 ASCII,
  • 也与 ASCII 在设计理念和字符范围上不同,
  • 两者不应混为一谈。

GB2312、GBK 和 GB18030 编码

GB2312

  • 中国最早的简体中文字符集标准,由国家标准局于 1980 年发布
  • 收录字符:6763 个汉字,覆盖简体中文常用字符。
  • 编码方式: 使用双字节编码表示汉字,ASCII 字符使用单字节编码。
  • 局限性: 仅支持简体中文和部分符号,不支持繁体字、生僻字及其他语言字符。

GBK

  • 是 GB2312 的扩展版本,由中国国家标准化管理委员会于 1995 年发布
  • 收录字符:21,000 个汉字,大幅增加了繁体字和生僻字。
  • 编码方式: 双字节编码,向下兼容 GB2312
  • 特点: 支持部分繁体字,但不包含藏文、蒙古文等少数民族文字。

GB18030

  • 是 GBK 的进一步扩展,最新版本为 GB18030-2022(于 2022 年发布)。
  • 收录字符: 完整支持 Unicode 的全部字符,包括所有汉字(简体、繁体)、生僻字、少数民族文字,甚至覆盖日文、韩文、部分符号等。
  • 编码方式: 变长编码(可使用单字节、双字节或四字节),向下兼容 GBK 和 GB2312
  • 特点: 是中国官方的 强制性编码标准所有在中国境内销售的软件产品必须支持 GB18030 编码

编码对比表:

GB2312 GBK GB18030
制定时间 1980 年 1995 年 2000 年(最新版本为 2022 年)
编码长度 单字节(ASCII)+ 双字节(汉字) 单字节(ASCII)+ 双字节(汉字) 变长编码(单字节、双字节、四字节)
收录字符数 约 6,763 个汉字 约 21,000 个汉字 支持全部 Unicode 字符(超过百万字符)
支持繁体字
兼容性 不支持繁体、生僻字和其他语言字符 支持繁体字,不支持部分民族文字 向下兼容 GBK、GB2312,支持所有 Unicode 字符
法律地位 中国强制性国家标准(GB18030)

Unicode 编码

Unicode 是一个全球统一的“字符大字典”,目标是把全世界所有字符(英文、汉字、阿拉伯文、日文、韩文、特殊符号、甚至表情符号等)都纳入到一个统一的标准体系里。

每个字符在 Unicode 中都有一个唯一编号(码点,code point) 通过这样一种方式,Unicode 确保了字符编码的统一性,避免了过去各种字符集相互冲突的问题。

下面介绍几种 Unicode 的实现方式:

UTF-8

UTF-8 是一种编码方式(Encoding),用于将 Unicode 中的码点转换为计算机可处理的二进制字节序列。

UTF-8 的特点:

  • 是 Unicode 最流行的实现方式之一。
  • 变长编码(1–4 个字节)
    • 英文字符(ASCII 范围)仅使用 1 个字节。
    • 常用汉字、日文等非 ASCII 字符通常使用 3 个字节。
    • 生僻字符、表情符号等可能使用 4 个字节。
  • 与 ASCII 编码完全兼容(ASCII 字符的 UTF-8 编码与 ASCII 完全一致)。

UTF-8 与 UTF-8 with BOM

UTF-8 with BOM(Byte Order Mark)是 UTF-8 的一种带有标记的形式,用来标识文件使用 UTF-8 编码。

  • 什么是 BOM:
    BOM(字节顺序标记)是一个特殊的 Unicode 字符(U+FEFF),在 UTF-8 中对应的字节序列为 EF BB BF。它可以出现在文件开头,用于指示编码格式。

  • 作用:
    添加 BOM 有助于某些编辑器或软件自动识别文件编码为 UTF-8。

  • 注意事项:

    • UTF-8 不涉及字节序,因此 BOM 并非必须。
    • 跨平台开发或服务器环境中,通常推荐使用不带 BOM 的 UTF-8文件。
    • Windows 上的一些编辑器(如记事本)保存 UTF-8 文件时默认带 BOM。例如,配置文件经记事本保存后添加了 BOM,可能导致程序读取失败。

UTF-16 编码

UTF-16 是另一种 Unicode 编码方式,使用 2 个或 4 个字节表示一个字符。

UTF-16 的特点:

  • 变长编码(2 或 4 字节):
    • 对于 BMP(基本多语言平面,码点范围 U+0000 ~ U+FFFF)中的字符,使用一个 UTF-16 单元(2 字节)。
    • 对于超出 BMP 的字符(如 emoji、大量生僻字),使用一对代理项(surrogate pair),总共占用 4 字节。

UTF-32 编码

UTF-32 是一种固定长度的 Unicode 编码方式,每个字符使用固定的 4 个字节。

UTF-32 的特点:

  • **固定长度:**每个字符占用 4 个字节(实现简单,但空间占用大)。
  • 与 ASCII 编码在字符值层面兼容(ASCII 范围字符的 UTF-32 编码与其 Unicode 编码值一致)。

C/C++ 语言中的字符类型

char

  • 最基本的字符类型,通常占用 1 字节
  • 默认是否为 signed 取决于平台(如 x86 是 signed,但非所有架构一致)
  • 主要用于存储 ASCII 字符或 UTF-8 编码的文本(但标准本身不绑定具体编码)。
  • 衍生类型:
    • unsigned char:无符号版本
    • signed char:有符号版本

wchar_t

  • 宽字符类型,大小依赖于平台
    • Windows 上通常为 16 位
    • Unix/Linux 上通常为 32 位
  • 用于表示宽字符,在国际化应用中曾广泛使用,但并不等价于 Unicode。
  • 某些库函数(如 wcoutwprintf)在不同平台上处理宽字符的行为可能不同
  • 字符串字面量 L"..." 对应 wchar_t 类型。

char8_t(C++20 起)

  • char 一样占用 1 字节(8 位),但含义更明确:专用于存储 UTF-8 编码单元
  • 是一种独立类型,不能与 char 互相隐式转换,以避免混淆本地编码与 UTF-8。
  • 不同类型字符串(如 string vs u8string)不可直接拼接,需要显式转换
  • 字符串字面量 u8"..." 对应 char8_t 类型。
  • 类型间转换需显式进行,例如使用 reinterpret_cast

char16_t(C++11 起)

  • 占用 16 位(2 字节),用于存储 UTF-16 编码单元
  • 同样是独立类型,不与 charwchar_t 互相隐式转换。
  • 字符串字面量 u"..." 对应 char16_t 类型。

char32_t(C++11 起)

  • 占用 32 位(4 字节),用于存储 UTF-32 编码单元,可以表示所有 Unicode 码点(范围 U+0000 ~ U+10FFFF)。
  • 是一种独立类型,不与 charwchar_t 做隐式转换。
  • 编码简单,但占用空间大
  • 字符串字面量 U"..." 对应 char32_t 类型。

总结对比表

类型名称 字节/位大小 主要用途/编码类型 字符串字面量前缀 引入版本 / 说明
char 通常 1 字节 存储 ASCII 或 UTF-8(不强制) "" 最基础类型,有符号/无符号依平台而定
wchar_t Windows: 16 位 / Linux: 32 位 早期宽字符类型,支持国际化,但非专属 Unicode L"" 宽字符类型,平台相关
char8_t 1 字节(8 位) 专门存储 UTF-8 编码 u8"" C++20 引入,独立类型
char16_t 2 字节(16 位) 存储 UTF-16 编码单元 u"" C++11 引入,独立类型
char32_t 4 字节(32 位) 存储 UTF-32 编码单元,表示所有 Unicode 码点 U"" C++11 引入,独立类型

C++ STL 中的字符串类型

C++ 提供了 std::basic_string<T> 模板,可以为不同字符类型构建对应的字符串类型:

STL 类型 模板参数 用途说明
std::string std::basic_string<char> 常规字符串,默认用于 ASCII / UTF-8
std::wstring std::basic_string<wchar_t> 宽字符字符串,国际化场景中使用
std::u8string std::basic_string<char8_t> UTF-8 字符串(C++20 起支持
std::u16string std::basic_string<char16_t> UTF-16 字符串
std::u32string std::basic_string<char32_t> UTF-32 字符串

Windows 平台字符串处理

Windows 对宽字符的支持

在 Windows 平台上,系统内部全面采用 Unicode(严格来说是 UTF-16 LE 编码)表示文本内容。为兼容早期系统,Windows API 分为 ANSI 版本和 Unicode 版本,分别以 AW 为后缀:

  • CreateFileA:参数为 char*,根据当前系统的本地代码页(ACP)进行编码解释。
  • CreateFileW:参数为 wchar_t*,使用 UTF-16 编码,原生支持 Unicode。

Windows 宏自动切换机制:

Windows 头文件使用宏来自动映射 Unicode 或 ANSI 版本的 API。例如:

1
2
3
4
5
#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif

这意味着只要定义了 UNICODE(或 _UNICODE),就能透明地调用 W 版本的 API 函数。

常见字符类型

  • TCHAR:可移植字符类型,在启用 UNICODE 宏时表示 wchar_t,否则为 char
  • WCHAR:Windows 上的宽字符类型,等价于 wchar_t,占用 2 字节(16 位),用于表示 UTF-16 编码的字符。
  • LPWSTR / LPCWSTR:分别表示“指向宽字符串的指针”和“指向常量宽字符串的指针”,用于 API 参数传递。

Windows API 中的 Unicode 处理方式

使用 UTF-16 编码字符串

Windows 中的宽字符字符串通过 UTF-16 编码表示。示例如下:

1
wchar_t* ws = L"示例字符串";

字面量 L"..." 会在编译时转换为 UTF-16 字节序列(LE)。

多字节(如 UTF-8)与宽字符互转

若需支持 UTF-8 或其他多字节编码的输入,可以使用 Windows 提供的编码转换函数:

1
2
3
4
5
6
7
8
int MultiByteToWideChar(
UINT CodePage, // 如 CP_UTF8
DWORD dwFlags,
LPCCH lpMultiByteStr,
int cbMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar
);

对应地,使用 WideCharToMultiByte 可将宽字符转为指定编码(如 UTF-8、GBK、系统默认 ACP 等)。

控制台下的编码设置

在 Windows 控制台中在中文系统中默认编码为 GBK 编码,输出 UTF8 文本需要设置控制台代码页为 UTF-8 编码:

  • 执行命令:chcp 65001 切换到 UTF-8 代码页
  • 或者使用 WriteConsoleW 直接输出 wchar_t* 字符串(UTF-16)
  • 注意 std::wcout 输出非 ASCII 字符时,控制台必须能正确支持字体和编码,否则仍可能出现乱码

Linux 平台字符串处理

编码机制与字符处理方式

Linux 系统中,对字符串的处理本质上是对“字节序列”的处理:

  • 系统调用层(如 openreadwrite)不关心字符串的编码,只看是否是合法的字节流。
  • 编码的解释主要发生在用户空间(应用层)或库函数中。

现代 Linux 发行版几乎默认使用 UTF-8 编码:

  • 通常环境变量中会有:LANG=zh_CN.UTF-8LC_CTYPE=en_US.UTF-8
  • Shell、终端、文本编辑器、Glibc 函数(如 wprintf)等都默认使用 UTF-8

因此,在 Linux 上编写 UTF-8 兼容代码通常比 Windows 更简单和自然。

C++ Builder 中的字符串类型

在 C++ Builder 中,UnicodeString 是最常用的字符串类型,也是 VCL 和 FMX 框架中默认的字符串类型

早期版本(如 C++ Builder 2007 及之前)默认使用的是 AnsiString。从 C++ Builder 2009 开始,默认字符串类型转为 UnicodeString,以更好地支持 Unicode 和多语言应用。

在现代 C++ Builder 项目中,与控件之间的字符串交互(如获取或设置控件文本)默认都使用 UnicodeString

AnsiString

  • 编码形式:
    使用 单字节字符,或者依赖指定代码页的多字节字符集(MBCS),常被称为“ANSI 编码”或“本地编码”。

  • 支持自定义代码页(新版本):
    在较新的 C++ Builder 版本(如 10 Seattle、10.1 Berlin、10.2 Tokyo、10.3 Rio、10.4 Sydney、11 Alexandria 等)中,可使用模板 AnsiStringT<codepage> 显式指定字符编码,例如:

    1
    AnsiStringT<936> str_gbk; // 指定 GBK 编码
  • 局限性:

    • 所能表示的字符范围依赖于指定的代码页。
    • 如果某些字符不在目标编码内,将会出现乱码或数据丢失
    • 不推荐在多语言应用中继续使用 AnsiString,应使用 UnicodeString

UnicodeString

  • 编码形式:
    使用 UTF-16 编码,每个字符占用 2 字节。

  • 兼容 Windows 宽字符 API:
    可通过 UnicodeString::c_str() 方法将字符串转换为 const wchar_t*,用于与 Windows 原生的 W 系列 API 互通,例如:

    1
    2
    UnicodeString str = L"标题";
    MessageBoxW(NULL, str.c_str(), L"提示", MB_OK);
  • 默认字符串类型(C++ Builder 2009 起):
    所有与界面控件交互的字符串默认类型均为 UnicodeString,无需手动转换。

  • 注意:

    • 第一个字符从 1 开始而不是 0
1
2
3
4
5
UnicodeString str = L"0123546";
for (int i = 1; i <= str.Length(); i++) {
wchar_t ch = str[i];
wprintf(L"%d: %d\n", i, ch);
}

总结对比表

类型名称 编码方式 字节大小 可否指定代码页 是否支持 Unicode 与 Windows API 的兼容性
AnsiString 单字节/MBCS 1 字节/字符 ✅(AnsiStringT<codepage> ❌ 依赖代码页,无法覆盖全部 Unicode 部分支持(需使用 ANSI API)
UnicodeString UTF-16 2 字节/字符 ✅ 全面支持 Unicode ✅(支持 Windows W 系列 API)

C++ Builder 中的多语言字符串处理

使用 L"" 常量初始化 UnicodeString

1
UnicodeString uniStr = L"Hello, 你好!";

使用宽字符字面量 L"" 可直接初始化 UnicodeString,该类型以 UTF-16 编码存储字符。

AnsiString 转换为 std::string

1
2
AnsiString ansiStr = "Hello, 你好!";
std::string str = ansiStr.c_str();

std::string 转换为 AnsiString

1
2
std::string str = "Hello, 你好!";
AnsiString ansiStr = str.c_str();

AnsiString(UTF-8) 转换为 UnicodeString

使用函数 MultiByteToWideChar 将 UTF-8 编码的 char* 字符串转换为 UnicodeString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UnicodeString UTF8ToUnicode(const char* str)
{
UnicodeString strResult;

int textlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
if (textlen <= 0) return strResult;

size_t bufsize = static_cast<size_t>(textlen + 1);
wchar_t* pBuf = new wchar_t[bufsize];
memset(pBuf, 0, sizeof(wchar_t) * bufsize);

MultiByteToWideChar(CP_UTF8, 0, str, -1, pBuf, textlen);
strResult = pBuf;

delete[] pBuf;
return strResult;
}

UnicodeString 转换为 AnsiString(UTF-8)

1
2
3
4
5
6
7
8
9
AnsiString UnicodeToUTF8(UnicodeString str)
{
int len = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), -1, NULL, 0, NULL, NULL);
char* buffer = new char[len];
WideCharToMultiByte(CP_UTF8, 0, str.c_str(), -1, buffer, len, NULL, NULL);
AnsiString result = buffer;
delete[] buffer;
return result;
}

UTF-8 与 GB2312 编码转换说明

文件编码与显示编码不一致, UTF-8 文件用 GB2312 打开,常见乱码,需在编辑器中指定正确编码。

  • GBK 是对 GB2312 的超集,因此:

    • GB2312 → UTF-8 可无损转换
    • UTF-8 → GB2312,无法反向完整转换,非交集字符会变乱码或问号
  • 示例说明:

    1
    GB2312 不支持字符串 "ñáéíóúü"

字符串常量初始化

UnicodeString 支持使用宽字符常量 L"" 初始化。

1
UnicodeString uniStr = L"Hello, 你好!";
  • 建议源码文件保存为 UTF-8 编码,否则如使用 GBK 编码时,部分非 ASCII 字符(如中文、ñáéíóúü)可能出现乱码。
  • 编译器将 L"" 文字编译为 UTF-16 编码,存储于 UnicodeString 中。

日志打印(编码转换示例)

界面读取内容为 UnicodeString 类型,打印日志时应转换为 UTF-8 编码再传入格式化函数。

1
2
UnicodeString str = L"Hello, 你好!";
GosLog(LOG_INFO, "%s", UnicodeToUTF8(str).c_str());