65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual Component Framework 中的区域支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (10投票s)

2006年4月12日

14分钟阅读

viewsIcon

51179

downloadIcon

498

一篇关于使用VCF处理和添加应用程序的多语言和区域设置支持的文章。

引言

您可能会发现,随着应用程序的进展,您希望添加对除开发应用程序的原生语言之外的其他语言的支持。这可能会有点棘手,具体取决于您想实现什么,以及您想在应用程序中支持多大程度的本地化。

幸运的是,Visual Component Framework 使用 VCF 的 Locale 类,可以轻松地为您的应用程序添加区域设置支持。Locale 类用于表示特定的国家、语言和地区。它有一系列函数,用于处理字符串、转换以及各种其他以区域设置为中心的服务。

Locale 类设计为利用底层操作系统的区域设置特性和服务,而不是重新发明轮子。这反过来会使您的应用程序更符合用户在平台上的期望。Windows 和 OSX 都在其操作系统 API 中提供了强大的区域设置和国际化功能,所以这不成问题。对于缺乏这种支持的操作系统,我们将使用 IBM 的 ICU 库。换句话说,对于 Win32 代码,我们直接调用国家语言支持 (NLS) API,例如 GetNumberFormat()GetDateFormat() 等。

之所以开发 Locale 类而不是依赖 C++ 的 std::locale,是因为 C++ 的区域设置支持效果不佳,并且没有完全实现,因为 STL 规范的某些部分将其留给供应商实现。例如,std::locale 消息类别,旨在将源消息翻译成不同语言的字符串,在 Win32、OS X 以及我上次检查时 Linux 上都无用(即不工作)。这使得它使用起来不可预测,并且难以在不同平台之间,甚至在同一平台的不同供应商之间,依赖任何功能。此外,一个令人信服的论点是,C++ 的区域设置类并不完全直观或易于使用。

特点

VCF 的 Locale 类支持字符串排序,包括区分大小写和不区分大小写,各种类型的字符串转换(例如将整数转换为字符串,以及将字符串转换回数字),以及日期/时间转换为字符串。Locale 类还利用当前操作系统用户设置的区域设置。除了各种比较和转换/解析例程外,Locale 类还支持将源字符串翻译成特定区域设置的字符串。

翻译功能对特定类型的基于文本的翻译文件提供了默认支持,但它也支持自定义翻译例程,因此您可以编写代码来重用现有的翻译文件格式,例如 .PO 格式,这是一种存储本地化翻译的标准。

Locale 类还集成到 VCF 的 UI 类中,支持 UI 字符串的自动翻译,例如菜单项的标题或命令按钮的文本。在您自己的自定义控件中添加此支持也相对容易。

字符串处理

Locale 类使用 VCF 的 String 类来处理文本。VCF 的 String 类是 std::basic_string<> 的一个非常薄的包装器,并将字符串数据存储为无符号短整型,采用 UTF-16 格式,这与 Win32 和 Mac OS X 存储其 Unicode 字符串的格式相同。在原生 UTF-16 和 ANSI 字符串格式之间转换时,String 类在 Win32 上默认使用原生的 MultiByteToWideCharWideCharToMultiByte 函数。也可以使用您自己的文本编解码器,但这需要显式调用。由于 VCF 的内部使用所有字符串处理,任何 VCF 程序都将调用原生的宽字符串 API 函数。如果 VCF 检测到它运行在非 NT 系统上(即 Win9X),它会将字符串转换为 ANSI 并调用相应的 ANSI 函数。这意味着您不必担心 UNICODE 或 MBCS 是否已定义,并且同一个可执行文件可以在任一系统上正确运行。这意味着在 NT 上运行时,您实际上是在没有 ANSI -> Unicode 转换开销的情况下运行,就像您进行了程序的 UNICODE 构建一样。

用法

Locale 类旨在易于使用。您只需使用标准的国家和语言代码创建一个新实例,或者获取当前线程的区域设置。当前线程的区域设置将具有用户为其选择的语言所做的设置。

Locale locEnUS( "en", "US" );
//use the locale...

语言代码是标准的 ISO-639 代码之一。国家代码是标准的 ISO-3166 代码之一。

或者,您可以从各种语言/国家枚举中创建一个区域设置。

Locale locEnUS( Locale::lcEnglish, Locale::ccUnitedStates );
//use the locale...

获取当前线程的区域设置

Locale* locale = System::getCurrentThreadLocale();

排序

