Java核心技術 國際化

1.Locale對象

對於日期、數字等的顯示不同國家有不同,又若干個專門負責格式處理的類。爲了對格式化進行控制,可以使用Locale類。
locale由多達5個部分構成:
1.一種語言,由2個或3個小寫字母表示:
在這裏插入圖片描述
2.可選的一段腳本,由首字母大寫的四個字母表示。例如Hant(繁體中文字符)
3.可選的一個國家或地區,由2個大寫字母或3個數字表示,例如US(美國)
在這裏插入圖片描述
4.可選的一個變體,用於指定各種雜項特性,例如方言和拼寫規則
5.可選的一個擴展,擴展描述了日曆和數字等內容的本地偏好。例如,u-nu-thai表示使用泰語數字。
locale的規則在Internet Engineering Task Force的“Best Current Practices”備忘錄BCP47進行了明確闡述。也可以在https://www.w3.org/International/articles/language-tags/處找到更容易的理解總結。

// 可以使用標籤字符串來構建Locale對象
Locale usEnglish = Locale.forLanguageTag("en-US");
toLanguageTag方法可以生成給定的Locale語言標籤
System.out.println(Locale.US.toLanguageTag());

Java SE爲各個國家預定義了Locale對象,還預定義了大量的語言Locale,它們只設定了語言而沒有設定位置:

static public final Locale CHINESE = createConstant("zh", "");

靜態方法getAvailableLocales()返回由Java虛擬機所能夠識別的所有Locale構成的數組。
除了構建Locale或使用預定義Locale外,可以有兩種方法獲得Locale對象,靜態getDefault方法可以獲得作爲本地操作系統的一部分而存放的Locale。可以調用setDefault方法改變默認JavaLocale,但是這種改變只對程序有效,對操作系統不會產生影響。
對於所有的Locale相關的工具類,可以返回一個他們所支持的Locale數組:

// 返回所有NumberFormat所能處理的Locale
NumberFormat.getAvailableLocales();

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

Locale loc = new Locale("de", "CH");
System.out.println(loc.getDisplayName(Locale.GERMAN));
// Deutsch (Schweiz)

2.數字格式

Java類庫提供了一個格式器(formatter)對象的集合,可以對java.text包中的數字值進行格式化和解析。可以通過下面步驟對特定Locale的數字進行格式化:
1.使用上一節的方法,得到Locale對象
2.使用一個工廠方法得到一個格式器對象
3.使用這個格式器對象來完成格式化和解析工作
工廠方法是NumberFormat類的靜態方法,它們接受一個Locale類型的參數。總共有3個工廠方法:getNumberInstance、getCurrencyInstance和getPercentInstance,這些方法返回對象可以分別對數字、貨幣量和百分比進行格式化和解析。

Locale loc = Locale.US;
NumberFormat currFmt = NumberFormat.getCurrencyInstance(loc);
double amt = 123456.78;
System.out.println(currFmt.format(amt));
// $123,456.78

使用parse方法,讀取一個按照某Locale的慣用法而輸入或存儲的數字。parse的返回類型是抽象類型的Number。返回的對象是一個Double或Long的包裝器類對象,這取決於解析的數字是否是浮點數。如果不關心差異,直接使用Number類的doubleValue方法來讀取被包裝的數字。
如果數字文本的格式不正確,該方法會拋出一個ParseException異常,例如字符串以空白字符開頭(可以用trim方法去掉)。
由getXxxInstance工廠方法返回的類並非是NumberFormat類型,NumberFormat只是一個抽象類,實際得到的是它的子類。

