细谈软件界面多语言

    软件要走出国门,迈向国际客户,必须要实现软件界面多语言。

比如download.com,elance.com,odesk.com,猪八戒国际站等,所发布定制的软件外包程序,要求是多语言的,英韩,英汉,英西界面等等。

   软件程序多语言解决最简单的方法

如:大名鼎鼎的GNU GetText,另外,还有一种方式是操作程序的资源文件。

通过多语言代码生成工具,图:

实现方法:

1.  建立一个文件夹, 把要支持多语种的源码工程拷贝到此文件夹下, 工程数目不限, 源文件数目不限。 (同时你备份一份源码工程)

点击选择工程目录按钮, 选择此目录。

2.点击选择配置目录按钮,选择随EXE一起的配置文件目录, 同时你的翻译文档也拷贝到这个配置目录。

3.点击转成多语种按钮, 这时转换开始,下面的文本框会提示转换过程发生的一切。  转换一般瞬间即可完成, 除非源码和翻译信息是海量, 时间可能会稍长些。


OK,第一步建立的文件夹下的所有源码工程现在都是支持多语种的了, 就这么简单。

    当然还有别的方法,通过编程语言来实现软件程序多语言

  Delphi实现软件多语言

先按照Delphi2010,图:

也可以安装Delphi的IDE---cnpack ide,支持c+与Delphi的IDE

Delphi 2010作为一个优秀的快速RAD开发工具,可以很容易地实现国际化支持,因为Delphi 2010内置了对多语言界面的支持。

   你必须设计好你的软件界面,以便能够使你的软件界面元素有足够的空间显示语言文字信息。一般说来,在50个字节以内的英文单词所表达的意思,用其他的语言来描述的话,长度要超过50字节,但中文是一个例外。特别对于几个字节的英文单词,其他的语言的长度几乎百分之百要超过英文的长度!因此,必须在控件中留出足够的长度以便在更改语言之后,还能显示全部的语言文字信息。

3. 你必须翻译所有的资源。

Delphi 2010实现多语言的支持和切换,界面设计和上述要求不在本文讨论范围之内。

要为程序添加语言支持,只要在Delphi主菜单项Project下面选择LanguagesàAdd…即可。点击之后出现语言向导,读者按照向导进行操作即可。向导结束之后,会生成一个工程组文件(BPG),最后出现Translation Manager,软件开发者可以在这里翻译所有语言的所有资源,包括字体、位置、文字等等。说明一下:你可以随时随地用Project下面的Languages子菜单的功能来添加、删除、修改各种界面元素。

做完上述工作之后,我们现在就差切换语言的代码了。为了切换语言,大家可以使用下面的一个单元[],单元中提供了两个函数,用来更换语言界面元素,其中LoadNewResourceModule是用来修改文字信息等等,ReinitializeForms用来重新刷新窗体和控件以保证同步。


   c+builder实现软件多语言界面,以vc++为主

  • 以Unicode为核心

  • 采用 GNU gettext 

  • 基于Qt的多语言开发工具:Qt Linguist 

以Unicode为核心


软件多国语言化,使程序员在编码处理上花费了大量时间和精力;然而各种各样的乱码问题,如 XML 格式错误、文本显示异常、解析器异常等依然层出不穷。特别的,相对于 JAVA 语言,C/C++ 在处理编码问题上有更大的困难。本文避免纠缠不同编码格式的具体异同,以 Unicode 为核心,以简体中文为例,从工程应用角度分析编码问题存在的原因,不仅提出 C/C++ 标准库编程的解决方案,更结合项目经验,总结出处理多国语言编码问题的一般思路。

resolution

多国语言的存在、不同语言操作系统的存在,使得针对多语言的设计颇费周章,在编码上所付出的工作量也是可观的。所谓编码的问题,归结起来,就是二进制的编码以何种编码格式进行解析的问题。特别是在硬盘文件和内存数据的相互转化、即读写过程中,如果采用了错误的编码格式,就会造成乱码。JAVA 语言在字符串、编码等处理方面给了程序员更为直接、方便的接口,习惯使用 JAVA 做编码的程序员,在使用 C/C++ 进行文本编码相关的操作时,常会感到困惑。本文的目的在于以常用的 Unicode(UCS-2)、GB2312、UTF8 三种编码为例,分析不同编码在实用中的关系,特别是 C/C++ 中,怎样处理各种编码的问题。

编码处理常见的问题

  • 1. 将内存中编码 A 的字符串以编码 B 格式处理成字节流写入文件
  • 2. 将原本以 A 编码组成的文件以字节流形式读入内存、并以编码 B 解析为字符串。

第一种情况,可能造成数据的变化、失真。

如果使用 JAVA 语言,发生这种错误的情况稍少一些,因为在 JAVA 中没有 wstring 这种概念,在内存中的 String,使用的编码都是 Unicode,其中的转换对于程序员来讲是透明的。只要使用输入 / 输出方法时注意字节流的字符集选择即可。

例如,编码为中文 GB2312 的“标准”字符串被读入内存后转存为 UTF8 的过程:

图 1. 文件转换编码的 JAVA 处理方式

文件转换编码的 JAVA 处理方式

但 C/C++ 编程,由于通常使用 char、string 类型的时候比较多,特别是进行文件读写,基本都是操作 char* 类型的数据。并且也没有像 JAVA 中 getByte(String charsetname) 这种函数,不能直接根据字符集重新编码得到字符串的 byte 数组。这时候,我们使用的 string 其实就一般不是 Unicode,而是符合某种编码表的。这使得我们往往困惑于 string 的编码问题。假设有 utf8 的字符串“一”(E4 B8 80),而我们错误的认为它是符合 gb2312(编码 A)的,并将其转换为 utf8(编码 B),这种转换结果是破坏性的,错误的输出将永远无法正确识别。依然以“标准”为例,这是一个正确的转换:

图 2. 文件转换编码的 C/C++ 处理方式

文件转换编码的 C/C++ 处理方式

第二种情况,则是更常见到的。例如:浏览器浏览网页时的发生的乱码问题;在写 XML 文件时,指定了 < ?xml version="1.0" encoding="utf-8" ?> 然而文件中却包含 GB2312 的字符串——这样经常会导致 XML 文件 bad formatted,而使得解析器出错。