一旦您拥有一个有效的区域设置实例,我们就可以开始对其执行操作了。让我们看看字符串排序。排序字符串只是意味着以特定顺序对它们进行排序。通常,如果您有一系列字符串,“hello”,“goodbye”,“123”,以及“ascrobotic”,并想按字母顺序排序,那么您可以这样做:

  • 使用字符串内置的“<”和“>”运算符。
  • 使用区域设置的排序函数。

前者可能对英语文本还可以,但对于其他语言,它将无法正确排序,因此我们需要诉诸第二种方法。Locale 类允许您进行区分大小写或不区分大小写的排序。要排序两个字符串并考虑大小写,我们将使用:

String s1 = "Hello";
String s2 = "hello";
Locale* locale = System::getCurrentThreadLocale();
int res = locale->collate( s1, s2 );

Locale::collate 将返回 -1 如果 s1 在排序顺序上“小于” s2,返回 0 如果它们被认为是等效的,并返回 1 如果 s1 被认为是“大于” s2

您可能会像这样对字符串列表进行排序(使用一些 STL 来帮助我们):

class MySort {
public:
 MySort(Locale* l) :locale(l){}

 bool operator() ( const String& x, const String& y ) {
       return locale->collate( x,y) >= 0;
 }

 Locale* locale;
};

std::vector<String> strings(4);
strings[0] = "Hello";
strings[1] = "Asdf";
strings[2] = "1233";
strings[3] = "VCF";

std::sort( strings.begin(), strings.end(), 
           MySort( System::getCurrentThreadLocale() ) );

使用不区分大小写的排序也可以做到这一点,只需调用 Locale::collateCaseInsensitive() 函数即可。

对于那些想知道这如何工作的人来说,在 Win32 平台上,区域设置对等实现最终会调用 CompareString API 函数。

符号

您可以访问各种符号,例如货币符号、数字分隔符等。每个符号都由一个字符串表示,并且可能包含 Unicode 字符。请注意,当尝试在控制台上显示这些字符时,您可能会看到一个奇怪的符号,而不是您期望的。让我们看看如何访问这些符号:

Locale locItalian( Locale::lcItalian, Locale::ccItaly );
System::println( "100's separator: " +  locItalian.getNumberThousandsSeparator() );
System::println( "Decimal point: " +  locItalian.getNumberDecimalPoint() );
System::println( "Decimal point: " +  locItalian.getCurrencySymbol() );

转换

我们可以通过调用 toString() 函数,使用 Locale 类将各种数据类型转换为字符串。支持的基本类型有:

  • int
  • 无符号整数
  • long
  • unsigned long
  • double
  • float
  • double 作为货币进行评估

所有这些转换函数都将考虑区域设置符号以及可能适用的各种数字分组。这包括任何特定的用户区域设置设置。例如:

Locale* locale = System::getCurrentThreadLocale();
String s = locale->toString( 123456789 );
System::println( s );

这应该输出(假设默认设置)字符串“123,456,789”。要将货币值转换为字符串,您可以使用 Locale::toStringFromCurrency() 函数。这将根据区域设置的货币规则格式化字符串。例如:

Locale* locale = System::getCurrentThreadLocale();
String s = locale->toStringFromCurrency( 567432645.9883 );
System::println( s );

假设区域设置为“en-US”且用户设置默认,这将在 Windows 上输出“$567,432,645.99”。

我们可以使用区域设置的格式规则将字符串转换为全大写或全小写。为此,我们只需调用 Locale::toLowerCase()Locale::toUpperCase()。两个函数都将返回一个已根据需要转换的新字符串。

也可以解析字符串并将其转换为原始类型。Locale 支持转换为 int、unsigned int、float 或 double 类型。解析是根据操作系统强制执行的区域设置规则进行的。例如:

Locale* locale = System::getCurrentThreadLocale();
int num = locale->toInt( "1,000,023" );

如果字符串值是货币,也可以解析它:

Locale* locale = System::getCurrentThreadLocale();
double moneyVal = locale->toInt( "$ 1,000,023.89" );

区域设置标识

区域设置可以通过四种不同的值来标识。区域设置名称是语言和国家代码的组合,例如“en_US”或“it_IT”。您可以通过调用 Locale::getName() 来检索此值。函数 Locale::getLanguageCodeString()Locale::getCountryCodeString() 分别返回区域设置的语言和国家代码。要检索区域设置的人类可读语言名称,您可以调用 Locale::getLanguageName(),它将返回诸如“English”或“Italian”之类的名称。目前,此名称是通过使用 LOCALE_SLANGUAGE 参数调用 GetLocaleInfo 来获取的。这应该返回语言名称的本地化字符串。

