【Java】アプリケーションの多言語化

今回はアプリケーションの多言語化を考えたときに使用できる関連クラスについての動作をみていきます。

特定の国、地域、言語などを表すLocaleクラス

Localeクラスは特定の国、地域、言語などを表すためのクラスです。

Localeクラスは国などの情報を表すstaticクラスオブジェクトが多数定義されています。

例えば日本であればLocale.JAPANでアクセス可能です。

試しに国の情報をいくつか表示してみましょう。

import java.util.Locale;

public class Test {
	
	public static void main(String[] args) {
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale china = Locale.CHINA;

		//デフォルトロケールで表示
		System.out.println(japan.getDisplayName());
		System.out.println(us.getDisplayName());
		System.out.println(china.getDisplayName());
		
		//ENGLISHで表示
		System.out.println(japan.getDisplayName(Locale.ENGLISH));
		System.out.println(us.getDisplayName(Locale.ENGLISH));
		System.out.println(china.getDisplayName(Locale.ENGLISH));
	}
}
実行結果
日本語 (日本)
英語 (アメリカ合衆国)
中国語 (中国)
Japanese (Japan)
English (United States)
Chinese (China)

Localeクラスは他のクラスと組み合わせることによって特定の国や言語に合った処理を行うことができます。

通貨を表現する

通貨を表すCurrencyというクラスがあります。

Localeを元にCurrencyオブジェクトを構築できます。

import java.util.Currency;
import java.util.Locale;

public class Test {
	
	public static void main(String[] args) {
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale china = Locale.CHINA;

		//通貨の取得
		Currency jpy = Currency.getInstance(japan);
		Currency dollar = Currency.getInstance(us);
		Currency chy = Currency.getInstance(china);

		//デフォルトロケールで表示
		System.out.println(jpy.getDisplayName());
		System.out.println(dollar.getDisplayName());
		System.out.println(chy.getDisplayName());
		
		//ENGLISHで表示
		System.out.println(jpy.getDisplayName(Locale.ROOT));
		System.out.println(dollar.getDisplayName(Locale.ENGLISH));
		System.out.println(chy.getDisplayName(Locale.ENGLISH));
	}
}
実行結果
日本円
米ドル
中国人民元
Japanese Yen
US Dollar
Chinese Yuan

さらにNumberFormatクラスを利用すると数値を国ごとの通貨表示に変更することもできます。

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

public class Test {
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale france = Locale.FRANCE;
		
		int money = 15000;
		
		//日本通貨で表示
		NumberFormat jpy = NumberFormat.getCurrencyInstance(japan);
		System.out.println(jpy.format(money));
		
		//米国通貨で表示
		NumberFormat dollar = NumberFormat.getCurrencyInstance(us);
		System.out.println(dollar.format(money));
		
		//フランス通貨で表示
		NumberFormat fr = NumberFormat.getCurrencyInstance(france);
		System.out.println(fr.format(money));
	}
}
実行結果
¥15,000
$15,000.00
15 000,00 €

日付、時間を表現する

日付時間の表示は国ごとに違ったりします。DateFormatで表示の違いをみることができます。

import java.text.DateFormat;
import java.text.ParseException;
import java.time.Instant;
import java.util.Locale;
 
public class Test {
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale italy = Locale.ITALY;
		
		long epochTime = Instant.EPOCH.toEpochMilli();
		
		//日本日時を表示
		DateFormat jpdt = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,DateFormat.DEFAULT,japan);
		System.out.println(jpdt.format(epochTime));
		
		//米国日時を表示
		DateFormat usdt = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,DateFormat.DEFAULT,us);
		System.out.println(usdt.format(epochTime));
		
		//イタリア日時を表示
		DateFormat itdt = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,DateFormat.DEFAULT,italy);
		System.out.println(itdt.format(epochTime));
	}
}
実行結果
1970/01/01 9:00:00
Jan 1, 1970, 9:00:00 AM
1 gen 1970, 09:00:00

グローバルなアプリケーションはタイムゾーンを考慮しなければならないこともあります。

タイムゾーンと国ごとの表示を考慮しての表示も試してみましょうか。

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

public class Test {
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale china = Locale.CHINA;

		//各地域のタイムゾーン取得
		TimeZone jpzn = TimeZone.getTimeZone("Asia/Tokyo");
		TimeZone uszn = TimeZone.getTimeZone("America/New_York");
		TimeZone chzn = TimeZone.getTimeZone("Asia/Shanghai");
		
		//現在時刻を取得
		Date date = new Date();
		