这种情况下,其实数据都是正确的,只要浏览器选择正确的编码,将 XML 文件中的 GB2312 转换为 UTF8 或者修改 encoding,就可以解决问题。

需要注意的是,ASCII 码的字符,即单字节字符,一般不受编码变动影响,在所有编码表中的值是一样的;需要小心处理的是多字节字符,例如中文语言。

一般的编码转换,直接做映射的不太可能,需要比较多的工作量,大多情况下还是选择 Unicode 作为转换的中介。

使用库函数

如在开发JAVA项目中 的 String 对象是以 Unicode 编码存在的,所以 JAVA 程序员主要关心的是读入时判断字节流的编码,从而确保可以正确的转化为 Unicode 编码;相比之下,C/C++ 将外部文件读出的数据存为字符数组、或者是 string 类型;而 wstring 才是符合 Unicode 编码的双字节数组。一般常用的方法是 C 标准库的 wcstombs、mbstowcs 函数,和 windows API 的 MultiByteToWideChar 与 WideCharToMultiByte 函数来完成向 Unicode 的转入和转出。

这里以 MBs2WCs 函数的实现说明 GB2312 向 Unicode 的转换的主要通过 多字节字符串向宽字节字符串转换

wchar_t * MBs2WCs(const char* pszSrc)
 wchar_t* pwcs = NULL;  
intsize = 0;  #ifdefined(_linux_)    
setlocale(LC_ALL, "zh_CN.GB2312");  
 size = mbstowcs(NULL,pszSrc,0);   
pwcs = new wchar_t[size+1];   
size = mbstowcs(pwcs, pszSrc, size+1);  
 pwcs[size] = 0;  #else   
size = MultiByteToWideChar(20936, 0,
pszSrc, -1, 0, 0);   if(size <= 0)   
 returnNULL;   pwcs = new wchar_t[size];  
 MultiByteToWideChar(20936, 0
pszSrc, -1, pwcs, size);  #endif   returnpwcs; 
 }

相应的,WCs2MBs 可以将宽字符串转化为字节流。

清单 2. 宽字节字符串向多字节字符串转换

char* WCs2MBs(const wchar_t * wcharStr)
 char* str = NULL;  intsize = 0;  #ifdefined(_linux_)    
setlocale(LC_ALL, "zh_CN.UTF8");  
 size = wcstombs( NULL, wcharStr, 0);  
 str = new char[size + 1];  
 wcstombs( str, wcharStr, size);  
 str[size] = '\0';  #else  
 size = WideCharToMultiByte( CP_UTF8, 0
wcharStr, -1, NULL, NULL, NULL, NULL );   
str = new char[size];   
WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, str, size, NULL, NULL );  #endif   returnstr; 
 }

Linux 的 setlocale 的具体使用可以参阅有 C/C++ 文档,它关系到文字、货币单位、时间等很多格式问题。Windows 相关的代码中 20936 和宏定义 CP_UTF8 是 GB2312 编码对应的的 Code Page[ 类似的 Code Page 参数可以从 MSDN 的 Encoding Class 有关信息中获得 ]。

这里需要特别指出的是 setlocale 的第二个参数,Linux 和 Windows 是不同的:

  • 1. 笔者在 Eclipse CDT + MinGW 下使用 [country].[charset](如 zh_CN.gb2312 或 zh_CN.UTF8)的格式并不能通过编码转换测试,但可以使用 Code Page,即可以写成 setlocale(LC_ALL, ".20936") 这样的代码。这说明,这个参数与编译器无关,而与系统定义有关,而不同操作系统对于已安装字符集的定义是不同的。
  • 2. Linux 系统下可以参见 /usr/lib/locale/ 路径,系统所支持的 locale 都在这里。转换成 UTF8 时,并不需要 [country] 部分一定是 zh_CN,en_US.UTF8 也可以正常转换。

另外,标准 C 和 Win32 API 函数返回值是不同的,标准 C 返回的 wchar_t 数组或者是 char 数组都没有字符串结束符,需要手动赋值,所以 Linux 部分的代码要有区别对待。

最后,还要注意应当在调用这两个函数后释放分配的空间。如果将 MBs2WCs 和 WCs2MBs 的返回值分别转化为 wstring 和 string,就可以在它们函数体内做 delete,这里为了代码简明,故而省略,但请读者别忘记。

第三方库

目前的第三方工具已经比较完善,这里介绍两个,本文侧重点不在此,不对其做太多探讨。

  • Linux 上存在第三方的 iconv 项目,使用也较为简单,其实质也是以 Unicode 作为转换的中介。可以参阅  iconv 相关网站。
  • ICU 是一个很完善的国际化工具。其中的 Code Page Conversion 功能也可以支持文本数据从任何字符集向 Unicode 的双向转换。可以访问其 网站

在代码中调用“编码转换方法”一节里提到的函数,将 gb2312 编码的字符串转换为 UTF8 编码,分析其编码转换的行为:

在英文 Linux 环境下,执行下列命令:

export LC_ALL=zh_CN.gb2312

然后编译并执行以下程序(其中汉字都是在 gb2312 环境中写入源文件)

L1:  wstring ws = L"一"; 
L2:  string s_gb2312 = "一";  
L3:  wchar_t * wcs = MBs2WChar(s_gb2312.c_str()); 
L4:  char* cs = WChar2MBs(wcs);

查看输出:

  • L1 - 1 wide char: 0x04bb
  • L2 - 2 bytes:0xd2,0xbb,即 gb2312 编码 0xD2BB
  • L3 - 返回的 wchar_t 数组内容为 0x4E00,也就是 Unicode 编码
  • L4 - 将 Unicode 再度转换为 UTF8 编码,输出的字符长度为 3,即 0xE4,oxB8,0x80

在 L1 行,执行结果显示编码为一个 0x04bb,其实这是一个转换错误,如果使用其他汉字,如“哈”,编译都将无法通过。也就是说 Linux 环境下,直接声明中文宽字符串是不正确的,编译器不能够正确转换。

而在中文 windows 下使用相同测试代码,则会在 L1 处出现区别,ws 中的 wchar_t 元素十六进制值是 0x4e00,这是汉字“一”的 Unicode 编码。