消息翻译

Locale 类的最后一个关键功能是将字符串 ID 翻译成其本地化版本。您可能有字符串“Hello”,并希望将其(或本地化)翻译成适合该区域设置的字符串,例如:

Locale loc1("fr", "FR");
String s1 = loc1.translate( "Hello" );

Locale loc2("it", "IT");
String s2 = loc2.translate( "Hello" );

其中 s1 将是“Salute”,s2 将是“Ciao”。翻译“魔法”发生在因为存在一个特殊文件(或文件),每个支持的区域设置都有一个,它将 ID 或“键”(在本例中为“Hello”)映射到特定区域设置的相应等效值。此文件由一个称为 MessageLoader 的特殊类加载,该类了解特定的文件格式,并可以解析和提取特定键的值。

可以为不同的扩展名注册多个 MessageLoader 实例,允许您添加对其他翻译文件类型的自定义支持。例如,如果您有 PO 格式的翻译文件,您可以为该格式编写自定义 MessageLoader 并重用您现有的翻译文件。目前,默认翻译格式是“.strings”格式,这与 Mac OS X 使用的格式相同,并且非常易于阅读、编写和解析。

“.strings”格式非常简单。它是一种纯文本格式,由一个键和一个值组成。每个键名都是唯一的,并用双引号(“"”)括起来。只要用引号括起来,值可以是您想要的任何字符串。Unicode 字符可以用反斜杠(“\”)和一个“U”字符后跟一个四位十六进制数字(例如“\U010F”)来表示。注释使用 C 风格的“/*”开始和“*/”结束。注释可以嵌套。

示例文件

/*
Spanish localization test file
/**
nesting comments
*/

*/

"Hello" = "Hola" /*this is Hello in spanish*/

"I understand" = "Yo comprendo"

“.strings”格式的 BNF 表示法(大致)

strings-file ::=
       (string-entry)*
string-entry ::=
       key '=' value
key ::=
       '"' (char | uni-char)+ '"'
value ::=
       '"' (char | uni-char)+ '"'
uni-char ::=
       '\' 'U' (0-9,A-F,a-f)+4

执行翻译的机制如下:

  • 调用 Locale::translate()
  • 框架确定可执行文件的资源目录。
  • 框架使用区域设置名称作为要查找的子目录。
  • 确定翻译文件名。
  • 如果翻译文件存在,框架将确定要使用的 MessageLoader
  • 使用 MessageLoader 实例加载翻译文件(参见 MessageLoader::loadMessageFile())。
  • 消息加载器实例提取给定 ID 的消息(参见 MessageLoader::getMessageFromID())。
  • 如果此时翻译结果仍然是空字符串,则返回的结果是传入的原始 ID 值。

实际翻译字符串非常简单:

Locale loc("it", "IT");
String s = loc.translate( "Hello" );

假设您的波兰语 .strings 文件中有以下条目:“The file %s cannot be found.” = “Plik %s nie znaleziony.”

您可以像这样结合 Format 类使用格式化符号:

String fileName = "Income2005.xls";
Locale loc("pl", "PL");
String s = Format( loc.translate( "The file %s cannot be found." ) ) % fileName;

您应该得到字符串:“Plik Income2005.xls nie znaleziony.”

用户界面中的区域设置使用

虽然 Locale 类可以独立于 UI 类使用,因为它属于 FoundationKit 库,但它支持区域设置敏感的文本渲染,并支持各种 UI 元素中的字符串翻译。

要支持 GraphicsKit(处理所有核心绘图功能的库)中的区域设置,您可以为给定的 Font 实例指定一个特定的区域设置实例。默认情况下,Font 具有 null 区域设置。在 Windows 上,在绘制文本之前,会检查当前字体的区域设置值;如果为 NULL,则使用当前线程的区域设置。提取 LCID,并基于此确定 LOGFONT 的字符集。如果无法从 LCID 确定字符集,则使用 DEFAULT_CHARSET 值。这确保了字体能够正确渲染。这确实假定用户在其系统上拥有支持 Unicode 字符的正确字体。

所有这些允许我们使用默认区域设置,或覆盖它并动态控制要使用的区域设置。例如:

GraphicsContext* ctx = ....//get graphics context
Locale loc("pl", "PL");
ctx->getCurrentFont()->setLocale( &loc );
ctx->textAt( 100, 100, "Czecs" );  //"Hello" in Polish

这将正确绘制文本“Czecs”,包括重音字符。

ApplicationKit(提供 UI 功能的库)使用 Locale 类为各种 UI 类(如控件、窗口标题和菜单项)提供翻译支持。这意味着您可以设置控件(如 CommandButtonLabel)的标题,框架将在运行时查找文本的翻译。您也可以选择关闭此自动行为,如果您不想这样做。这意味着您只需要提供一个特定于区域设置的翻译文件,并且对于大多数 UI 元素,本地化文本的显示将发生,而无需您编写任何额外代码。

为自定义控件或组件添加区域设置支持

如果您编写自定义控件,您可能希望像其他控件一样为区域设置提供内置支持。这样做非常容易。让我们创建一个显示日期和时间的小控件,并确保它已本地化以进行显示。首先,让我们创建控件类:

class DateTimeLabel : public CustomControl {
protected:
       TimerComponent* timer;
       String extraTxt;
       Locale* locale;
       void onTimer( Event* e ) {
               repaint();
       }
public:

       DateTimeLabel() : CustomControl(false),locale(NULL){

       setBorder( new TitledBorder() );

       timer = new TimerComponent(this);
       timer->setTimeoutInterval ( 1000 );
       timer->TimerPulse +=
              new GenericEventHandler<DateTimeLabel>(this, 
              &DateTimeLabel::onTimer, 
              "DateTimeLabel::onTimer" );
       }

       virtual ~DateTimeLabel() {
               delete locale;
       }

       void setLocale( Locale* loc ) {
               if ( NULL != locale ) {
                       delete locale;
               }

               locale = new Locale( loc->getLanguageCode(), 
                                    loc->getCountryCode() );

               TitledBorder* border = (TitledBorder*)getBorder();
               border->setCaption( locale->getLanguageName() );
               border->getFont()->setLocale( locale );
       }

       void setLocale( const String& lang, const String& country ) {
               if ( NULL != locale ) {
                       delete locale;
               }

               locale = new Locale( lang, country );

               TitledBorder* border = (TitledBorder*)getBorder();
               border->setCaption( locale->getLanguageName() );
               border->getFont()->setLocale( locale );
       }

       void start() {
               timer->setActivated ( true );
       }

       void stop() {
               timer->setActivated ( false );
       }

       void setExtraTxt( const String& val ) {
               extraTxt = val;
               repaint();
       }

       String getExtraTxt() {
               return extraTxt;
       }

};

这就是创建控件所需的所有内容。我们添加了一个成员 String,它保存一些额外的文本。我们还添加了一个指向自定义 Locale 实例的成员,我们对其进行跟踪并允许用户随时更改。最后,我们有一个计时器组件,每秒触发一次并重绘控件,以便我们可以显示当前本地化的日期和时间。

我们添加了两个函数来控制计时器 - stop() 停止计时器组件触发,start() 启动计时器组件的计时器事件。

但是,它还没有自行绘制。为此,我们需要重写 Controlpaint() 函数。完成后,我们就可以创建一个窗口并将控件添加到窗口中。

让我们看看我们的自定义 paint 函数:

class DateTimeLabel : public CustomControl {
protected:
       TimerComponent* timer;
       String extraTxt;
       Locale* locale;
       //rest omitted

       virtual void paint( GraphicsContext* ctx ) {

       CustomControl::paint( ctx );

       //get the current locale
       Locale* currentLocale = System::getCurrentThreadLocale();

       if ( NULL != locale ) {
               currentLocale = locale;
       }

       String localizedExtra = extraTxt;

       //check if we can localize the string
       if ( getUseLocaleStrings() ) {
               //Yep! Let's get the localized version. 
               //Worst case scenario is that
               //no translation exists, which means 
               //localizedExtra will be the same
               //as extraTxt
               localizedExtra = currentLocale->translate( extraTxt );
       }


       DateTime dt = DateTime::now();
       //localize the date/time value into a string
       String dateStr = currentLocale->toStringFromDate( dt, 
                        "dddd, MMM d yyyy" );
       String timeStr = currentLocale->toStringFromTime ( dt );


       ctx->getCurrentFont()->setName( "Times New Roman" );
       ctx->getCurrentFont()->setPointSize( 16 );

       Rect r = getClientBounds();

       Rect xtraRect = r;
       xtraRect.bottom_ = xtraRect.top_ + 
                          ctx->getTextHeight( localizedExtra );

       long textDrawOptions = GraphicsContext::tdoCenterHorzAlign;

       ctx->textBoundedBy( &xtraRect, 
                              localizedExtra, textDrawOptions );

       Rect textRect = r;

       textRect.inflate( -10, -10 );
       textRect.top_ = xtraRect.bottom_;

       ctx->getCurrentFont()->setBold( true );

       textDrawOptions = GraphicsContext::tdoWordWrap | 
                         GraphicsContext::tdoCenterHorzAlign;
       ctx->textBoundedBy( &textRect, dateStr + 
                              "\n" + timeStr, textDrawOptions );
   }
};

我们首先调用超类(CustomControl::paint(ctx))的 paint,以确保基本绘图操作(如绘制背景)已完成。然后,我们确定要使用的当前区域设置。接下来,我们翻译我们的“额外”字符串,并获取当前日期和时间的本地化字符串。然后,我们计算两个矩形:一个用于绘制额外文本,另一个用于绘制日期/时间文本。通过调用 GraphicsContext::textBoundedBy() 函数来实际绘制文本。

现在我们有了控件,可以通过创建简单的顶级框架窗口,然后添加多个实例来使用它。让我们看看那段代码:

Window* window = new Window();

DateTimeLabel* label;

label = new DateTimeLabel();

label->setLocale( "en", "US" );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

window->add( label, AlignTop );

label->start();


label = new DateTimeLabel();

label->setLocale( "it", "IT" );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

window->add( label, AlignTop );

label->start();


label = new DateTimeLabel();

label->setLocale( "pl", "PL" );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

window->add( label, AlignTop );

label->start();


label = new DateTimeLabel();

label->setLocale( "de", "DE" );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

window->add( label, AlignTop );

label->start();


label = new DateTimeLabel();

Locale loc( Locale::lcJapanese, Locale::ccJapan );
label->setLocale( &loc );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

window->add( label, AlignTop );

label->start();

label = new DateTimeLabel();

Locale loc2( Locale::lcRussian, Locale::ccRussianFederation );
label->setLocale( &loc2 );

label->setExtraTxt( "Hello it's:" );

label->setHeight( 100 );

add( label, AlignTop );

window->label->start();

window->setBounds( 100.0, 100.0, 350.0, 620.0 );
window->show();

我们现在有六个不同的自定义控件实例,看起来像这样:

您会注意到,我们的额外文本“Hello it's:”尚未翻译。我们需要将翻译文件添加到应用程序中。为此,我们只需在可执行文件同一级别的目录下创建一个名为“Resources”的目录。然后,我们在“Resources”目录下列出子目录,每个子目录对应一个我们要支持的区域设置。在这种情况下,我们将为德语、意大利语、波兰语和俄语区域设置创建名为“de_DE”、“it_IT”、“pl_PL”和“ru_RU”的目录。

然后,我们为每个区域设置创建一个 .strings 文件,并将其放在每个子目录中。文件名必须是可执行文件的名称加上“.strings”扩展名。文件内容将如下所示:

file name: Resources/de_DE/LocaleUI.strings
/*German*/
"Hello it's:" = "Hallo ist es:"
file nam lang=texte: Resources/ru_RU/LocaleUI.strings
/*Russian*/
"Hello it's:" = "\U0417\U0434\U0440\U0430\U0432\U0441\
                  U0442\U0432\U0443\U043B\U0442\
                  U0435! \U043E\U043D\U043E:"
file name: Resources/it_IT/LocaleUI.strings
/*Italian*/
"Hello it's:" = "Ciao è:"
file name: Resources/pl_PL/LocaleUI.strings
/*Polish*/
"Hello it's:" = "Cze\U0107\U015B to jest:"

请注意,我在此提供的翻译可能不太准确 - 我从 Babelfish 获取了德语和俄语翻译。我对德国和俄罗斯的发言人表示歉意。

有了这些文件,我们现在可以看到翻译文本的效果了!

关于构建示例的说明

您需要安装最新版本的 VCF(至少 0-9-0 或更高版本),并且需要确保您已构建 VCF 的静态库(与 DLL 版本相对)。示例配置为静态链接到 VCF。有关构建 VCF 的更多文档,请参见:构建 VCF,位于 VCF 在线文档中。

结论

我们几乎涵盖了 VCF 中使用区域设置的所有基本内容,以及 Locale 类的各种特性和函数。还有一些我们尚未涵盖的高级区域设置问题,例如自定义数字字符串解析或格式化。这可能会在 VCF 的未来版本中添加,但目前不支持。

但是,我们确实看到了从字符串中提取和转换基本类型的能力,获取区域设置信息,翻译字符串,然后在用户界面中利用所有这些。

欢迎对框架有疑问,您可以将它们发布在这里,或者在 我们的论坛中发布。如果您对如何改进这些方面有任何建议,我们很乐意听取!

© . All rights reserved.