		//日本日時を表示
		DateFormat jpdt = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL,japan);
		jpdt.setTimeZone(jpzn);
		System.out.println(jpdt.format(date));
		
		//米国日時を表示
		DateFormat usdt = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL,us);
		usdt.setTimeZone(uszn);
		System.out.println(usdt.format(date));
		
		//中国日時を表示
		DateFormat chdt = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL,china);
		chdt.setTimeZone(chzn);
		System.out.println(chdt.format(date));
	}
}
実行結果
2022年6月17日金曜日 22時35分12秒 日本標準時
Friday, June 17, 2022 at 9:35:12 AM Eastern Daylight Time
2022年6月17日星期五 中国标准时间 下午9:35:12

それぞれの国の現在時刻が表示されたはずです。

文字列を表現する

言語を表現するパターンを考えてみます。

マップにそれぞれのテキストテンプレートを保持しておいて、MessageFormatクラスで引数の処理を行うという形にしてみました。

import java.text.MessageFormat;
import java.text.ParseException;
import java.util.EnumMap;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Stream;

public class Test {
	
	enum TextKey{
		TITLE,MSG;
	}
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale ja = Locale.JAPANESE;
		Locale en = Locale.ENGLISH;
		Locale zh = Locale.CHINESE;
		
		//各言語のテキストテンプレート
		Map<Locale, Map<TextKey, String>> mapMsg = new HashMap<>() {
			{
				{
					//日本語テキスト
					Map<TextKey, String> tx = new EnumMap<>(TextKey.class);
					tx.put(TextKey.TITLE, "車競争");
					tx.put(TextKey.MSG, "バージョン {0,number} {1,date} {1,time}");
					this.put(ja, tx);
				}
				{
					//英語テキスト
					Map<TextKey, String> tx = new EnumMap<>(TextKey.class);
					tx.put(TextKey.TITLE, "CarRace");
					tx.put(TextKey.MSG, "Version {0,number} {1,date} {1,time}");
					this.put(en, tx);
				}
				{
					//中国語テキスト
					Map<TextKey, String> tx = new EnumMap<>(TextKey.class);
					tx.put(TextKey.TITLE, "賽車");
					tx.put(TextKey.MSG, "版本 {0,number} {1,date} {1,time}");
					this.put(zh, tx);
				}
			}
		};
		
		//バージョン
		double version = 1.2;
		
		//日時
		long millis = new GregorianCalendar(2000, 11, 2, 11, 55, 34).getTimeInMillis();
		
		//各種言語に合わせて内容を表示する
		Stream.of(ja,en,zh).forEach(e -> {
			System.out.println(e.getDisplayLanguage());
			System.out.println(mapMsg.get(e).get(TextKey.TITLE));
			MessageFormat fmt = new MessageFormat(mapMsg.get(e).get(TextKey.MSG),e);
			System.out.println(fmt.format(new Object[] {version, millis}));
			System.out.println();
		});
	}
}
実行結果
日本語
車競争
バージョン 1.2 2000/12/02 11:55:34

英語
CarRace
Version 1.2 Dec 2, 2000 11:55:34 AM

中国語
賽車
版本 1.2 2000年12月2日 上午11:55:34

言語ごとに用意されたリソースを使用する

アプリケーションを作成する時はリソースファイルなどを読み出すようなパターンもあります。

例えばゲームなら画像などを読み込んで表示させることがあるはずです。タイトルロゴが日本語と英語で変わってるパターンはよくありますよね。

そういう時に使えそうなResourceBundleクラスの使い方を見ていきましょう。

ResourceBundleは抽象クラスであり、継承して使用します。それぞれ継承したクラスのクラス名などにより自動でそれぞれが判別されて読み込まれるようになります。

handleGetObjectメソッドをオーバーライドし、指定キーに紐付いたリソースを返すように実装するだけ。getKeysはキー一覧を返すように実装します。

また、利用しやすくするため、既にResourceBundleクラスを実装したListResourceBundleやPropertyResourceBundleも存在します。

クラスが準備出来たらResourceBundle.getBundleメソッドでインスタンス化できます。getBundleで指定されたキーを元に、自動でクラスがロードされます。ロードされるのはキー+「_言語」に一致するクラスやpropertiesファイルです。読み込みルールに関してはAPIを参照するのが良いでしょう。

それでは、これらのクラスを利用して実際の動作を確認してみましょうか。

キーの値をTextTempとして実装してみます。構成は次の通りです。

日本語用のはListResourceBundleを利用して実装してみました。