首先,这里先简单说明一下 Unicode 和 UTF8 的关系:Unicode 的实现方式和它的编码方式并不相同,UTF8 就是其实现之一。比方使用 UltraEdit 打开 UTF8 编码的中文文件,使用 16 进制查看,可以发现看到的中文对应部分应当是 Unicode 编码,每个中文字长度 2 字节—— UltraEdit 在这里已经做了转化;如果直接查看其二进制文件,可以发现是 3 字节。但两者的差别仅在于 Unicode 向 UTF8 做了数学上的转化。

其次,关于第三方库的选择,应当综合考虑项目的需求。一般的文本字符转换,系统的库函数已经可以满足需求,实现也很简单;如果需要针对不同地区的语言、文字、习惯进行编程,需要更为丰富的功能,当然选择成熟的第三方工具可以事半功倍。

最后,从逻辑上保持字符串的编码正确,需要注意几条一般规律:

  • 编码选择 :多国语言环境的编程,以使用 UTF 编码为原则,减少字符集转换。
  • string 并不包含编码信息,但是编码确定了 string 的二进制内容。
  • 读写一致 :读入时使用的字符集要与写出时使用的一致。如果不需要改变字符串内容,仅仅是将字符串读入、再写出,建议不要调整任何字符集——即使程序使用的系统默认字符集 A 与文件的实际编码 B 不符合,写出的字符串依然会是正确的 B 编码。
  • 读入已知 :对于必须处理、解析或显示的字符串,从文件读入时必须知道它的编码,避免处理字符串的代码简单使用系统默认字符集;即便对于程序从系统中收集到的内存字符串,也应知道其符合的编码格式——一般为系统默认字符集。
  • 避免直接使用 Unicode :这里是说将非 ASCII 编码的 16 进制或者 10 进制数值用 &# 与 ; 包含起来的使用方式,例如将中文“一”写成“&#4e00;”。这种方法的实质是 Unicode 编码直接写入文件。这不仅会降低代码的通用性、输出文件的可读性,处理起来也很困难。比如法文字符在其他字符集中是大于 80H 的单字节字符,程序同时要支持中文的时候,很有可能会将多字节的中文字符错误割裂。
  • 避免陷入直接的字符集编程 :国际化、本地化的工具已经比较成熟,非纯粹做编码转换的程序员没有必要自己去处理不同编码表的映射转换问题。
  • Unicode/UTF8 并不能解决一切乱码问题 :Unicode 可以说是将世界语言统一起来的一套编码。但是这并不意味着在一个系统中可以正常显示的按照 UTF8 编码的文件,在另一个系统中也可以正常显示。例如,在中文的 UTF8 编码或者 Unicode 编码在没有东亚语言包支持的法文系统中,依然是不可识别的乱码——尽管 UTF8、Unicode 它们都支持。

采用 GNU gettext

参考:http://zh.wikipedia.org/wiki/Gettext

gettext 是 GNU 国际化与本地化 (i18n)函数库。它常被用于编写多语言程序。

开发 

程序源代码需要进行修改以响应 GNU gettext 请求。多数 编程语言 均已通过字符封装的方式实现了对其的支持。为了减少输入量和代码量,此功能通常以标记 别名 _ 的形式使用,所以例如以下 C语言 代码:

printf(gettext("My name is %s.\n"), my_name); 

应当写作:

printf(_("My name is %s.\n"), my_name); 

gettext使用其中的字符串寻找对应的其他语言翻译,若没有可用翻译则返回原始内容。

除 C语言 外, GNU gettext 还支持  C++ ,  Objective-C , Pascal / Object Pascal, sh 脚本, bash 脚本, Python ,GNU  CLISP , Emacs Lisp ,librep,GNU Smalltalk , Java ,GNU  awk , wxWidgets (通过 wxLocale类),YCP ( YaST2 语言), Tcl , Perl , PHP , Pike , Ruby 以及 R 。用法均与在 C语言 上类似。

xgettext程序从源代码生成.pot文件,作为源代码中需翻译内容的模板。一个典型的 .pot 文件条目应当是这样的:

#: src/name.c:36 msgid "My name is %s.\n" msgstr "" 

注释 被直接放置在字符串前,用于帮助翻译者理解待翻译内容:

/// TRANSLATORS: Please leave %s as it is, because it is needed by the program. /// Thank you for contributing to this project. printf(_("My name is %s.\n"), my_name); 

本例中的注释是以///开头的,其作用是用于 xgettext 程序生成 .pot 模板文件。

xgettext --add-comments=///

在 .pot文件中的注释应为以下形式:

#. TRANSLATORS: Please leave %s as it is, because it is needed by the program. #. Thank you for contributing to this project.  #: src/name.c:36 msgid "My name is %s.\n" msgstr "" 

翻译 

翻译者需要工作的对象是 .po 文件,它是由 msginit 程序从 .pot 模板文件生成的。例如使用 msginit 初始化法语翻译文件时,我们运行以下命令:

msginit --locale=fr --input=name.pot 

这将会使用指定的 name.pot 在当前目录创建一个 fr.po,其中的一个条目应该是以下形式的:

#: src/name.c:36 msgid "My name is %s.\n" msgstr "" 

翻译者需要手工或使用类似 Poedit 、 gtranslator 或 Emacs 等工具的相应模式编辑该文件。翻译完成后,文件应为如下的样子:

#: src/name.c:36 msgid "My name is %s.\n" msgstr "Je m'appelle %s.\n" 

最后 .po 文件需要使用 msgfmt 编译为 .mo 文件以用作发布。

运行 

使用 Unix 类型操作系统的用户只需设置 环境变量 中的 LC_MESSAGES ,程序将自动从相应的 .mo 文件中读取语言信息。

补充:最新版 gettext-0.18.3.2可在MSVC中实现多语言

参考:http://www.aslike.net/showart.asp?id=154

“通常,程序及其文档信息都是用英语语言写的,程序运行时同用户交互的信息也是英语。这是一个事实,不仅仅GNU的软件是这样,其他大部分私有软件或自由软件也是这样。一方面,对于来自所有国家的开发者、维护者和用户来说,相互沟通中使用一种通用的语言非常的方便。另一方面,相对于母语来说大多数人并不适应使用英语,而且他们的日常工作都是尽可能的使用他们自己的母语。多数人都会喜欢他们的计算机屏幕显示的英语更少,显示的母语更多。"