public class NumberFormatTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new NumberFormatFrame();
            frame.setTitle("NumberFormatTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}

class NumberFormatFrame extends JFrame {
    private Locale[] locales;
    private double currentNumber;
    private JComboBox<String> localeCombo = new JComboBox<>();
    private JButton parseButton = new JButton("Parse");
    private JTextField numberText = new JTextField(30);
    private JRadioButton numberRadioButton = new JRadioButton("Number");
    private JRadioButton currencyRadioButton = new JRadioButton("Currency");
    private JRadioButton percentRadioButton = new JRadioButton("Percent");
    private ButtonGroup rbGroup = new ButtonGroup();
    private NumberFormat currentNumberFormat;

    public NumberFormatFrame() {
        setLayout(new GridBagLayout());

        ActionListener listener = event -> updateDisplay();

        JPanel p = new JPanel();
        addRadioButton(p, numberRadioButton, rbGroup, listener);
        addRadioButton(p, currencyRadioButton, rbGroup, listener);
        addRadioButton(p, percentRadioButton, rbGroup, listener);

        add(new JLabel("Locale:"), new GBC(0, 0).setAnchor(GBC.EAST));
        add(p, new GBC(1, 1));
        add(parseButton, new GBC(0, 2).setInsets(2));
        add(localeCombo, new GBC(1, 0).setAnchor(GBC.WEST));
        add(numberText, new GBC(1, 2).setFill(GBC.HORIZONTAL));

        locales = NumberFormat.getAvailableLocales().clone();
        Arrays.sort(locales, Comparator.comparing(Locale::getDisplayName));
        for (Locale loc : locales) {
            localeCombo.addItem(loc.getDisplayName());
        }
        localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());
        currentNumber = 123456.78;
        updateDisplay();

        localeCombo.addActionListener(listener);

        parseButton.addActionListener(event -> {
            String s = numberText.getText().trim();
            try {
                Number n = currentNumberFormat.parse(s);
                if (n != null) {
                    currentNumber = n.doubleValue();
                    updateDisplay();
                } else {
                    numberText.setText("Parse error: " + s);
                }
            } catch (ParseException e) {

            }
        });
        pack();
    }

    public void addRadioButton(Container p, JRadioButton b, ButtonGroup g, ActionListener listener) {
        b.setSelected(g.getButtonCount() == 0);
        b.addActionListener(listener);
        g.add(b);
        p.add(b);
    }

    public void updateDisplay() {
        Locale currentLocale = locales[localeCombo.getSelectedIndex()];
        currentNumberFormat = null;
        if (numberRadioButton.isSelected()) {
            currentNumberFormat = NumberFormat.getNumberInstance(currentLocale);
        } else if (currencyRadioButton.isSelected()) {
            currentNumberFormat = NumberFormat.getCurrencyInstance(currentLocale);
        } else if (percentRadioButton.isSelected()) {
            currentNumberFormat = NumberFormat.getPercentInstance(currentLocale);
        }
        String formatted = currentNumberFormat.format(currentNumber);
        numberText.setText(formatted);
    }
}

在這裏插入圖片描述
GBC源碼可參考自http://code1.okbase.net/codefile/GBC.java_2012113012759_291.htm

public class GBC extends GridBagConstraints {
    public GBC(int gridx, int gridy){
        this.gridx = gridx;
        this.gridy = gridy;
    }
    public GBC(int gridx, int gridy, int gridwidth, int gridheight){
        this.gridx = gridx;
        this.gridy = gridy;
        this.gridwidth = gridwidth;
        this.gridheight = gridheight;
    }
    public GBC setAnchor(int anchor){
        this.anchor = anchor;
        return this;
    }
    public GBC setFill(int fill){
        this.fill = fill;
        return this;
    }
    public GBC setWeight(double weightx, double weighty){
        this.weightx = weightx;
        this.weighty = weighty;
        return this;
    }
    public GBC setInsets(int distance){
        this.insets = new Insets(distance, distance, distance, distance);
        return this;
    }
    public GBC setInsets(int top, int left, int bottom, int right){
        this.insets = new Insets(top, left, bottom, right);
        return this;
    }
    public GBC setIpad(int ipadx, int ipady){
        this.ipadx = ipadx;
        this.ipady = ipady;
        return this;
    }
}

3.貨幣

NumberFormat.getCurrencyInstance方法靈活性不好,它返回的指示針對一種貨幣的格式器。
比如爲美國客戶設置歐元格式,不能去創建兩個格式器,應該使用Currency類來控制被格式器所處理的貨幣。通過將一種貨幣標識符傳給靜態的Currency.getInstance方法來得到一個Currency對象,然後對每一個格式器都調用setCurrency方法:

NumberFormat euroFormatter = NumberFormat.getCurrencyInstance(Locale.US);
euroFormatter.setCurrency(Currency.getInstance("EUR"));