resource/TextTemp_ja.java
package resource;

import java.util.ListResourceBundle;

//日本語用のクラス
public class TextTemp_ja extends ListResourceBundle{

	@Override
	protected Object[][] getContents() {
		// TODO 自動生成されたメソッド・スタブ
		return new String[][] {
			 {"title","車競争"}
		};
	}
}

英語用のはPropertyResourceBundleを利用するためのプロパティファイルを用意します。

英語はデフォルト実装にしたいので、TextTemp_enではなくTextTempの名前そのまんまにしてみました。

resource/TextTemp.properties
title=CarRace

中国用のはResourceBundleを直接実装した例です。getKeysは面倒だったので実装していません。

resource/TextTemp_zh.java
package resource;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;

//中国用クラス
public class TextTemp_zh extends ResourceBundle {
	
	Map<String, String> map = new HashMap<>() {
		{
			this.put("title", "賽車");
		}
	};

	@Override
	protected Object handleGetObject(String key) {
		return map.get(key);
	}

	@Override
	public Enumeration<String> getKeys() {
		return null;
	}
}

これらのクラスを実際に利用してみます。

Test.java
import java.text.ParseException;
import java.util.Locale;
import java.util.ResourceBundle;
 
public class Test {
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale china = Locale.CHINA;

		//指定ロケールでリソースバンドル取得
		ResourceBundle bundleJp = ResourceBundle.getBundle("resource.TextTemp", japan);
		ResourceBundle bundleUs = ResourceBundle.getBundle("resource.TextTemp", us);
		ResourceBundle bundleIt = ResourceBundle.getBundle("resource.TextTemp", china);
		
		//各国の適用言語のタイトル表示
		System.out.println("日本:"+bundleJp.getString("title")+"/クラス名="+bundleJp.getClass().getName());
		System.out.println("米国:"+bundleUs.getString("title")+"/クラス名="+bundleUs.getClass().getName());
		System.out.println("中国:"+bundleIt.getString("title")+"/クラス名="+bundleIt.getClass().getName());
	}
}
実行結果
日本:車競争/クラス名=resource.TextTemp_ja
米国:車競争/クラス名=resource.TextTemp_ja
中国:賽車/クラス名=resource.TextTemp_zh

なぜかデフォルトに設定した英語が日本語になっていますね。

ResourceBundleは読み込む優先順位が設定されており、該当データが見つからない場合はデフォルトロケールのデータを参照します。

そしてデフォルトロケールで見つからない場合にデフォルトのTextTempを検索します。

今回はデフォルトロケールが日本の状態で実行したので、この結果になりました。(※別の国だとデフォルトロケールが違うはずなので違う結果になってるはず。)

問題を解決するにはデフォルトロケールを日本ではなく米国にするかResourceBundle.ControlクラスのgetNoFallbackControl​を利用し、検索処理の規定を変える必要があります。

getNoFallbackControlを利用した解決方法を次に示します。

Test.java
import java.text.ParseException;
import java.util.Locale;
import java.util.ResourceBundle;
 
public class Test {
	
	public static void main(String[] args) throws ParseException {
		
		//国を取得
		Locale japan = Locale.JAPAN;
		Locale us = Locale.US;
		Locale china = Locale.CHINA;

		//指定ロケールでリソースバンドル取得
		ResourceBundle.Control ctrl = ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT);
		ResourceBundle bundleJp = ResourceBundle.getBundle("resource.TextTemp", japan, ctrl);
		ResourceBundle bundleUs = ResourceBundle.getBundle("resource.TextTemp", us, ctrl);
		ResourceBundle bundleIt = ResourceBundle.getBundle("resource.TextTemp", china, ctrl);
		
		//各国の適用言語のタイトル表示
		System.out.println("日本:"+bundleJp.getString("title")+"/クラス名="+bundleJp.getClass().getName());
		System.out.println("米国:"+bundleUs.getString("title")+"/クラス名="+bundleUs.getClass().getName());
		System.out.println("中国:"+bundleIt.getString("title")+"/クラス名="+bundleIt.getClass().getName());
	}
}
実行結果
日本:車競争/クラス名=resource.TextTemp_ja
米国:CarRace/クラス名=java.util.PropertyResourceBundle
中国:賽車/クラス名=resource.TextTemp_zh

狙い通りのクラスがロードされました。

日本と中国はctrlインスタンスを渡す必要はなかったですが、実際使うときは固定で行うことがほとんどだと思ったので、あえてすべて設定しています。

Java

Posted by nompor