" GNU 的 'gettext' 是 GNU翻译项目的一个重要步骤,我们依赖于它 作很多其他的步骤。这个软件包给程序员、翻译者,或者用户提供了一套集成工具和文档。详细地说,GNU gettext 提供了一套工具, 能让其他 GNU 软件创建多语言信息。..."

gettext的工作流程是这样的:比如我们写一个Visual C++(MSVC)程序,通常printf等输出信息都是English的。如果我们在程序中加入gettext支持,在需要交互的字符串上用gettext函数,程序运行是就可以先调用gettext函数获取当前语言的字符串,替换当前的字符串了。注意是运行时替换。

GNU gettext-0.18.3.2 是最新版本, GNU官网 上可以直接下载,只是没有Visual C++(MSVC)可用的运行支持库,只能自己动手编译了,编译好的运行支持库, 点击这里下载 

在Visual C++(MSVC)中使用GNU gettext实现多语言时,可以编写翻译函数来实现界面与菜单字符串的自动替换,程序中的字符串只能一个个手工替换了,这样使用起来,就跟在Delphi与C++Builder中使用GNU gettext差不多方便快捷了。

简单使用的例子

一个简单的例子,

#include <stdio.h>#include <libgnuintl.h>

/*使用gettext通常使用类似下面的一个带函数的宏定义

*你完全可以不用,直接使用 gettext(字符串)

*/

#define _(S) gettext(S)

/*PACKAGE是获取语言字符串的文件名字(运行时输入的命令)*/#define PACKAGE "default"

int main(int argc, char **argv)

{

/* 下面三个参数都是使用gettext时候需要使用的

* setlocale

* bindtextdomain

* textdomain

*/

setlocale(LC_ALL,"");

bindtextdomain(PACKAGE, "locale");

textdomain(PACKAGE);

printf(_("Hello,GetText!\n"));

return 0;

}

其中语言字符串文件的结构: .\locale\语言名称\LC_MESSAGES\default.mo,如简体中文:.\locale\ZH_CN\LC_MESSAGES\default.mo

mo文件是编译后的语言字符串文件,GNU网站上有相应的工具软件可以编辑与生成;

点击这里下载Visual C++(MSVC)中可用的GNU gettext-0.18.3.2运行支持库

基于Qt的多语言开发工具:Qt Linguist


Qt Linguist 是一个用来给 Qt 编写的应用程序增加多语言支持的工具。

QT-Linguist工具主要用在项目的多语言翻译处理过程中,所有先简单介绍一下整个多语言处理过程,最后介绍Linguist的用法。

(一)QT项目实现多语言,必须做两件事:

1)确保每一个用户可见的字符串都使用了tr()函数。

2)在应用程序启动的时候,使用QTranslator载入一个翻译文件(.qm)。 

tr() 的用法:

?
1

caseCheckBox = new QCheckBox(tr( "Match &case" ));

在main()函数里载入翻译文件:

?
1
2
3
4
5
6
7
8
9

int main( int argc, char *argv[])

{

QApplication app(argc, argv);

//翻译程序

QTranslator translator;

translator.load( "spreadsheet_cn.qm" );

app.installTranslator(&translator);

……

}

注意:翻译文件加载的位置必须在界面实例化之前完成。

(二)生成.qm翻译文件

1、 在该应用程序的.pro文件文件中添加TRANSLATIONS项,可分别对应于不同的语言,如:spreadsheet_cn.ts, 对应中文,名字可以自己定义,后缀名.ts不可变动。<.ts是可读的翻译文件,使用简单的XML格式;而.qm是经过.ts转换而成的二进制机器 语言>

2、翻译文件。分三步来完成:

1)运行lupdate, 从应用程序的源代码中提取所有用户可见的字符串。 

2)使用Qt Linguist 翻译该应用程序。 

3)运行lrelease,生成二进制的.qm 文件。 

以上三步均需用到QT自带的命令行控制台,启动方法:开始--->所有程序--->Qt by Nokia v4.6.3 (OpenSource)--->Qt 4.6.3 Command Prompt 

启动命令行后,对应输入如下命令: 

1)lupdate –verbose spreadsheet.pro //生成相应的.ts 文件 

2)linguist //启动Linguist语言翻译工具,可以翻译相应可见字符串 

3)lrelease –verbose spreadsheet.pro //将翻译好的文件生成.qm文件

(三)Linguist 语言工具的使用

1)启动:命令行或者开始菜单均可

2)打开:工具界面中的File--->Open,可以打开所需的 .ts 文件 

3)翻译:界面中部的翻译栏,两行:第一行:Source Text 第二行:… Translation, 在地二行进行相应的翻译即可,翻译完一条之后点击“确定下一个”按钮。 

4)发布:点击File--->Release, 生成 .qm 文件。(与命令行的效果一样)

(四)Linguist 语言工具使用方法建议

1、在代码中所有需要使用中文的地方都用一段英文暂时代替,并用tr()函数做标记。

2、使用Qt Linguist对所有被tr()函数标记的字符串进行翻译,并发布翻译包。

3、在程序中加载翻译包。




     
  





      
 



  



 




    
   

 

   Java语言实现软件多语言


在eclipse的Java软件项目中的菜单栏、导航条、错误提示信息,状态信息等这些固定不变的文本信息,可以把它们写在一个properties文件中,并根据不同的国家编写不同的properties文件。这一组properties文件称之为一个资源包。

创建资源包和资源文件

  一个资源包中的每个资源文件都必须拥有共同的基名。除了基名,每个资源文件的名称中还必须有标识其本地信息的附加部分。例如:一个资源包的基名是“myproperties”,则与中文、英文环境相对应的资源文件名则为: "myproperties_zh.properties"  "myproperties_en.properties"

28231137_DvnQ.jpg

每个资源包都应有一个默认资源文件,这个文件不带有标识本地信息的附加部分。若ResourceBundle对象在资源包中找不到与用户匹配的资源文件,它将选择该资源包中与用户最相近的资源文件,如果再找不到,则使用默认资源文件。例如:myproperties.properties

资源文件的书写格式

  资源文件的内容通常采用"关键字=值"的形式,软件根据关键字检索值显示在页面上。一个资源包中的所有资源文件的关键字必须相同,值则为相应国家的文字。