貨幣標識符有ISO 4217定義:
在這裏插入圖片描述

4.日期和時間

java.time包中的DateTimeFormatter類,而非java1.1遺留的java.text.DateTimeFormatter(可以操作Date和Calendar):
在這裏插入圖片描述

// 使用當前Locale
DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(style).withLocale(locale);
// 可以格式化LocalDate、LocalDateTime、LocalTime、ZonedDateTime
ZonedDateTime appointment = ...;
String formatted = formatter.format(appointment);

可以使用LocalDate、LocalDateTime、LocalTime、ZonedDateTime的靜態的parse方法來解析字符串中的時間和日期:

LocalTime time = LocalTime.parse("9:32 AM", formatter);

這些方法不適合解析人類輸入,因爲它解析不了9:32AM和9:32 am。
日期格式器可以解析不存在的日期,如November 31,它會將日期調整爲給定月的最後一天。

有時候需要顯示星期和月份的名字,可調用DayOfWeek和Month枚舉的getDisplayName:

for (Month m : Month.values()) {
    System.out.println(m.getDisplayName(TextStyle.FULL, Locale.CHINA) + " ");
}
for (DayOfWeek d : DayOfWeek.values()) {
	System.out.println(d.getDisplayName(TextStyle.FULL, Locale.CHINA+ " ");
}

在這裏插入圖片描述
STANDALONE版本用於格式化日期之外的顯示。例如芬蘭語中,一月在日期中是“tammikuuta”,但是單獨顯示是“tammikuu”。
星期的第一天可以是星期六、星期日或星期一,這取決於Locale:

DayOfWeek first = WeekFields.of(Locale.CHINA).getFirstDayOfWeek();
public class DateFormatTest {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new DateTimeFormatterFrame();
            frame.setTitle("DateFormatTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}
class DateTimeFormatterFrame extends JFrame {
    private Locale[] locales;
    private LocalDate currentDate;
    private LocalTime currentTime;
    private ZonedDateTime currentDateTime;
    private DateTimeFormatter currentDateFormat;
    private DateTimeFormatter currentTimeFormat;
    private DateTimeFormatter currentDateTimeFormat;
    private JComboBox<String> localeCombo = new JComboBox<>();
    private JButton dateParseButton = new JButton("Parse");
    private JButton timeParseButton = new JButton("Parse");
    private JButton dateTimeParseButton = new JButton("Parse");
    private JTextField dateText = new JTextField(30);
    private JTextField timeText = new JTextField(30);
    private JTextField dateTimeText = new JTextField(30);
    private EnumCombo<FormatStyle> dateStyleCombo = new EnumCombo<>(FormatStyle.class, "Short", "Medium", "Long", "Full");
    private EnumCombo<FormatStyle> timeStyleCombo = new EnumCombo<>(FormatStyle.class, "Short", "Medium");
    private EnumCombo<FormatStyle> dateTimeStyleCombo = new EnumCombo<>(FormatStyle.class, "Short", "Medium", "Long", "Full");
    public DateTimeFormatterFrame() {
        setLayout(new GridBagLayout());
        add(new JLabel("Locale"), new GBC(0,0).setAnchor(GBC.EAST));
        add(localeCombo, new GBC(1, 0, 2, 1).setAnchor(GBC.WEST));
        add(new JLabel("Date"), new GBC(0,1).setAnchor(GBC.EAST));
        add(dateStyleCombo, new GBC(1, 1).setAnchor(GBC.WEST));
        add(dateText, new GBC(2, 1, 2,1).setFill(GBC.HORIZONTAL));
        add(dateParseButton, new GBC(4, 1).setAnchor(GBC.WEST));
        add(new JLabel("Time"), new GBC(0,2).setAnchor(GBC.EAST));
        add(timeStyleCombo, new GBC(1, 2).setAnchor(GBC.WEST));
        add(timeText, new GBC(2, 2, 2,1).setFill(GBC.HORIZONTAL));
        add(timeParseButton, new GBC(4, 2).setAnchor(GBC.WEST));
        add(new JLabel("Date and Time"), new GBC(0,3).setAnchor(GBC.EAST));
        add(dateTimeStyleCombo, new GBC(1, 3).setAnchor(GBC.WEST));
        add(dateTimeText, new GBC(2, 3, 2,1).setFill(GBC.HORIZONTAL));
        add(dateTimeParseButton, new GBC(4, 3).setAnchor(GBC.WEST));
        locales = Locale.getAvailableLocales().clone();
        Arrays.sort(locales, Comparator.comparing(Locale::getDisplayName));
        for (Locale loc : locales) {
            localeCombo.addItem(loc.getDisplayName());
        }
        localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());
        currentDate = LocalDate.now();
        currentTime = LocalTime.now();
        currentDateTime = ZonedDateTime.now();
        updateDisplay();
        ActionListener listener = event -> updateDisplay();
        localeCombo.addActionListener(listener);
        dateStyleCombo.addActionListener(listener);
        timeStyleCombo.addActionListener(listener);
        dateTimeStyleCombo.addActionListener(listener);
        dateParseButton.addActionListener(event -> {
            String d = dateText.getText().trim();
            try {
                currentDate = LocalDate.parse(d, currentDateFormat);
                updateDisplay();
            } catch (Exception e) {
                dateText.setText(e.getMessage());
            }
        });
        timeParseButton.addActionListener(event -> {
            String t = timeText.getText().trim();
            try {
                currentTime = LocalTime.parse(t, currentTimeFormat);
                updateDisplay();
            } catch (Exception e) {
                timeText.setText(e.getMessage());
            }
        });
        dateTimeParseButton.addActionListener(event -> {
            String t = dateTimeText.getText().trim();
            try {
                currentDateTime = ZonedDateTime.parse(t, currentDateTimeFormat);
                updateDisplay();
            } catch (Exception e) {
                dateTimeText.setText(e.getMessage());
            }
        });
        pack();
    }
    public void updateDisplay() {
        Locale currentLocale = locales[localeCombo.getSelectedIndex()];
        FormatStyle dateStyle = dateStyleCombo.getValue();
        currentDateFormat = DateTimeFormatter.ofLocalizedDate(
                dateStyle).withLocale(currentLocale);
        dateText.setText(currentDateFormat.format(currentDate));
        FormatStyle timeStyle = timeStyleCombo.getValue();
        currentTimeFormat = DateTimeFormatter.ofLocalizedTime(
                timeStyle).withLocale(currentLocale);
        timeText.setText(currentTimeFormat.format(currentTime));
        FormatStyle dateTimeStyle = dateTimeStyleCombo.getValue();
        currentDateTimeFormat = DateTimeFormatter.ofLocalizedDateTime(
                dateTimeStyle).withLocale(currentLocale);
        dateTimeText.setText(currentDateTimeFormat.format(currentDateTime));
    }
}

在這裏插入圖片描述
輔助類EnumCombo類,用Short、Medium和Long等值來填充一個組合框(combo),然後自動將用戶的選擇轉換成整數值DateFormat.SHORT、DateFormat.MEDIUM、DateFormat.LONG。並沒有編寫重複的代碼,而是使用了反射:將用戶的選擇轉換成大寫字母,所有空格都替換成下劃線,然後找到使用這個名字的靜態域的值。

public class EnumCombo<T> extends JComboBox<String> {
    private Map<String, T> table = new TreeMap<>();
    public EnumCombo(Class<?> cl, String... labels) {
        for (String label : labels) {
            String name = label.toUpperCase().replaceAll(" ", "_");
            try {
                java.lang.reflect.Field f = cl.getField(name);
                @SuppressWarnings("unchecked") T value = (T) f.get(cl);
                table.put(label, value);
            } catch (Exception e) {
                label = "(" + label + ")";
                table.put(label, null);
            }
            addItem(label);
        }
        setSelectedItem(labels[0]);
    }
    public T getValue() {
        return table.get(getSelectedItem());
    }
}

5.排序和範化

使用String類中的compareTo方法對字符串進行比較,但compareTo方法使用的是字符串的UTF-16編碼,即使在英文比較中也是如此。
對於下面的5個字符串進行排序的結果爲America、Zulu、able、zebra、Ångstrom,對於英語讀者來說期望大小寫是等價排序是:able、America、Ångstrom、zebra、Zulu,但是對於瑞典用戶字母Å和字母A是不同的,它應該排在字母Z之後:able、America、zebra、Zulu、Ångstrom。
爲了獲得Locale敏感的比較符,可以調用靜態Collator.getInstance方法:

Collator coll = Collator.getInstance(Locale.getDefault());
words.sort(coll);

因爲Collator類實現了Comparator接口,因此可以傳遞一個Collator對象給list.sort(Comparator)方法來對一組字符串進行排序。
排序器有幾個高級設置項。字符間的差別可以被分爲首要的(primary)、其次的(secondary)和再次的(tertiary)。比如英語中,A和Z之間的差別是首要的,A和Å之間差別是其次的,A和a之間是再次的。
如果排序器的強度設置成Collator.PRIMARY,那麼排序器將只關注primary級的差別。如果設置成Collator.SECONDARY,排序器將把secondary級的差別也考慮進去。
在這裏插入圖片描述
如果強度被設置爲Collator.IDENTICAL,則不允許有任何差異。這種設置在與排序器的第二種具有相當技術性的設置,即分解模式(decomposition mode),聯合使用時,就會非常有用。
Å可以是Unicode字符U+00C5,或者表示成普通的A(U+0065)後跟°(上方組合環,U+030A)。Unicode標準對字符串定義了四種範化形式(normalization form)😄、KD、C和KC。在範化形式C中,重音符號總是組合的。在範化形式D中,重音字符被分解爲基字符和組合重音符。
可以選擇排序器所使用的範化程度:Collator.NO_DECOMPOSITION表示不對字符串做任何範化,這個選項處理較快,但是對於多形式的文本顯得不適用。默認值Collator.CANONICAL_DECOMPOSITION使用範化形式D,這對於包含重音但不包含連字的文本是非常有用的形式;最後是使用範化形式KD的“完全分解”。
在這裏插入圖片描述
讓排序器去多次分解一個字符串是很浪費的。如果一個字符串要和其他字符串進行多次比較,可以將分解結果保存在一個排序鍵對象中。getCollationKey方法返回一個CollationKey對象,可以用它來進行更進一步的、更快捷的比較操作。

String a = ...;
CollationKey aKey = coll.getCollationKey(a);
if (aKey.compareTo(coll.getCollationKey(b)) == 0) //快速比較

最後,可能不需要排序,但也希望將字符串轉成它們的範化形式:

String name = "Ångstrom";
String normalized = Normalizer.normalize(name, Normalizer.Form.NFD);

範化後包含10個字符,其中Å替換成了A°。
但這通常並非用於存儲或傳輸的最佳形式。範化形式C首先進行分解,然後將重音按照標準化的順序組合。根據W3C標準,這是用於互聯網上進行數據傳輸的推薦模型。

public class CollationTest {
    public static void main(String[] args) {
        JFrame frame = new CollationFrame();
        frame.setTitle("CollationTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}
class CollationFrame extends JFrame {
    private Collator collator = Collator.getInstance(Locale.getDefault());
    private List<String> strings = new ArrayList<>();
    private Collator currentCollator;
    private Locale[] locales;
    private JComboBox<String> localeCombo = new JComboBox<>();
    private JTextField newWords = new JTextField(20);
    private JTextArea sortedWords = new JTextArea(20, 20);
    private JButton addButton = new JButton("Add");
    private EnumCombo<Integer> strengthCombo = new EnumCombo<Integer>(Collator.class, "Primary", "Secondary", "Tertiary", "Identical");
    private EnumCombo<Integer> decompositionCombo = new EnumCombo<Integer>(Collator.class, "Canonical Decomposition", "Full Decomposition", "No Decomposition");

    public CollationFrame() {
        setLayout(new GridBagLayout());
        add(new JLabel("Locale"), new GBC(0, 0).setAnchor(GBC.EAST));
        add(new JLabel("Strength"), new GBC(0, 1).setAnchor(GBC.EAST));
        add(new JLabel("Decomposition"), new GBC(0, 2).setAnchor(GBC.EAST));
        add(addButton, new GBC(0, 3).setAnchor(GBC.EAST));
        add(localeCombo, new GBC(1, 0).setAnchor(GBC.WEST));
        add(strengthCombo, new GBC(1, 1).setAnchor(GBC.WEST));
        add(decompositionCombo, new GBC(1, 2).setAnchor(GBC.WEST));
        add(newWords, new GBC(1, 3).setFill(GBC.HORIZONTAL));
        add(new JScrollPane(sortedWords), new GBC(0, 4, 2, 1).setFill(GBC.BOTH));

        locales = Collator.getAvailableLocales().clone();
        Arrays.sort(locales, (l1, l2) -> collator.compare(l1.getDisplayName(), l2.getDisplayName()));
        for (Locale loc : locales) {
            localeCombo.addItem(loc.getDisplayName());
        }
        localeCombo.setSelectedItem(Locale.getDefault().getDisplayName());

        strings.add("America");
        strings.add("able");
        strings.add("Zulu");
        strings.add("zebra");
        strings.add("\u00C5ngstr\u00F6m");
        strings.add("A\u030angstro\u0308m");
        strings.add("Angstrom");
        strings.add("Able");
        strings.add("office");
        strings.add("o\uFB03ce");
        strings.add("Java\u2122");
        strings.add("JavaTM");
        updateDisplay();

        addButton.addActionListener(event -> {
            strings.add(newWords.getText());
            updateDisplay();
        });

        ActionListener listener = event -> updateDisplay();
        localeCombo.addActionListener(listener);
        strengthCombo.addActionListener(listener);
        decompositionCombo.addActionListener(listener);
        pack();
    }

    public void updateDisplay() {
        Locale currentLocale = locales[localeCombo.getSelectedIndex()];
        localeCombo.setLocale(currentLocale);

        currentCollator = Collator.getInstance(currentLocale);
        currentCollator.setStrength(strengthCombo.getValue());
        currentCollator.setDecomposition(decompositionCombo.getValue());

        Collections.sort(strings, currentCollator);

        sortedWords.setText("");
        for (int i = 0; i < strings.size(); i++) {
            String s = strings.get(i);
            if (i > 0 && currentCollator.compare(s, strings.get(i -1)) == 0) {
                sortedWords.append("= ");
            }
            sortedWords.append(s + "\n");
        }
        pack();
    }
}

在這裏插入圖片描述
=號表示這兩個詞被認爲是等同的。
在組合框中的locale名的顯示順序,使用默認locale的排序器進行排序而產生的順序的。如果用美國英語locale運行這個程序,即使逗號的Unicode值比右括號的Unicode值大,“Norwegian(Norway,Nynorsk)”也會顯示在“Norwegian(Norway)”前面。

6.消息格式化

格式化數字和日期

下面是一個典型的消息格式化字符串,括號中的數字是佔位符,可以用實際的名字和值來替換它們。使用靜態方法MessageFormat.format可以用實際的值來替換這些佔位符:

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

假如不想需要“上午12:00”,而且將造成的損失量打印出貨幣值,通過佔位符可以提供可選格式:

On {2,date,long}, a {0} destroyed {1} houses and caused {3,number,currency} of damage.
//On 1999年1月1日, a hurricane destroyed 99 houses and caused ¥1,000,000,000.00 of damage.

一般佔位符索引後面可以跟一個類型(type)和一個風格(style),它們之間用逗號隔開,類型可以是:number、time、date、choice。
類型是number,風格可以是integer、currency、percent或者是可以數字格式模式,就像$,##0。
如果類型是time或date,風格可以是:short、medium、long、full,或者是一個日期格式模式,就像yyyy-MM-dd。
靜態的MessageFormat.format方法使用當前的locale對值進行格式化。要想用任意locale進行格式化:

MessageFormat mf = new MessageFormat("On {2,date,long}, a {0} destroyed {1} houses and caused {3,number,currency} of damage.", Locale.US);
String s = mf.format(new Object[]{"hurricane", 99, new GregorianCalendar(1999, 0, 1).getTime(), 10.0E8});

選擇格式

a {0} destroyed…,如果用“earthquake”來替換代表災難的佔位符{0},該語法就不正確。
或者{0} destroyed,就應該用“a hurricane”或“an earthquake”來替換{0}。
但是{1} houses的替換值可能是數字1,消息就會變成 1 houses,希望消息更夠隨佔位符的值而變化,這樣就會根據具體的值形成:
no houses, one house, 2 houses
choice格式化選項就是爲了這個目的而設計的。
一個選擇格式由一個序列對構成的,每一個對包括:
1.一個下限(lower limit)
2.一個格式字符串(format string)
下限和格式字符串由一個#符號分隔,對與對之間由符號|分隔。
例如,{1,choice,0#no houses|1#one house|2#{1} houses}
在這裏插入圖片描述
當消息格式將選擇的格式應用於佔位符{1}而且替換值是2時,那麼選擇格式會返回“{1} houses”。
可以使用<符號來表示如果替換值嚴格小於下限,則選中這個選擇項。
也可以使用≤(unicode中的代碼是\u2264)來實現和#相同的效果。如果願意的話,甚至可以將第一個下限的值定義爲- ∞(unicode代碼是- \u221E)。

-<no houses|0<one house|2{1} houses
// 或者使用Unicode轉義字符
-\u221E<no houses|0<one house|2\u2264{1} houses

7.文本文件和字符集

文本文件

如今最好是使用UTF-8來存儲和加載文本文件,但是需要操作遺留文件,就要知道遺留文件的字符編碼機制:

PrintWriter out = new PrintWriter(filename, "Windows-1252");
// 調用下面方法可以獲得最佳的編碼機制(平臺的編碼機制)
Charset platformEncoding = Charset.defaultCharset();

行結束符

Windows中每行末尾是\r\n,而UNIX只需要一個\n字符。大多數Windows程序都可以處理一個\n的情況,一個重要例外是記事本。
任何用println方法寫入的行都會被正確終止的。唯一的問題是是否打印了包含\n字符的行,它們不會被自動修改爲平臺的行結束符。與在字符串中使用\n不同,可以使用printf和%n格式說明符來產生平臺相關的行結束符:

System.out.printf("Hello%nWorld%n");
// Windows產生Hello\r\nWorld\r\n
// 其他平臺產生Hello\nWorld\n
// Hello
// World

控制檯

如果通過System.in/System.out或System.console()與用戶交互,那麼就不得不對控制檯使用字符編碼機制與CharSet.defaultCharset()報告的平臺編碼機制有所差異的可能性。
Windows的美國版本的命令行Shell使用的是陳舊的IBM437編碼機制。Charset.defaultCharset()方法將返回Windows-1252字符集,它與IBM437完全不同。在Windows-1252中有歐元符號,但是在IBM437中沒有:

System.out.println("100€");
// 100?

Windows中可以通過chcp命令切換控制檯字符的編碼機制:

# 切換爲Windos-1252編碼頁
chcp 1252
# 切換爲UTF-8
chcp 65001

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

java -Dfile.encoding=UTF-8 MyProg

日誌文件

java.util.logging庫的日誌消息,會使用控制檯編碼機制書寫(上一節)。
但文件中的日誌消息會使用FileHandler來處理,它在默認情況下會使用平臺編碼機制。
修改日誌管理器的設置:

java.util.logging.FileHandler.encoding=UTF-8

UTF-8字節順序標誌

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文件才能隨處使用。因爲它們會自動使用“modifiedUTF-8”編碼來處理標識符和字符串。這意味着即使在程序編譯和運行時,依然有3中字符編碼參與其中:
1.源文件:本地編碼
2.類文件:modified UTF-8
3.虛擬機:UTF-16
可以用-encoding標記來設定源文件的字符編碼:

javac -encoding UTF-8 Myfile.java

爲了使源文件到處使用,必須使用普遍的ASCII編碼。就是說,需要將所有非ASCII字符轉換成等價的Unicode編碼。比如,不要使用字符“Häuser”,應該使用“H\u0084user”。JDK包含了一個native2ascii,可以用它將本地字符編碼轉換成普通的ASCII。這個工具直接將每一個非ASCII字符替換爲一個\u加四位十六進制的Unicode值。使用native2ascii時,需要提供輸入和輸出文件的名字:

native2ascii Myfile.java Myfile.temp

可以使用-reverse進行逆向轉換:

native2ascii -reverse Myfile.temp Myfile.java

可以使用-encoding選項知道另一種編碼:

native2ascii -encoding UTF-8 Myfile.java Myfile.temp

8.資源包

一個本地化程序,會有大量的消息字符串、按鈕標籤和其他的需要被翻譯,爲了能夠靈活地完成這項任務,會希望在外部定義消息字符串,通常稱之爲資源(resource)。翻譯人員不需要接觸程序源代碼就可以編輯資源文件。
在Java中,使用屬性文件來設定字符串資源,併爲其他類型的資源實現相應的類。

定位資源包

本地化一個應用時,會產生很多資源包(resource bundle)。
統一的命名規則,使用 包名_語言_國家(包名_de_DE) 來命名所有和國家相關的資源。
使用 包名_語言(包名_de) 來命名所有和國家相關的資源。作爲後備,可以把默認資源放在一個沒有後綴的文件中。

可以使用下面代碼加載一個包:

ResourceBundle currentResources = ResourceBundle.getBundle(bundleName, currentLocale);

getBundle方法試圖加載匹配當前locale定義的語言和國家的包。如果失敗,通過依次放棄國家和語言來繼續查找,然後同樣的查找被應用於默認的locale,最後,如果還不行就去查看默認的包文件,如果失敗,就拋出一個MissingResourceException。
一旦getBundle方法定位了一個包。比如,包名_de_DE,它還會繼續查找 包名_de 和 包名 這兩個包。如果這些包也存在,它們在資源層次中就成爲了 包名_de_DE的父包。以後要查找一個資源時,如果當錢包沒有,就會去查找父包。

屬性文件

MyProgramStrings.properties,每行存放一個鍵-值對的文本文件:

computeButton=Rechnen
colorName=black
defaultPaperSize=210×297

然後:
MyProgramStrings.properties
MyProgramStrings_en.properties
MyProgramStrings_ch.properties
然後直接加載包:

ResourceBundle currentResources = ResourceBundle.getBundle("MyProgramStrings", Locale.getDefault());
// 查找具體字符串字符串
String conputeButtonLabel = currentResources.getString("computeButton");

存儲屬性的文件都是ASCII文件。如果需要將Unicode字符放到屬性文件中,那麼請用\uxxxx編碼方式進行編碼。比如“colorName=Grün”,可以使用“colorName=Gr\uooFCn”。可以使用native2ascii工具來產生這些文件。

包類

爲了提供字符串以外的資源,需要定義類,它必須擴展自ResourceBundle類。應該使用標準的命名規則來命名類:
MyProgramResources.java
MyProgramResources_en.java
MyProgramResources_ch.java
可以使用與加載屬性文件相同的getBundle方法來加載這個類:

ResourceBundle bundle = ResourceBundle.getBundle("MyProgramResources", Locale.getDefault());

每一個資源包類都實現一個查詢表,需要爲每一個想定位的設置都提供一個關鍵字字符串,使用這個字符串來提取相應的設置:

Color backgroudColor = (Color) bundle.getObject("backgroundColor");
double[] paperSize = (double[]) bundle.getObject("defaultPaperSize");

實現資源包類的最簡單方法是繼承ListResourceBundle類。ListResourceBundle讓你把所有資源都放到一個對象數組中並提供查找功能:
bunleName_language_country.java

public class MyProgramResources_de extends ListResourceBundle {
    private static final Object[][] contents = {
            {"backgroundColor", Color.black},
            {"defaultPaperSize", new double[]{210, 297}}
    };
    @Override
    protected Object[][] getContents() {
        return contents;
    }
}
public class MyProgramResources_en_US extends ListResourceBundle {
    private static final Object[][] contents = {
            {"backgroundColor", Color.blue},
            {"defaultPaperSize", new double[]{216, 279}}
    };
    @Override
    protected Object[][] getContents() {
        return contents;
    }
}

或者資源類包擴展ResourceBundle類。然後需要實現兩個方法,一個是枚舉所有鍵,二是用給定的鍵查找相應的值:
Object handleGetObject(String key)
Enumeration< String > getKeys()
ResourceBundle類的getObjecy方法會調用handleGetObject方法。

發佈了233 篇原創文章 · 獲贊 22 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章