20230630 7. 國際化

國際化

Java 編程語言是第一種設計成爲全面支持國際化的語言。從一開始,它就具備了進行有效的國際化所必需的一個重要特性:使用 Unicode 來處理所有字符串。支持 Unicode 使得在 Java 編程語言中,編寫程序來操作多種語言的字符串變得異常方便

國際化一個程序所要做的事情絕不僅僅是提供 Unicode 支持。在世界的不同地方,日期、時間、貨幣甚至數字的格式都不相同

Locale 對象( Locale

不同的國家可以使用相同的語言

爲了對格式化進行控制,可以使用 Locale 類。locale 由多達 5 個部分構成:

  1. 一種語言,由 2 個或 3 個小寫字母表示,例如 en (英語)、 de (德語)和 zh (中文)
  2. 可選的一段腳本,由首字母大寫的四個字母表示,例如 Latn (拉丁文)、 Cyrl (西里爾文)和 Hant (繁體中文字符) 這個部分很有用,因爲有些語言,例如塞爾維亞語,可以用拉丁文或西里爾文書寫, 而有些中文讀者更喜歡閱讀繁體中文而不是簡體中文
  3. 可選的一個國家或地區,由 2 個大寫字母或 3 個數字表示,例如 US(美國)和 CH(瑞士)
  4. 可選的一個變體,用於指定各種雜項特性,例如方言和拼寫規則。變體現在已經很少使用了。過去曾經有一種挪威語的變體“尼諾斯克語”,但是它現在已經用另一種不同的代碼 nn 來表示了。過去曾經用於日本帝國曆和泰語數字的變體現在也都被表示成了擴展
  5. 可選的一個擴展。擴展描述了日曆(例如日本歷)和數字(替代西方數字的泰語數字)等內容的本地偏好。Unicode 標準規範了其中的某些擴展,這些擴展應該以 u- 和兩個字母代碼開頭,這兩個字母的代碼指定了該擴展處理的是日曆( ca )還是數字( nu ),或者是其他內容。例如,擴展 u-nu-thai 表示使用泰語數字。其他擴展是完全任意的,並且以 x- 開頭,例如 x-java

locale 的規則

locale 是用標籤描述的,標籤是由 locale 的各個元素通過連字符連接起來的字符串,例如 en-US

在德國,你可以使用 de-DE 。瑞士有 4 種官方語言(德語、法語、意大利語和裏託羅曼斯語)。在瑞士講德語的人希望使用的 localede-CH 。這個 locale 會使用德語的規則,但是貨幣值會表示成瑞士法郎而不是歐元

如果只指定了語言,例如 de ,那麼該 locale 就不能用於與國家相關的場景,例如貨幣

用標籤字符串來構建 Locale 對象:

System.out.println(Locale.getDefault());    // zh_CN

Locale usEnglish = Locale.forLanguageTag("en-US");
System.out.println(usEnglish);    // en-US

String languageTag = Locale.US.toLanguageTag();
System.out.println(languageTag);    // en-US

Java SE 爲各個國家預定義了 Locale 對象:

Locale.CANADA
Locale.CANADA_FRENCH
Locale.CHINA
Locale.FRANCE
Locale.GERMANY
Locale.ITALY
Locale.JAPAN
Locale.KOREA
Locale.PRC
Locale.TAIWAN
Locale.UK
Locale.US

Java SE 還預定義了大量的語言 Locale ,它們只設定了語言而沒有設定位置:

Locale.CHINESE
Locale.ENGLISH
Locale.FRENCH
Locale.GERMAN
Locale.ITALIAN
Locale.JAPANESE
Locale.KOREAN
Locale.SIMPLIFIED_CHINESE
Locale.TRADITIONAL_CHINESE

靜態的 getAvailableLocales 方法會返回由 Java 虛擬機所能夠識別的所有 Locale 構成的數組

Locale[] availableLocales = Locale.getAvailableLocales();

除了構建一個 Locale 或使用預定義的 Locale 外,還可以有兩種方法來獲得一個 Locale 對象

Locale 類的靜態 getDefault 方法可以獲得作爲本地操作系統的一部分而存放的默認 Locale 。可以調用 setDefault 來改變默認的 Java Locale ;但是,這種改變只對你的程序有效,不會對操作系統產生影響

對於所有與 Locale 有關的工具類,可以返回一個它們所支持的 Locale 數組

Locale[] numberFormatAvailableLocales = NumberFormat.getAvailableLocales();
Locale[] dateFormatAvailableLocales = DateFormat.getAvailableLocales();

爲了測試,你也許希望改變你的程序的默認 Locale ,可以在啓動程序時提供語言和地域特性。比如,下面的語句將默認的 Locale 設爲 de-CH

java -Duser.language=de -Duser.region=CH MyProgram

Locale 中唯一有用的是那些識別語言和國家代碼的方法,其中最重要的一個是 getDisplayName ,它返回一個描述 Locale 字符串。這個字符串並不包含前面所說的由兩個字母組成的代碼,而是以一種面向用戶的形式來表現

String displayName = Locale.CHINA.getDisplayName();
System.out.println(displayName);    // 中文 (中國)

System.out.println(Locale.GERMANY.getDisplayName());    // 德文 (德國)
System.out.println(Locale.GERMANY.getDisplayName(Locale.GERMANY));    // Deutsch (Deutschland)


Locale locale = Locale.CHINA;
System.out.println(locale.toLanguageTag());     // zh-CN
System.out.println(locale.toString());      // zh_CN

所以爲什麼需要 Locale 對象。你把它傳給 Locale 感知的那些方法,這些方法將根據不同的地域產生不同形式的文本

java.util.Locale 方法名稱 方法聲明 描述
構造器 public Locale(String language)
public Locale(String language, String country)
public Locale(String language, String country, String variant)
用給定的語言、國家和變量創建一個 Locale 。在新代碼中不要使用變體,應該使用 IETF BCP 47 語言標籤
forLanguageTag public static Locale forLanguageTag(String languageTag) 構建與給定的語言標籤相對應的 Locale 對象
getDefault public static Locale getDefault() 返回默認的 Locale
setDefault public static synchronized void setDefault(Locale newLocale) 設定默認的 Locale
getDisplayName public final String getDisplayName() 返回一個在當前的 Locale 中所表示的用來描述 Locale 的名字
getDisplayName public String getDisplayName(Locale inLocale) 返回一個在給定的 Locale 中所表示的用來描述 Locale 的名字
getLanguage public String getLanguage() 返回語言代碼,它是兩個小寫字母組成的 ISO-639 代碼
getDisplayLanguage public final String getDisplayLanguage() 返回在當前 Locale 中所表示的語言名稱
getDisplayLanguage public String getDisplayLanguage(Locale inLocale) 返回在給定 Locale 中所表示的語言名稱
getCountry public String getCountry() 返回國家代碼,它是由兩個大寫字母組成的 ISO-3166 代碼
getDisplayCountry public final String getDisplayCountry() 返回在當前 Locale 中所表示的國家名
getDisplayCountry public String getDisplayCountry(Locale inLocale) 返回在當前 Locale 中所表示的國家名
toLanguageTag public String toLanguageTag() 返回該 Locale 對象的語言標籤
toString public final String toString() 返回 Locale 的描述,包括語言和國家,用下劃線分隔(比如,de_CH ) 應該只在調試時使用該方法

數字格式( NumberFormat

數字和貨幣的格式是高度依賴於 locale 。Java 類庫提供了一個格式器 ( formatter )對象的集合,可以對 java.text 包中的數字值進行格式化和解析。你可以通過下面的步驟對特定 Locale 的數字進行格式化:

  1. 使用上一節的方法,得到 Locale 對象
  2. 使用一個“工廠方法”得到一個格式器對象
  3. 使用這個格式器對象來完成格式化和解析工作

工廠方法是 java.text.NumberFormat 類的靜態方法,它們接受一個 Locale 類型的參數。總共有 3 個工廠方法: getNumberInstancegetCurrencyInstancegetPercentInstance 。這些方法返回的對象可以分別對數字、貨幣量和百分比進行格式化和解析

Locale loc = Locale.GERMAN;
NumberFormat currFmt = NumberFormat.getCurrencyInstance(loc);
double amt = 123456.78;
String result = currFmt.format(amt);

System.out.println(result);
/*
    Locale.CHINA    ¥123,456.78
    Locale.JAPAN    ¥123,457
    Locale.US       $123,456.78
    Locale.UK       £123,456.78
    Locale.GERMANY  123.456,78 €    帶國家地區
    Locale.GERMAN   ¤ 123.456,78    
*/

如果要想讀取一個按照某個 Locale 的慣用法而輸入或存儲的數字,那麼就需要使用 parse 方法。parse 方法能夠處理小數點和分隔符以及其他語言中的數字

String source = "123.456,78 €";
NumberFormat fmt = NumberFormat.getNumberInstance(Locale.GERMANY);
Number number = fmt.parse(source.trim());
double doubleValue = number.doubleValue();
System.out.println(doubleValue);    // 123456.78

parse 的返回類型是抽象類型的 Number 。返回的對象是一個 DoubleLong 的包裝器類對象,這取決於被解析的數字是否是浮點數。如果不關心兩者的差異,可以直接使用 Number 類中的 doubleValue 方法來讀取被包裝的數字

警告Number 類型的對象並不能自動轉換成相關的基本類型,因此,不能直接將一個 Number 對象賦給一個基本類型,而應該使用 doubleValueintValue 方法

如果數字文本的格式不正確,該方法會拋出一個 ParseException 異常。例如,字符串以空白字符開頭是不允許的(可以調用 trim 方法來去掉它) 。但是,任何跟在數字之後的字符都將被忽略,所以這些跟在後面的字符是不會引起異常的

getXxxInstance 工廠方法返回的類並非是 NumberFormat 類型的。NumberFormat 類型是 個抽象類,而我們實際上得到的格式器是它的一個子類。工廠方法只知道如何定位屬於特定 locale 對象

可以用靜態的 getAvailableLocales 方法得到一個當前所支持的 Locale 對象列表。這個方法返回一個 Locale 對象數組,從中可以獲得針對它們的數字格式器對象

可以使用 Scanner 來讀取本地化的整數和浮點數,可以調用 useLocale 方法來設置 locale

java.text.NumberFormat 方法名稱 方法聲明 描述
getAvailableLocales public static Locale[] getAvailableLocales() 返回一個 Locale 對象的數組,其成員包含有可用的 NumberFormat 格式器
getNumberInstance
getCurrencyInstance
getPercentInstance
public final static NumberFormat getNumberInstance()
public static NumberFormat getNumberInstance(Locale inLocale)
public final static NumberFormat getCurrencyInstance()
public static NumberFormat getCurrencyInstance(Locale inLocale)
public final static NumberFormat getPercentInstance()
public static NumberFormat getPercentInstance(Locale inLocale)
爲當前的或給定的 locale 提供處理數字、貨幣量或百分比的格式器
format public final String format(double number)
public final String format(long number)
對給定的浮點數或整數進行格式化並以字符串的形式返回結果
parse public Number parse(String source) throws ParseException 解析給定的字符串並返回數字值,如果輸入字符串描述了一個浮點數,返回類型就是 Double ,否則返回類型就是 Long 。字符串必須以一個數字開頭;以空白字符開頭是不允許的。數字之後可以跟隨其他字符,但它們都將被忽略。解析失敗時拋出 ParseException 異常
setParseIntegerOnly
isParseIntegerOnly
public void setParseIntegerOnly(boolean value)
public boolean isParseIntegerOnly()
設置或獲取一個標誌,該標誌指示這個格式器是否應該只解析整數值
setGroupingUsed
isGroupingUsed
public void setGroupingUsed(boolean newValue)
public boolean isGroupingUsed()
設置或獲取一個標誌,該標誌指示這個格式器是否會添加和識別十進制分隔符(比如,100000 )
setMinimumIntegerDigits
getMinimumIntegerDigits
setMaximumIntegerDigits
getMaximumIntegerDigits
setMinimumFractionDigit
getMinimumFractionDigits
setMaximumFractionDigit
getMaximumFractionDigits
public void setMinimumIntegerDigits(int newValue)
public int getMinimumIntegerDigits()
public void setMaximumIntegerDigits(int newValue)
public int getMaximumIntegerDigits()
public void setMaximumFractionDigits(int newValue)
public int getMaximumFractionDigits()
設置或獲取整數或小數部分所允許的最大或最小位數

DecimalFormat

上一節中看到的各個 NumberFormat 工廠方法都會返回 DecimalFormat 類的一個實例。這個類描述了世界各地的各種格式化機制。可以修改現有對象的每個設置項,也可以創建全新的格式化器。模式語法使這種設置變得更簡便了

模式描述了必需和可選的數字位數,以及正數和負數的前綴和後綴等。

DecimalFormat 的屬性

屬性名 描述 格式
GroupingSize 羣組在一起的整數數字位數(通常是3或4) 在最後一個 , 和整數部分末尾之間的數字位數,例如 #,### 表示羣組尺寸是 3
MinimumFractionDigits / MaximumFractionDigits 小數部分最少和最大的位數 在小數部分中使用必需(0)和可選(#)的位數: .00##
MinimumIntegerDigits 整數部分最少的位數 整數中必需(0)的位數: 000
MaximumIntegerDigits 最大的整數位數 使用指數表示法時,整數部分允許出現的 # 的數量,例如 ###,##0.00E0 。如果不使用指數表示法,則不能在模式中設置該屬性
- 指數部分最少的位數 在 E 部分中使用 0 的位數: #.00E00
Multiplier 百分號和千分號 % 表示百分號, (U+2030) 表示千分號
NegativePrefix / NegativeSuffix / PositivePrefix / PositiveSuffix 正數和負數的前綴和後綴 用字面值表示的前綴和後綴包圍模式中的正數和負數部分。例如,在 +#0.00;(#) 中,+ 表示正數的前綴,且沒有指定後綴,用圓括號將負數括了起來
DecimalSeparatorAlwaysShown 如果在小數部分爲 0 也顯示分隔符,則設置爲 true 不能在模式中設置

例如

var formatter = new DecimalFormat("0.00;(#)");

模式中的分號將正數和可選的負數部分分隔開。在正數部分中,其整數部分至少有一位數字,而在小數部分至少有兩位數字。所有的數字都要滿足這兩點,無論其符號是正還是負。負數使用了會計模式,即包含一個前綴(和一個後綴)

前綴和後綴可以包含一個貨幣符號(U+00A4) ,用來表示貨幣符號應該出現的位置

如果需要在前綴或後綴中插入特殊字符,需要在其前面插入一個單引號。例如,前綴 '# 表示字面哈希符號,而 o''clock 中包含一個單引號

數字位、分隔符和其他各種數字部分的實際符號可以從 DecimalFormatSymbols 對象中獲取,你也可以自定義該對象

DecimalFormatSymbols 的屬性

屬性 類型 描述
CurrencySymbol String 像 `
--- --- ---
EUR 這樣的字符串,用於包含貨幣符號的前綴或後綴中
DecimalSeparator / MonetaryDecimalSeparator char 小數分隔符,用於數字或貨幣值
ExponentSeparator String 在指數 E 部分之前的字符串,通常是 E
GroupingSeparator / MonetaryGroupingSeparator char 羣組分隔符,用於數字或貨幣值
Infinity / NaN String 用於格式化的字符串,Double.POSITIVE_INFINITY , Double.NEGATIVE_INFINITY , Double.NaN
InternationalCurrencySymbol String 遵循 ISO 4217 標準的貨幣符號
MinusSign char 在未指定負數模式的情況下使用的負號
Percent / PerMill char 用於百分號和千分號的符號
ZeroDigit char 用於數字位0的字符。其他數字位爲後續的 9 個 Unicode 字符

貨幣( Currency

爲了格式化貨幣值,可以使用 NumberFormat.getCurrencyInstance 方法。但是,這個方法的靈活性不好,它返回的是一個只針對貨幣的格式器。假設你爲一個美國客戶準備了一張貨物單,貨單中有些貨物的金額是用美元表示的,有些是用歐元表示的,此時,你不能只是使用兩種格式器:

NumberFormat dollarFormatter = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.GERMANY);

這樣一來,你的發票看起來非常奇怪,有些金額的格式像 $100 000 ,另一些則像 100.000 €

處理這樣的情況,應該使用 Currency 類來控制被格式器所處理的貨幣

NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.US);
String format = euroFormatter.format(100000);
System.out.println(format);     // $100,000.00


euroFormatter.setCurrency(Currency.getInstance("CNY"));
format = euroFormatter.format(100000);
System.out.println(format);     // CNY100,000.00


Currency currency = Currency.getInstance(Locale.CHINA);
System.out.println(currency);   // CNY

貨幣標識符由 ISO 4217 定義,參考

java.util.Currency 方法名稱 方法聲明 描述
getInstance public static Currency getInstance(String currencyCode)
public static Currency getInstance(Locale locale)
返回與給定的 ISO 4217 貨幣代號或給定的 Locale 中的國家相對應的 Curreny 對象
toString
getCurrencyCode
public String toString()
public String getCurrencyCode()
獲取該貨幣的 ISO 4217 代碼
getSymbol public String getSymbol()
public String getSymbol(Locale locale)
根據默認或給定的 Locale 得到該貨幣的格式化符號。比如美元的格式化符號可能是 `
---------------------------------- ------------------------------------------------------------ ------------------------------------------------------------
getInstance public static Currency getInstance(String currencyCode)
public static Currency getInstance(Locale locale)
返回與給定的 ISO 4217 貨幣代號或給定的 Locale 中的國家相對應的 Curreny 對象
toString
getCurrencyCode
public String toString()
public String getCurrencyCode()
獲取該貨幣的 ISO 4217 代碼
US$ ,具體是哪種形式取決於 Locale
getDefaultFractionDigits public int getDefaultFractionDigits() 獲取該貨幣小數點後的默認位數
getAvailableCurrencies public static Set<Currency> getAvailableCurrencies() 獲取所有可用的貨幣

日期和時間( DateTimeFormatter

當格式化日期和時間時 ,需要考慮 4 個與 Locale 的問題:

  • 月份和星期應該用本地語言來表示
  • 年月日的順序要符合本地習慣
  • 公曆可能不是本地首選的日期表示方法
  • 必須要考慮本地的時區

java.time.format.DateTimeFormatter 類可以處理這些問題:

FormatStyle dateStyle = FormatStyle.FULL;
FormatStyle timeStyle = FormatStyle.FULL;
FormatStyle dateTimeStyle = FormatStyle.FULL;
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(dateStyle);
DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(timeStyle);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(dateTimeStyle);
// DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle);

DateTimeFormatter usDateTimeFormatter = dateTimeFormatter.withLocale(Locale.US);

ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(dateTimeFormatter.format(zonedDateTime));    // 2022年2月11日 星期五 下午02時25分31秒 CST
System.out.println(usDateTimeFormatter.format(zonedDateTime));  // Friday, February 11, 2022 2:25:31 PM CST

格式器都會使用當前的 Locale 。爲了使用不同的 Locale ,需要使用 withLocale 方法

可以格式化 LocalDateLocalDateTimeLocalTimeZonedDateTime

這裏使用的是 java.time 包中的 DateTimeFormatter 。還有一種來自於 Java 1.1 的遺留的 java.text.DateFormatter 類,它可以操作 DateCalendar 對象

可以使用 LocalDateLocalDateTimeLocalTimeZonedDateTime 的靜態的 parse 方法來解析字符串中的日期和時間:

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
String dateStr = dateTimeFormatter.format(LocalDate.now());

LocalDate localDate = LocalDate.parse(dateStr, dateTimeFormatter);

LocalDate parse = LocalDate.parse("2022-02-11");

警告:日期格式器可以解析不存在的日期,例如 November 31 ,它會將這種日期調整爲給定月份的最後一天

有時,你需要顯示星期和月份的名字,例如在日曆應用中 此時可以調用 DayOfWeekMonth 枚舉的 getDisplayName 方法:

System.out.println("======== Month : ");
for (Month month : Month.values()) {
    System.out.println(month.getDisplayName(TextStyle.FULL, Locale.getDefault()));
}

System.out.println("======== DayOfWeek : ");
for (DayOfWeek dayOfWeek : DayOfWeek.values()) {
    System.out.println(dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()));
}

注意:星期的第一天可以是星期六、星期日或星期一,這取決於 Locale ,可以這樣獲取星期的第一天:

DayOfWeek firstDayOfWeek = WeekFields.of(Locale.getDefault()).getFirstDayOfWeek();
System.out.println(firstDayOfWeek);     // SUNDAY

排序和規範化( Collator

使用 String 類中的 compareTo 方法對字符串進行比較, compareTo 方法使用的是字符串的 UTF-16 編碼值,這可能導致與正常認知不同的結果

爲了獲得 Locale 敏感的比較符,可以調用靜態的 Collator.getInstance 方法:

List<String> words = new ArrayList<>();
Locale locale = Locale.getDefault();
Collator coll = Collator.getInstance(locale);
words.sort(coll);   // Collator implements java.util.Comparator<Object>

coll.setStrength(Collator.PRIMARY);     // 強度
coll.setDecomposition(Collator.NO_DECOMPOSITION);       // 範化

排序器有幾個高級設置項,你可以設置排序器的 強度 以此來選擇不同的排序行爲。字符間的差別可以被分爲首要的( primary )、其次的( secondary )和再次的( tertiary )

Unicode 標準對字符串定義了四種 範化 形式(normalization form) :DKDCKC

java.text.Collator 方法名稱 方法聲明 描述
getAvailableLocales public static synchronized Locale[] getAvailableLocales() 返回 Locale 對象的一個數組,該 Collator 對象可用於這些對象
getInstance public static synchronized Collator getInstance()
public static Collator getInstance(Locale desiredLocale)
爲默認或給定的 locale 返回一個排序器
compare public abstract int compare(String source, String target); 如果 sourcetarget 之前,則返回負值;如果它們等價,則返回 0 ,否則返回正值
equals public boolean equals(String source, String target) 如果它們等價,則返回 true ,否則返回 false
setStrength
getStrength
public synchronized void setStrength(int newStrength)
public synchronized int getStrength()
設置或獲取排序器的強度。更強的排序器可以區分更多的詞
setDecomposition
getDecomposition
public synchronized void setDecomposition(int decompositionMode)
public synchronized int getDecomposition()
設置或獲取排序器的分解模式。分解越細,判斷兩個字符串是否相等時就越嚴格
getCollationKey public abstract CollationKey getCollationKey(String source); 返回一個排序器鍵,這個鍵包含一個對一組字符按特定格式分解的結果,可以快速地和其他排序器鍵進行比較
java.text.CollationKey 方法名稱 方法聲明 描述
compareTo abstract public int compareTo(CollationKey target); 如果這個鍵在 target 之前,則返回一個負值;如果兩者等價,則返回 0 ,否則返回正值
java.text.Normalizer 方法名稱 方法聲明 描述
normalize public static String normalize(CharSequence src, Form form) 返回 src 的範化形式

消息恪式化( MessageFormat

Java 類庫中有一個 MessageFormat 類,它與用 printf 方法進行格式化很類似,但是它支持 Locale ,並且會對數字和日期進行格式化

格式化數字和日期( numbertimedate

String msg = MessageFormat.format("On 【 {2} 】, a 【 {0} 】 destroyed 【 {1} 】 houses and caused 【 {3} 】 of damage. ", "hurrricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8);
System.out.println(msg);	// On 【 99-1-1 上午12:00 】, a 【 hurrricane 】 destroyed 【 99 】 houses and caused 【 1,000,000,000 】 of damage.

一般來說,佔位符索引後面可以跟一個類型( type )和一個風格( style ),它們之間用逗號隔開。類型可以是:

number 
time 
date 
choice

如果類型是 number ,那麼風格可以是

integer
currency
percent

或者可以是數字格式模式,就像 $,##0 (參見 DecimalFormat 類)

如果類型是 timedate ,那麼風格可以是

short
medium
long
full

或者是一個日期格式模式,就像 yyyy-MM-dd (參見 SimpleDateFormat 類)

警告: 靜態的 MessageFormat.format 方法使用當前的 locate 對值進行格式化。要想用任意的 locale 進行格式化,需要將要格式化的值置於 Object[] 數組

String pattern = "On 【 {2,date,short} 】, a 【 {0} 】 destroyed 【 {1} 】 houses and caused 【 {3,number,currency} 】 of damage. ";
Locale locale = Locale.getDefault();
MessageFormat mf = new MessageFormat(pattern, locale);
String msg =
        mf.format(new Object[]{"hurrricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8});

System.out.println(msg);    // On 【 99-1-1 】, a 【 hurrricane 】 destroyed 【 99 】 houses and caused 【 ¥1,000,000,000.00 】 of damage.
java.text.MessageFormat 方法名稱 方法聲明 描述
構造器 public MessageFormat(String pattern)
public MessageFormat(String pattern, Locale locale)
用給定的模式和 locale 構建一個消息格式對象
applyPattern public void applyPattern(String pattern) 給消息格式對象設置特定的模式
setLocale
getlocale
public void setLocale(Locale locale)
public Locale getLocale()
設置或獲取消息中佔位符所使用的 locale 。這個 locale 僅僅被通過調用 applyPattern 方法所設置的後續模式所使用
format public static String format(String pattern, Object ... arguments) 通過使用 args[i]作爲佔位符 {i} 的輸入來格式 pattern 字符串
format public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) 格式化 MessageFormat 模式。 args 參數必須是一個對象數組。被格式化的字符串會被附加到 result 末尾,並返回 result 。如果 pos 等於 new FieldPosition(MessageFormat.Field.ARGUMENT),就用它的 beginIndexendIndex 屬性值來設置替換佔位符 {1} 的文本位置。如果不關心位置信息,可以將它設爲 null
java.text.Format 方法名稱 方法聲明 描述
format 按照格式器的規則格式化給定的對象,這個方法將調用 format(obj, new StringBuffer(), new FieldPosition(1)).toString()

選擇格式( choice

我們希望消息能夠隨佔位符的值而變化,這樣就能根據具體的值形成

no houses 
one house 
2 houses

choice 格式化選項就是爲了這個目的而設計的,一個選擇格式是由一個序列對構成的,每一個對包括

  • 一個下限( lower limit)
  • 一個格式字符串( format string )

下限和格式字符串由一個 # 符號分隔,對與對之間由符號 | 分隔

{1, choice, 0#no houses|1#one house|2#{1} houses}

文本文件和字符集

Java 編程語言自身是完全基於 Unicode 。 但是, Windows 和 Mac OS 仍舊支持遺留的字符編碼機制,例如西歐國家的 Windows-1252 和 Mac Roman ,以及中國臺灣的 Big5

文本文件

最好是使用 UTF-8 來存儲和加載文本文件

可以在讀寫文本文件時指定字符編碼:

PrintWriter out = new PrintWriter(filename, "Windows-1252");

如果想要獲得最佳的編碼機制,可以通過下面的調用來獲得“平臺的編碼機制”:

Charset platformEncoding = Charset.defaultCharset();

行結束符

這不是 Locale 的問題,而是平臺的問題。在 Windows 中,文本文件希望在每行末尾使用 \r\n ,而基於 UNIX 的系統只需要一個 \n 字符

任何用 println 方法寫入的行都會是被正確終止的。唯一的問題是你是否打印了包含 \n 字符的行。它們不會被自動修改爲平臺的行結束符

與在字符串中使用 \n 不同,可以使用 printf%n 格式說明符來產生平臺相關的行結束符

out.printf("Hello%nWorld%n");

控制檯

如果你編寫的程序是通過 System.in / System.out / System.console 與用戶交互的,那麼就不得不面對控制檯使用的字符編碼機制與 CharSet.defaultCharset() 報告的平臺編碼機制有所差異的可能性

Charset charset = Charset.defaultCharset();
System.out.println(charset);    // UTF-8

Windows 上的 cmd 工具,可以通過【屬性-選項-當前代碼頁】,查看使用的字符集

切換控制檯的字符編碼爲 UTF-8

chcp 65001

這種命令還不足 Java 在控制檯中使用 UTF-8,我們還必須使用非官方的 file.encoding 系統屬性來設置平臺的編碼機制:

java -Dfile.encoding=UTF-8 MyProg

UTF-8 字節順序標誌

如果你的應用必須讀取其他程序 建的 UTF-8 文本文件,那麼你可能會碰到另一個問題。在文件中添加一個“字節順序標誌”字符 U+FEFF 作爲文件的第一個字符,是一種完全合法的做法。在 UTF-16 編碼機制中,每個碼元都是一個兩字節的數字,字節順序標誌可以告訴讀入器該文件使用的是“高字節在前”還是“低字節在前”的字節順序。UTF-8 是一種單字節編碼機制,因此不需要指定字節的順序。但是如果一個文件以字節 0xEF 0xBB 0xBF (U+FEFF 的 UTF-8 編碼)開頭,那麼這就是一個強烈暗示,表示該文件使用了 UTF-8 。正是因爲這個原因,Unicode 標準鼓勵這種實踐方式。任何讀入器都被認爲會丟棄最前面的字節順序標誌

Oracle 的 Java 實現很固執地因潛在的兼容性問題而拒絕遵循 Unicode 標準。作爲程序員,這對你而言意味着必須去執行平臺並不會執行的操作。在讀入文本文件時,如果開頭碰到了 U+FEFF ,那麼就忽略它

警告: 遺憾的是, JDK 的實現沒有遵循這項建議。在向 javac 編譯器傳遞有效的以字節順序標誌開頭的 UTF-8 源文件時,編譯會以產生錯誤消息 illegal character: 65279 而失敗

源文件的字符編碼

作爲程序員,要牢記你需要與 Java 編譯器交互,這種交互需要通過本地系統的工具來完成。 例如,可以使用中文版的記事本來寫你的 Java 源代碼文件。但這樣寫出來的源碼不是隨處可用的,因爲它們使用的是本地的字符編碼( GB 或 Big5 ),這取決於你使用的是哪種中文操作系統)。

只有編譯後的 class 文件才能隨處使用,因爲它們會自動地使用 modified UTF-8 編碼來處理標識符和字符串。 這意味着即使在程序編譯和運行時,依然有 3 種字符編碼參與其中:

  • 源文件:本地編碼
  • 類文件:modified UTF-8
  • 虛擬機:UTF-16

你可以用 -encoding 標記來設定你的源文件的字符編碼,例如

javac -encoding UTF-8 Myfile.java

爲了使你的源文件能夠到處使用,必須使用普通的 ASCII 編碼。也就是說,你需要將所有非 ASCII 字符轉換成等價的 Unicode 編碼。JDK 包含一個工具 native2ascii ,可以用它來將本地字符編碼轉換成普通的 ASCII 這個工具直接將輸入中的每一個非 ASCII 字符替換爲一個 \u 加四位十六進制數字的 Unicode 值

native2ascii Myfile.java Myfile.temp
# 逆向轉換
native2ascii -reverse Myfile.temp Myfile.java
# 指定編碼
native2ascii -encoding UTF-8 Myfile.java Myfile.temp

資源包

在外部定義消息字符串,通常稱之 爲資源 ( resource )

Class 類的 getResource 方法可以找到相應的文件,打開它並返回資源的 URL 。通過將文件放置到 JAR 文件中,你就將查找這些資源文件的工作留給了類的加載器去處理,加載器知道如何定位 JAR 文件中的項。但是,這種機制不支持 locale

定位資源包

當本地化一個應用時,會產生很多 資源包( resource bundle )。每一個包都是一個屬性文件或者是一個描述了與 locale 相關的項的類(比如消息、標籤等) 。對於每一個包,都要爲所有你想要支持的 locale 提供相應的版本

需要對這些包使用一種統一的命名規則。 例如,爲德國定義的資源放在一個名爲 包名_de_DE 的文件中,而爲所有說德語的國家所共享的資源則放在名爲 包名_de 的文件中。一般來說,使用 包名_語言_國家 來命名所有和國家相關的資源,使用 包名_語言 來命名所有和語言相關的資源。最後,作爲後備,可以把默認資源放到一個沒有後綴的文件中。

加載一個資源包:

String bundleName = "my";
ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);
String v1 = currentResources.getString("k1");
System.out.println(v1);

getBundle 方法試圖加載匹配當前 locale 定義的語言和國家的包。如果失敗,通過依次放棄國家和語言來繼續進行查找,然後同樣的查找被應用於默認的 locale ,最後,如果還不行的話就去查看默認的包文件,如果這也失敗了,則拋出一個 MissingResourceException 異常。

這就是說, getBundle 方法會試圖加載以下的包

包名_當前 Locale 的語言_當前 Locale 的國家_當前 Locale 的變量
包名_當前 Locale 的語言_當前 Locale 的國家
包名_當前 Locale 的語言
包名_默認 Locale 的語言_默認 Locale 的國家_默認 Locale 的變量
包名_默認 Locale 的語言_默認 Locale 的國家
包名_默認 Locale 的語言
包名

一旦 getBundle 方法定位了一個包,比如,包名_de_DE ,它還會繼續查找 包名_de包名 這兩個包。如果這些包也存在,它們在資源層次中就成爲了 包名_de_DE 的父包。以後,當查找一個資源時,如果在當前包中沒有找到,就去查找其父包。就是說,如果一個特定的資源在當前包中沒有被找到,比如,某個特定資源在 包名_de_DE 中沒有找到,那麼就會去查詢 包名_de包名

不需要把你的程序的所有資源都放到同一個包中。可以用一個包來存放按鈕標籤,用另一個包存放錯誤消息等

屬性文件

對字符串進行國際化是很直接的,你可以把所有字符串放到一個屬性文件中,這是一個每行存放一個鍵 - 值對的文本文件。

警告:存儲屬性的文件都是 ASCII 文件。如果你需要將 Unicode 字符放到屬性文件中,那麼請用 \uxxxx 編碼方式對它們進行編碼

可以使用 native2ascii 工具來產生這些文件

包類

爲了提供字符串以外的資源,需要定義類,它必需擴展自 ResourceBundle 。應該使用標準的命名規則來命名你的類,比如

MyProgramResources.java
MyProgramResources_en.java
MyProgramResources_de_DE.java

警告:當搜索包時,如果在類中的包和在屬性文件中的包中都存在匹配,優先選擇類中的包

Locale currentLocale = Locale.getDefault();

String bundleName = "v2ch07.myclass";

ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);
String v1 = currentResources.getString("k1");
System.out.println(v1);     // class_vv1
package v2ch07;

import java.util.ListResourceBundle;

public class myclass_zh_CN extends ListResourceBundle {

    private static final Object[][] contents = {{"k1", "class_vv1"}};


    @Override
    protected Object[][] getContents() {
        return contents;
    }
}
java.util.ResourceBundle 方法名稱 方法聲明 描述
getBundle public static final ResourceBundle getBundle(String baseName)
public static final ResourceBundle getBundle(String baseName, Locale locale)
在給定的 locale 或默認 locale 下以給定的名字加載資源綁定類和它的父類。如果資源包類位於一個 Java 包中,那麼類的名字必須包含完整的包名。資源包類必須是 public 的,這樣 getBundle 方法才能訪問它們
getObject public final Object getObject(String key) 從資源包或它的父包中查找一個對象
getString public final String getString(String key) 從資源包或它的父包中查找一個對象並把它轉型成字符串
getStringArray public final String[] getStringArray(String key) 從資源包或它的父包中查找一個對象並把它轉型成字符串數組
getKeys public abstract Enumeration<String> getKeys(); 返回一個枚舉對象,枚舉出資源包中的所有鍵,也包括父包中的鍵
handleGetObject protected abstract Object handleGetObject(String key); 如果你要定義自己的資源查找機制,那麼這個方法就需要被覆寫,用來查找與給定的鍵相關聯的資源的值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章