并且资源文件中采用的是properties格式文件,所以文件中的所有字符都必须是ASCII字码,属性(properties)文件是不能保存中文的,对于像中文这样的非ACSII字符,须先进行编码。

例如:

  国际化的中文环境的properties文件

28231137_Do2B.jpg

国际化的英文环境的properties文件

28231137_Hkim.jpg

java提供了一个native2ascII工具用于将中文字符进行编码处理,native2ascII的用法如下所示:

28231137_jb00.jpg

编程实现固定文本的国际化

  在JavaAPI中提供了一个ResourceBundle 类用于描述一个资源包,并且 ResourceBundle类提供了相应的方法getBundle,这个方法可以根据来访者的国家地区自动获取与之对应的资源文件予以显示。

  ResourceBundle类提供了一个静态方法getBundle,该方法用于装载资源文件,并创建ResourceBundle实例:

Locale currentLocale = Locale.getDefault();
ResourceBundle myResources =ResourceBundle.getBundle(basename, currentLocale);

basename为资源包基名(且必须为完整路径)。
  如果与该locale对象匹配的资源包子类找不到。一般情况下,则选用默认资源文件予以显示。 
  加载资源文件后, 程序就可以调用ResourceBundle 实例对象的 getString 方法获取指定的资源信息名称所对应的值。

String value =  myResources.getString(“key");

范例:根据国家地区自动获取与之对应的资源文件

package me.gacl.i18n;

import java.util.Locale;
import java.util.ResourceBundle;
/**
* @ClassName: I18NTest
* @Description: 编程实现固定文本的国际化
* @author: 孤傲苍狼
* @date: 2014-8-29 下午9:34:05
*
*/ 
public class I18NTest {

    public static void main(String[] args) {
        //资源包基名(包名+myproperties)
        String basename = "me.gacl.i18n.resource.myproperties";
        //设置语言环境
        Locale cn = Locale.CHINA;//中文
        Locale us = Locale.US;//英文
        //根据基名和语言环境加载对应的语言资源文件
        ResourceBundle myResourcesCN = ResourceBundle.getBundle(basename,cn);//加载myproperties_zh.properties
        ResourceBundle myResourcesUS = ResourceBundle.getBundle(basename,us);//加载myproperties_en.properties
        
        //加载资源文件后, 程序就可以调用ResourceBundle实例对象的 getString方法获取指定的资源信息名称所对应的值。
        //String value =  myResources.getString(“key");
        String usernameCN = myResourcesCN.getString("username");
        String passwordCN = myResourcesCN.getString("password");
        
        String usernameUS = myResourcesUS.getString("username");
        String passwordUS = myResourcesUS.getString("password");
        
        System.out.println(usernameCN+"--"+passwordCN);
        System.out.println(usernameUS+"--"+passwordUS);
    }
}

运行结果:

28231137_4psu.jpg

3.4、在WEB应用中实现固定文本的国际化

如下所示:

<%@ page language="java"  import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
  <head>
    <title>国际化(i18n)测试</title>
  </head>
  <%
      //加载i18n资源文件,request.getLocale()获取访问用户所在的国家地区
      ResourceBundle myResourcesBundle = ResourceBundle.getBundle("me.gacl.i18n.resource.myproperties",request.getLocale());
  %>
  <body>
        <form action="" method="post">
            <%=myResourcesBundle.getString("username")%>:<input type="text" name="username"/><br/>
            <%=myResourcesBundle.getString("password")%>:<input type="password" name="password"/><br/>
            <input type="submit" value="<%=myResourcesBundle.getString("submit")%>">
        </form>
  </body>
</html>

运行结果:

  浏览器语言是中文环境下的显示效果:

28231137_OcqK.jpg

 浏览器语言是英文环境下的显示效果:

28231137_KR93.jpg

同样一个页面,在不同语言环境的浏览器下显示出了不同的语言文字效果,这样就实现了固定文本的国际化。

  IE浏览器切换使用语言:工具→Internet选项

28231138_pmJH.jpg

28231138_OBtp.jpg

28231138_wgLA.jpg

四、动态数据的国际化

  数值,货币,时间,日期等数据由于可能在程序运行时动态产生,所以无法像文字一样简单地将它们从应用程序中分离出来,而是需要特殊处理。Java 中提供了解决这些问题的 API 类(位于 java.util 包和 java.text 包中)

Locale 类

  Locale 实例对象代表一个特定的地理,政治、文化区域。
  一个 Locale 对象本身不会验证它代表的语言和国家地区信息是否正确,只是向本地敏感的类提供国家地区信息,与国际化相关的格式化和解析任务由本地敏感的类去完成。(若JDK中的某个类在运行时需要根据 Locale 对象来调整其功能,这个类就称为本地敏感类)

DateFormat类(日期格式化)

  DateFormat 类可以将一个日期/时间对象格式化为表示某个国家地区的日期/时间字符串。
  DateFormat 类除了可按国家地区格式化输出日期外,它还定义了一些用于描述日期/时间的显示模式的 int 型的常量,包括FULL, LONG, MEDIUM, DEFAULT, SHORT,实例化DateFormat对象时,可以使用这些常量,控制日期/时间的显示长度。

实例化DateFormat类

  实例化DateFormat类有九种方式,以下三种为带参形式,下面列出的三种方式也可以分别不带参,或只带显示样式的参数。
  getDateInstance(int style, Locale aLocale):以指定的日期显示模式和本地信息来获得DateFormat实例对象,该实例对象不处理时间值部分。
  getTimeInstance(int style, Locale aLocale):以指定的时间显示模式和本地信息来获得DateFormat实例对象,该实例对象不处理日期值部分。
  getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale):以单独指定的日期显示模式、时间显示模式和本地信息来获得DateFormat实例对象。

、DateFormat 对象的方法    

  format:将date对象格式化为符合某个本地环境习惯的字符串。
  parse:将字符串解析为日期/时间对象
  注意:parse和format完全相反,一个是把date时间转化为相应地区和国家的显示样式,一个是把相应地区的时间日期转化成date对象,该方法在使用时,解析的时间或日期要符合指定的国家、地区格式,否则会抛异常。
  DateFormat 对象通常不是线程安全的,每个线程都应该创建自己的 DateFormat  实例对象

、DateFormat使用范例

package me.gacl.i18n;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;

/**
* @ClassName: DateFormatTest
* @Description: DateFormat类测试
* DateFormat类可以将一个日期/时间对象格式化为表示某个国家地区的日期/时间字符串
* @author: 孤傲苍狼
* @date: 2014-8-29 下午10:03:26
*
*/ 
public class DateFormatTest {

    public static void main(String[] args) throws ParseException {
        Date date = new Date(); // 当前这一刻的时间(日期、时间)

        // 输出日期部分
        DateFormat df = DateFormat.getDateInstance(DateFormat.FULL,Locale.GERMAN);
        String result = df.format(date);
        System.out.println(result);

        // 输出时间部分
        df = DateFormat.getTimeInstance(DateFormat.FULL, Locale.CHINA);
        result = df.format(date);
        System.out.println(result);

        // 输出日期和时间
        df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG,Locale.CHINA);
        result = df.format(date);
        System.out.println(result);

        // 把字符串反向解析成一个date对象
        String s = "10-9-26 下午02时49分53秒";
        df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG,Locale.CHINA);
        Date d = df.parse(s);
        System.out.println(d);
    }
}

、NumberFormat类(数字格式化)

  NumberFormat类可以将一个数值格式化为符合某个国家地区习惯的数值字符串,也可以将符合某个国家地区习惯的数值字符串解析为对应的数值
  NumberFormat类的方法:
    format 方法:将一个数值格式化为符合某个国家地区习惯的数值字符串
    parse 方法:将符合某个国家地区习惯的数值字符串解析为对应的数值。
  实例化NumberFormat类时,可以使用locale对象作为参数,也可以不使用,下面列出的是使用参数的。

getNumberInstance(Locale locale):以参数locale对象所标识的本地信息来获得具有多种用途的NumberFormat实例对象

getIntegerInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理整数的NumberFormat实例对象

getCurrencyInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理货币的NumberFormat实例对象

getPercentInstance(Locale locale):以参数locale对象所标识的本地信息来获得处理百分比数值的NumberFormat实例对象

范例:

package me.gacl.i18n;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

/**
* @ClassName: NumberFormatTest
* @Description: NumberFormat类测试
* @author: 孤傲苍狼
* @date: 2014-8-29 下午10:25:29
*
*/ 
public class NumberFormatTest {

    public static void main(String[] args) throws ParseException {
        int price = 89;

        NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.CHINA);
        String result = nf.format(price);
        System.out.println(result);

        String s = "¥89.00";
        nf = NumberFormat.getCurrencyInstance(Locale.CHINA);
        Number n = nf.parse(s);
        System.out.println(n.doubleValue() + 1);

        double num = 0.5;
        nf = NumberFormat.getPercentInstance();
        System.out.println(nf.format(num));
    }
}

运行结果:

28231138_vRVH.jpg

4.4、MessageFormat(文本格式化)

  如果一个字符串中包含了多个与国际化相关的数据,可以使用MessageFormat类对这些数据进行批量处理。
  例如:At 12:30 pm on jul 3,1998, a hurricance destroyed 99 houses and caused $1000000 of damage
  以上字符串中包含了时间、数字、货币等多个与国际化相关的数据,对于这种字符串,可以使用MessageFormat类对其国际化相关的数据进行批量处理。
  MessageFormat 类如何进行批量处理呢?
    1.MessageFormat类允许开发人员用占位符替换掉字符串中的敏感数据(即国际化相关的数据)。
    2.MessageFormat类在格式化输出包含占位符的文本时,messageFormat类可以接收一个参数数组,以替换文本中的每一个占位符。

、模式字符串与占位符

模式字符串:

  At {0} on {1},a destroyed {2} houses and caused {3} of damage

  字符串中的{0}、{1}、{2}、{3}就是占位符

格式化模式字符串

  1、实例化MessageFormat对象,并装载相应的模式字符串。

  2、使用format(object obj[])格式化输出模式字符串,参数数组中指定占位符相应的替换对象。

范例:

package me.gacl.i18n;

import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;

/**
* @ClassName: MessageFormatTest
* @Description: MessageFormat类测试
* @author: 孤傲苍狼
* @date: 2014-8-29 下午10:29:19
*
*/ 
public class MessageFormatTest {

    public static void main(String[] args) {
        //模式字符串
        String pattern = "On {0}, a hurricance destroyed {1} houses and caused {2} of damage.";
        //实例化MessageFormat对象,并装载相应的模式字符串
        MessageFormat format = new MessageFormat(pattern, Locale.CHINA);
        Object arr[] = {new Date(), 99, 100000000};
        //格式化模式字符串,参数数组中指定占位符相应的替换对象
        String result = format.format(arr);
        System.out.println(result);
    }
}

运行结果:

28231138_r0j9.jpg

、占位符的三种书写方式

  {argumentIndex}: 0-9 之间的数字,表示要格式化对象数据在参数数组中的索引号
  {argumentIndex,formatType}: 参数的格式化类型
  {argumentIndex,formatType,FormatStyle}: 格式化的样式,它的值必须是与格式化类型相匹配的合法模式、或表示合法模式的字符串。

范例:

package me.gacl.i18n;

import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;

/**
* @ClassName: MessageFormatTest
* @Description: MessageFormat类测试
*
*/ 
public class MessageFormatTest {

    public static void main(String[] args) {
        //模式字符串
        String pattern = "At {0, time, short} on {0, date}, a destroyed {1} houses and caused {2, number, currency} of damage.";
        //实例化MessageFormat对象,并装载相应的模式字符串
        MessageFormat format = new MessageFormat(pattern, Locale.US);
        Object arr[] = {new Date(), 99, 100000000};
        //格式化模式字符串,参数数组中指定占位符相应的替换对象
        String result = format.format(arr);
        System.out.println(result);
    }
}

运行结果:

28231138_zHgH.jpg


net平台下的c#编程语言


比如你做一个C# winform 项目,C#如何实现多语言界面程序呢?

方法:首先设计多语言文件,这里我用XML来保存,基本结构如下。 

敲击代码:

  1. < ?xml version = "1.0" encoding = "GB2312"?> 
  2.  < AirControl language="简体中文"> 
  3.      < Menu> 
  4.          < Project> 
  5.              < Item id="0" key="MenuProject" value="项目(&P)" /> 
  6.              < Item id="1" key="MenuProjectItem1" value="新建(&N)" /> 
  7.              < Item id="2" key="MenuProjectItem2" value="打开(&O)" /> 
  8.              < Item id="3" key="MenuProjectItem3" value="保存(&S)" /> 
  9.              < Item id="5" key="MenuProjectItem5" value="退出(&X)" /> 
  10.         < /Project> 
  11.         < Manage> 
  12.             < Item id="0" key="MenuManage" value="管理(&M)" /> 
  13.             < Item id="1" key="MenuManageItem1" value="登录(&I)" /> 
  14.             < Item id="2" key="MenuManageItem2" value="注销(&O)" /> 
  15.             < Item id="3" key="MenuManageItem3" value="修改密码(&C)" /> 
  16.             < Item id="4" key="MenuManageItem4" value="用户管理(&U)" /> 
  17.         < /Manage> 
  18.         < Help> 
  19.             < Item id="0" key="MenuHelp" value="帮助(&H)" /> 
  20.             < Item id="1" key="MenuHelpItem1" value="帮助内容(&H)" /> 
  21.             < Item id="2" key="MenuHelpItem2" value="关于(&A)" /> 
  22.         < /Help> 
  23.     < /Menu> 
  24.     < Toolbar> 
  25.         < Statusbar> 
  26.             < Item id="1" key="StatusItem1" value="用户名: " /> 
  27.             < Item id="2" key="StatusItem2" value="用户组: " /> 
  28.             < Item id="3" key="StatusItem3" value="上次登录时间: " /> 
  29.             < Item id="4" key="StatusItem4" value="本次登录时间:" /> 
  30.         < /Statusbar> 
  31.     < /Toolbar> 
  32.     < Form> 
  33.         < MainForm> 
  34.             < Item id="0" key="MainForm" value="xx" /> 
  35.             < Item id="1" key="buttonGo" value="开始" /> 
  36.             < Item id="2" key="buttonStop" value="停止" />          
  37.             < Item id="3" key="groupBox1" value="用户信息" /> 
  38.             < Item id="4" key="groupBox2" value="常规数据" />      
  39.         < /MainForm> 
  40.         < UserLoginForm> 
  41.             < Item id="0" key="UserLoginForm" value="用户登录" /> 
  42.             < Item id="1" key="labelTitle"  value="xx" /> 
  43.             < Item id="2" key="labelUsername" value="用户名" /> 
  44.             < Item id="3" key="labelPassword" value="密码" /> 
  45.             < Item id="4" key="buttonLogin" value="登录" /> 
  46.         < /UserLoginForm> 
  47.         < ChangePasswordForm> 
  48.             < Item id="0" key="ChangePasswordForm" value="修改密码" /> 
  49.             < Item id="1" key="label1" value="原密码" /> 
  50.             < Item id="2" key="label2" value="新密码" /> 
  51.             < Item id="3" key="label3" value="再输入" /> 
  52.             < Item id="4" key="buttonConfirm" value="确认" /> 
  53.             < Item id="5" key="buttonCancel" value="取消" /> 
  54.         < /ChangePasswordForm> 
  55.     < /Form> 
  56.     < Dialog> 
  57.         < Title> 
  58.             < Item id="0" key="0001" value="xx" /> 
  59.             < Item id="1" key="0002" value="添加测试" /> 
  60.             < Item id="2" key="0003" value="添加用户" /> 
  61.             < Item id="3" key="0004" value="修改密码" /> 
  62.         < /Title> 
  63.         < Message> 
  64.             < Item id="0" key="0000" value="一切正常" /> 
  65.             < Item id="1" key="2001" value="用户名或密码错误" /> 
  66.             < Item id="5" key="2002" value="密码不一致" /> 
  67.             < Item id="6" key="2003" value="用户名已存在" /> 
  68.             < Item id="7" key="2004" value="添加用户成功" />              
  69.         < /Message>              
  70.     < /Dialog> 
  71. < /AirControl> 

这里是语言文件的局部,主体分为四个部分,Menu, Toolbar, Form 和 Dialog,分别对应菜单,工具栏,窗体和对话框的显示字符串。

在Form里面,其每个子树分别对应一个窗体。XML每项有三个域,id 这个只是用来标号,程序中为用,key,value形成一个字典,key是控件的名称,value是控件的text。在Dialog中key用数字编号。

做其他语言文件时,只用将value里面的值改成对应的语言即可。

当然,我们也不一定用XML来写语言文件,简单的ini文件也行。下面设计读取这个XML的类,


  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Xml;  
  6.  
  7. namespace AirLibrary  
  8. {  
  9.     /**//// < summary>  
  10.     /// 本地化类  
  11.     /// < /summary>  
  12.     public static class Localization  
  13.     {  
  14.         Property#region Property  
  15.         public static string Lang { getprivate set; }  
  16.         public static bool HasLang { getset; }          
  17.         #endregion //Property  
  18.  
  19.         Attribute#region Attribute  
  20.         private static Dictionary< string, Dictionary< stringstring>> forms = new Dictionary< string, Dictionary< stringstring>>();  
  21.         private static Dictionary< stringstring> menu = new Dictionary< stringstring>();  
  22.         private static Dictionary< stringstring> toolbar = new Dictionary< stringstring>();  
  23.         private static Dictionary< stringstring> dialog = new Dictionary< stringstring>();  
  24.         #endregion //Attribute  
  25.  
  26.         Method#region Method  
  27.          public static void AddForm(string formName)  
  28.          {  
  29.              forms.Add(formName, new Dictionary< stringstring>());  
  30.              //formMap.Add(formName, count++);  
  31.          }  
  32.    
  33.         /**//// < summary>  
  34.          /// 加载语言文件  
  35.          /// < /summary>  
  36.          /// < param name="lang">语言< /param>  
  37.          /// < returns>< /returns>  
  38.          public static bool Load(string lang)  
  39.          {  
  40.              string path = "";  
  41.              Localization.Lang = "English";  
  42.    
  43.              menu.Clear();  
  44.              toolbar.Clear();  
  45.              dialog.Clear();  
  46.              exception.Clear();  
  47.              foreach (Dictionary< stringstring> form in forms.Values)  
  48.                  form.Clear();  
  49.                
  50.              switch (lang)  
  51.              {  
  52.                  case "zh":                     
  53.                      path = @"resources/lang-zh.xml";  
  54.                      break;  
  55.                  case "en":                    
  56.                      path = @"resources/lang-en.xml";  
  57.                      break;  
  58.                  default:                      
  59.                      path = @"resources/lang-zh.xml";  
  60.                      break;  
  61.              }  
  62.    
  63.              return readLanguage(path);  
  64.          }  
  65.          #endregion //Method  
  66.    
  67.          Function#region Function  
  68.          private static bool readLanguage(string path)  
  69.          {  
  70.              // Read the language file  
  71.              XmlReader reader;  
  72.              try 
  73.              {  
  74.                  reader = XmlReader.Create(path);  
  75.              }  
  76.              catch (Exception)  
  77.              {  
  78.                  return false;  
  79.              }  
  80.    
  81.              // Begin to parase  
  82.             try 
  83.             {  
  84.                 reader.ReadToFollowing("AirControl");  
  85.                 Localization.Lang = reader.GetAttribute("language");  
  86.  
  87.                 paraseXml(reader, "Menu", menu);  
  88.                 paraseXml(reader, "Toolbar", toolbar);  
  89.     
  90.                 foreach (string formName in forms.Keys)  
  91.                 {  
  92.                    paraseXml(reader, formName, forms[formName]);  
  93.                 }  
  94.                 paraseXml(reader, "Dialog", dialog);                 
  95.             }  
  96.             catch (Exception)  
  97.             {  
  98.                 return false;  
  99.             }  
  100.             return true;  
  101.         }  
  102.  
  103.         private static void paraseXml(XmlReader reader, string item, Dictionary< stringstring> obj)  
  104.         {  
  105.             // Get the attribute key & value   
  106.             reader.ReadToFollowing(item);  
  107.  
  108.             XmlReader subreader = reader.ReadSubtree();  
  109.             while (subreader.Read())  
  110.             {  
  111.                 if (subreader.NodeType == XmlNodeType.Element && subreader.Name == "Item")  
  112.                     obj.Add(subreader.GetAttribute("key"), subreader.GetAttribute("value"));  
  113.             }  
  114.         }  
  115.         #endregion //Function  
  116.  
  117.         Property#region Property  
  118.         public static Dictionary< stringstring> Menu  
  119.         {  
  120.             get 
  121.             {  
  122.                 return menu;  
  123.             }  
  124.             private set 
  125.             { }  
  126.         }  
  127.  
  128.         public static Dictionary< stringstring> Toolbar  
  129.         {  
  130.             get 
  131.             {  
  132.                 return toolbar;  
  133.             }  
  134.             private set 
  135.             { }  
  136.         }  
  137.  
  138.         public static Dictionary< string, Dictionary< stringstring>> Forms  
  139.         {  
  140.             get 
  141.             {  
  142.                 return forms;  
  143.             }  
  144.             private set 
  145.             { }  
  146.         }  
  147.    
  148.         public static Dictionary< stringstring> Dialog  
  149.         {  
  150.             get 
  151.             {  
  152.                return dialog;  
  153.             }  
  154.             private set 
  155.             { }  
  156.         }  
  157.         #endregion //Property  
  158.     }  

这里我使用静态类来读取和保存,这样效率相对会高一些。读取XML时,我使用的是XmlReader,它使用流式读取,速度也比较快。

Forms, Menu, Toolbar, Dialog几个属性分别对应XML中的子树,使用.net中的Dictionary范型,Forms嵌套了一层Dictionary。

Load方法是加载语言文件,readLanguage 和paraseXML 函数对XML进行解析,并保存字符串到对应的属性中。

AddForm这个方法是将每个窗体的动态的添加到forms 里面。

在程序开始main 函数中,首先调用AddForm方法,添加所有窗体。


  
  
// 添加所有窗体用于本地化(按XML中顺序)   private static void AddForm()   {       Localization.AddForm("MainForm");       Localization.AddForm("UserLoginForm");       Localization.AddForm("UserManageForm");       Localization.AddForm("ChangePasswordForm");  

然后加载语言文件。 


  
  
if (!Localization.Load("zh"))               {                   MessageBox.Show("无法加载语言配置文件, 将显示英文.""错误", MessageBoxButtons.OK,                       MessageBoxIcon.Exclamation);                   Localization.HasLang = false;               }               else                  Localization.HasLang = true

在每个Form的Load事件中初始化每个控件的Text。 


  
  
if (Localization.HasLang)            RefreshLanguage();       // 更新窗体语言   public static void RefreshLanguage(Form form)   {                  form.Text = Localization.Forms[form.Name][form.Name];        SetControlsLanguage(form, Localization.Forms[form.Name]);   }              递归更新每个控件Text      /// < summary>    /// 设置control子控件语言    /// < /summary>    /// < param name="control">父控件< /param>    /// < param name="obj">语言字典< /param>    public static void SetControlsLanguage(Control control, Dictionary< stringstring> obj)    {         foreach (Control ctrl in control.Controls)         {            // set the control which one's key in the dictionary            string text = "";            if (obj.TryGetValue(ctrl.Name, out text))                  ctrl.Text = text;                if (ctrl.HasChildren)                  SetControlsLanguage(ctrl, obj);          }    } 

另外主窗体的Menu和Toolbar,我采用以下的方法更新。 


  
  
// Refresh the menu language    foreach (ToolStripMenuItem topItem in MainMenuStrip.Items)    {          topItem.Text = Localization.Menu[topItem.Name];          foreach (ToolStripItem item in topItem.DropDownItems)          {                if (item is ToolStripMenuItem)                {                     string text = "";                     if (Localization.Menu.TryGetValue(item.Name, out text))                          item.Text = text;                }          }    }        // Refresh the statusbar language    foreach (ToolStripItem item in mainStatus.Items)    {         string text = "";         if (Localization.Toolbar.TryGetValue(item.Name, out text))                 item.Text = text;    } 

Dialog就直接调用Localization中的Dialog属性即可。

需要转变为不同语言时只需要再调用一次Localization.Load方法。

这样,就完成了C#实现多语言界面程序。

大功告成

adiOS



转载于:https://my.oschina.net/bigfool007139/blog/552555

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值