چند زبانی در JavaFX

در این آموزش با چند زبانی در JavaFX آشنا می شویم ، متاسفانه چند زبانی هنوز آنطور که باید و شاید در JavaFX پشتیبانی نمی شود ولی با این حال روش های مختلفی برای پیاده سازی برنامه هایی که دارای چند زبان هستند وجود دارد ، یکی از متداول ترین روش ها استفاده از فایل هایی با پسوند properties. است که از طریق ResourceBundle قابل دستیابی هستند.

به صورت پیشفرض فایل های properties. در جاوا باید از استاندارد ISO-8859-1 استفاده کنند ، اگر در IDE هایی همچون Eclipse فایلی با این پسوند ایجاد کنید متوجه می شوید که هنگام تایپ فارسی عبارات به صورت uxxxx\ نوشته می شوند که خواندن و ویرایش چنین فایل هایی بسیار دشوار است. به عنوان برای شما بسیار سخت خواهد بود تا معنی عبارت زیر را بفهمید یا تغییر دهید :


lbl2.text=\u0633\u06CC\u0641 \u0627\u0644\u0644\u0647 

لذا ترجیح می دهیم هنگام کار با زبان فارسی ، کد گذاری فایل های properties. را به UTF-8 تغییر دهیم تا خواندن و ویرایش آن دشوار شود. این تغییر باعث می شود عبارات فارسی به درستی در برنامه نمایش داده نشوند که برای حل این مشکل چند راه حل وجود دارد :

  1. یک زیر کلاس از ResourceBundle ایجاد کنیم و در آن رشته های ISO-8859-1 را به UTF تبدیل کنیم.(ادقت کنید که ResourceBundle رشته ها را به صورت ISO-8859-1 در نظر می گیرد ،ین روش در انتهای این آموزش بررسی می شود)
  2. ابتدا فایل های مورد نظر را به صورت UTF-8 بسازیم سپس آن را با ابزار های خاصی به ISO-8859-1 کنیم.
  3. فایل های مورد نظر را به صورت UTF-8 ذخیره کنیم و در داخل برنامه خودمان رشته های خوانده شده از ResourceBundle را از ISO-8859-1 به UTF-8 تبدیل کنیم.

در ادامه بهتر با مطالب فوق آشنا می شوید.

ساخت برنامه :

برای ساخت برنامه دو روش وجود دارد :

  • استفاده از کد های جاوا
  • استفاده از FXML 

در این آموزش هر دو روش را بررسی می کنیم.

 

ساخت برنامه با کد های جاوا :

ابتدا یک پروژه ساده جاوایی ایجاد می کنیم.

میخواهیم برنامه ای طراحی کنیم که دارای دو Label و یک Button است که نوشته های بر اساس تغییر زبان تغییر می کنند.(البته دو Button هم برای تغییر زبان خواهیم داشت). همچنین می خواهیم عنوان برنامه نیز چند زبان باشد.

فایل جدیدی در داخل پروژه (در کنار کد های برنامه) به نام mystrings_en.properties ایجاد می کنیم ، این فایل حاوی کلید های مورد نظر و رشته های متناظر با آن ها در زبان انگلیسی خواهد بود.

در داخل این فایل عبارات زیر را می نویسیم :


app.title=Title of Application!
lbl1.text=What is your name?
lbl2.text=Book
btn1.text=Hello


فایل دیگری به نام mystrings_fa.properties ایجاد می کنیم و محتوی زیر را در آن می نویسیم و آن را به صورت UTF-8 ذخیره می کنیم(می توانید از ادیتور هایی مثل ++Notepad استفاده کنید)


app.title=عنوان برنامه
lbl1.text=اسم شما چیه؟
lbl2.text=کتاب
btn1.text=سلام


یک کلاس جدید به نام JavaFXMultilingualExample ایجاد می کنیم و کد زیر را در آن می نویسیم :


import java.io.UnsupportedEncodingException;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class JavaFXMultilingualExample extends Application {

	Label lbl1;
	Label lbl2;
	Button btn1;

	Button btnFarsi;
	Button btnEnglish;

	ResourceBundle enBundle;
	ResourceBundle faBundle;

	Stage stage;

	@Override
	public void start(Stage primaryStage) throws Exception {
		stage = primaryStage;
		VBox vb = new VBox();

		lbl1 = new Label();
		lbl2 = new Label();
		btn1 = new Button();

		btnEnglish = new Button("English");
		btnFarsi = new Button("فارسی");

		Locale enLocale = new Locale("en", "US");
		Locale faLocale = new Locale("fa", "IR");

		enBundle = ResourceBundle.getBundle("mystrings", enLocale);
		faBundle = ResourceBundle.getBundle("mystrings", faLocale);

		btnEnglish.setOnAction(new EventHandler<ActionEvent>() {

			@Override
			public void handle(ActionEvent arg0) {
				setText(enBundle);
			}
		});
		btnFarsi.setOnAction(new EventHandler<ActionEvent>() {

			@Override
			public void handle(ActionEvent arg0) {
				setText(faBundle);
			}
		});

		vb.getChildren().addAll(lbl1, lbl2, btn1, btnEnglish, btnFarsi);
		vb.setPadding(new Insets(10));
		vb.setSpacing(8);
		vb.setAlignment(Pos.TOP_CENTER);

		Scene scene = new Scene(vb, 300, 200);
		primaryStage.setScene(scene);
		setText(enBundle);
		primaryStage.show();
	}

	void setText(ResourceBundle bundle) {

		stage.setTitle(getUTF(bundle.getString("app.title")));
		lbl1.setText(getUTF(bundle.getString("lbl1.text")));
		lbl2.setText(getUTF(bundle.getString("lbl2.text")));
		btn1.setText(getUTF(bundle.getString("btn1.text")));

	}

	public String getUTF(String s) {
		try {
			return new String(s.getBytes("ISO-8859-1"), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	public static void main(String[] args) {
		Application.launch(args);
	}
}


در این برنامه دو ResourceBundle داریم یکی enBundle برای مدیریت رشته های انگلیسی و دیگری faBundle برای مدیریت رشته های فارسی ، هنگامی که ResourceBundle مورد نظر را ایجاد می کنیم فقط رشته mystrings را به متد getBundle ارسال می کنیم ، این متد بر اساس پارامتر دوم (یعنی نوع Local) تشخیص می دهد که باید از چه فایلی استفاده کند.

همانطور که در بحث ابتدای آموزش گفتم رشته هایی که از ResourceBundle خوانده می شود (با استفاده از متد getString) دارای استاندارد ISO-8859-1 هستند ولی ما فایل های مورد نظرمان را برای سادگی در کار به صورت UTF-8 ذخیره کردیم ، متد getUTF رشته های مورد نظر را به UTF-8 تبدیل می کند و می توانیم به درستی آن ها را بر روی Label ها و ... نمایش دهیم.

برنامه را اجرا می کنیم ، به صورت پیش فرض عبارات انگلیسی نمایش داده می شوند.

روی دکمه فارسی کلیک می کنیم :

نکته : اگر از همان ابتدا پس از ساخت فایل های properties. آن ها را به صورت ISO-8859-1 ذخیره می کردیم نیازی به تعریف متد getUTF نداشتیم ولی خواندن و ویرایش فایل های UTF-8 بسیار ساده تر است.

 

ساخت برنامه با FXML :

اگر از FXML برای طراحی ظاهر برنامه استفاده کنیم ، دو راه برای ایجاد چند زبانی خواهیم داشت :

  1. روش اول مشابه همان روش قبلی است و عملیات در Controller اجرا می شود.
  2. روش دوم مستقیماً از ResourceBundle استفاده می کند که در این روش حتماً باید فایل های properties. به صورت ISO-8859-1 باشند.(و یا ResourceBundle اختصاصی خودمان را بسازیم که بر اساس UTF-8 عمل می کند).

ابتدا روش اول را با هم بررسی می کنیم.

در کنار کد های برنامه فایل جدیدی به نام FXMLMultilingual.fxml ایجاد می کنیم و کد زیر را در آن می نویسیم :


<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.*?>
<?import javafx.stage.Stage?>
<Stage fx:id="stage" title="some title" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FXMLMultilingualController">
    <scene>
        <Scene>
            <VBox id="AnchorPane" spacing="10" prefHeight="200" prefWidth="300" alignment="top_center">
                <padding>
                    <Insets top="8" right="8" bottom="8" left="8" />
                </padding>
                <children>
                    <Label fx:id="lbl1" />
                    <Label fx:id="lbl2" />
                    <Button fx:id="btn1" />
                    <Button text="English" onAction="#toEnglish" fx:id="btnEnglish" />
                    <Button text="فارسی" onAction="#toFarsi" fx:id="btnFarsi" />
                </children>
            </VBox>
        </Scene>
    </scene>
</Stage>

کد فوق بسیار ساده و خواناست و در حقیقت همان طراحی برنامه قبلی است که به شکل xml در آمده است ، تنها نکته ای که در اینجا وجود دارد این است که در هنگام استفاده از FXML انتخاب با شماست که Stage را در فایل fxml. قرار دهید یا در فایل اجرا کننده آن را اجرا کنید.

همانطور که در کد بالا دقت می کنید نام کنترلر باید FXMLMultilingualController باشد ، پس کلاس جدیدی با این نام در فایل جدیدی به نام FXMLMultilingualController.java ایجاد می کنیم و کد زیر را در آن می نویسیم :


import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class FXMLMultilingualController implements Initializable {
	ResourceBundle enBundle;
	ResourceBundle faBundle;

	@FXML
	private Stage stage;

	@FXML
	private Label lbl1;

	@FXML
	private Label lbl2;

	@FXML
	private Button btn1;

	@FXML
	private void toEnglish(ActionEvent event) {
		setText(enBundle);
	}

	@FXML
	private void toFarsi(ActionEvent event) {
		setText(faBundle);
	}

	void setText(ResourceBundle bundle) {

		stage.setTitle(getUTF(bundle.getString("app.title")));
		lbl1.setText(getUTF(bundle.getString("lbl1.text")));
		lbl2.setText(getUTF(bundle.getString("lbl2.text")));
		btn1.setText(getUTF(bundle.getString("btn1.text")));

	}

	public String getUTF(String s) {
		try {
			return new String(s.getBytes("ISO-8859-1"), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			return null;
		}
	}

	@Override
	public void initialize(URL url, ResourceBundle rb) {

		Locale enLocale = new Locale("en", "US");
		Locale faLocale = new Locale("fa", "IR");

		enBundle = ResourceBundle.getBundle("mystrings", enLocale);
		faBundle = ResourceBundle.getBundle("mystrings", faLocale);

		setText(enBundle);
	}

}

کنترلر فوق بسیار ساده است و توضیح اضافه ای در این قسمت نخواهیم داشت. تنها کاری که باقی مانده این است که یک کلاس اجرا کننده ایجاد کنیم تا از روی فایل FXML بالا یک برنامه جدید اجرا کند ، در کنار کد های برنامه کلاس جدیدی به نام FXMLMultilingualExample.java ایجاد می کنیم و کد زیر را در آن می نویسیم :


import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;

public class FXMLMultilingualExample extends Application {

	@Override
	public void start(Stage stage) throws Exception {
		stage = FXMLLoader.load(getClass().getResource("/FXMLMultilingual.fxml"));
		stage.show();
	}

	public static void main(String[] args) {
		launch(args);
	}

}


FXMLMultilingualExample را اجرا می کنیم :

روی دکمه فارسی کلیک می کنیم :

مشاهده می کنید همه چیز مثل حالت قبل است با این تفاوت که این بار برنامه را با FXML طراحی کردیم.

 

روش دوم :

روش دوم کمی متفاوت است ، ابتدا فایل FXMLMultilingual.fxml را به صورت زیر تغییر می دهیم :


<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.*?>
<?import javafx.stage.Stage?>
<VBox id="AnchorPane" spacing="10" prefHeight="200" prefWidth="300" alignment="top_center" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FXMLMultilingualController">
    <padding>
        <Insets top="8" right="8" bottom="8" left="8" />
    </padding>
    <children>
        <Label text="%lbl1.text" fx:id="lbl1" />
        <Label text="%lbl2.text" fx:id="lbl2" />
        <Button text="%btn1.text" fx:id="btn1" />
        <Button text="English" onAction="#toEnglish" fx:id="btnEnglish" />
        <Button text="فارسی" onAction="#toFarsi" fx:id="btnFarsi" />
    </children>
</VBox>

دو تغییر اساسی مشاهده می کنید :

 یک : Stage حذف شده است ، زیرا در ادامه خواهید دید که در روش جدید هر بار UI به کلی از اول بارگذاری می شود و این اتفاق نباید برای stage بیافتد(احتمالاً کسی دوست ندارد تا موقع تغییر زبان برنامه ، برنامه بسته شده و مجدداً باز شود).

دو: کامپوننت هایی که قصد داریم چند زبانه باشند به شکلی تغییر کردند که صفت text آن ها با علامت % شروع می شود (و بعد از % کلید مورد نظر نوشته می شود) ، علامت % در تعریف FXML به JavaFX می فهماند که برای این مقدار باید از ResourceBundle استفاده کند.

فایل mystrings_fa.properties را به ISO-8859-1 تغییر می دهیم ، ساده ترین راه این است که رشته ها را مجدداً در IDE (مثلاً Eclipse) تایپ کنید.خروجی باید به شکل زیر باشد :


app.title=\u0639\u0646\u0648\u0627\u0646 \u0628\u0631\u0646\u0627\u0645\u0647
lbl1.text=\u0627\u0633\u0645 \u0634\u0645\u0627 \u0686\u06CC\u0647\u061F
lbl2.text=\u06A9\u062A\u0627\u0628
btn1.text=\u0633\u0644\u0627\u0645


کد کنترلر را به صورت زیر تغییر می دهیم  :


import java.net.URL;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;

public class FXMLMultilingualController implements Initializable {
	FXMLMultilingualExample runner;
	ResourceBundle enBundle;
	ResourceBundle faBundle;


	@FXML
	private void toEnglish(ActionEvent event) {
		runner.change(enBundle);
	}

	@FXML
	private void toFarsi(ActionEvent event) {
		runner.change(faBundle);
	}

	public void setRunner(FXMLMultilingualExample fme) {
		runner = fme;
	}

	@Override
	public void initialize(URL url, ResourceBundle rb) {

		Locale enLocale = new Locale("en", "US");
		Locale faLocale = new Locale("fa", "IR");

		enBundle = ResourceBundle.getBundle("mystrings", enLocale);
		faBundle = ResourceBundle.getBundle("mystrings", faLocale);
	}

}

مشاهده می کنید که یک متد مهم به نام setRunner اضافه شده است ، این متد اشاره گری به برنامه اجرا کننده را نگه میدارد ، بعداً با استفاده از این اشاره گر (شی runner) می توانیم متد change را فراخوانی کنیم.(این متد را در ادامه خواهیم دید).

برنامه اجرا کننده را به صورت زیر تغییر می دهیم :


import java.io.IOException;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class FXMLMultilingualExample extends Application {

	FXMLLoader fxmlLoader;
	Stage stage;
	Scene scene;

	@Override
	public void start(Stage mstage) throws Exception {

		stage = new Stage();

		change(ResourceBundle.getBundle("mystrings"));

		stage.setScene(scene);

		stage.show();

	}

	void change(ResourceBundle rb) {

		fxmlLoader = new FXMLLoader(getClass().getResource("/FXMLMultilingual.fxml"));
		fxmlLoader.setResources(rb);

		VBox vb = null;
		try {
			vb = fxmlLoader.load();
			FXMLMultilingualController fmc = (FXMLMultilingualController) fxmlLoader.getController();
			fmc.setRunner(this);
		} catch (IOException e) {
			e.printStackTrace();
		}

		stage.setTitle(rb.getString("app.title"));

		if (scene == null)
			scene = new Scene(vb);

		scene.setRoot(vb);
	}

	public static void main(String[] args) {
		launch(args);
	}

}


دقت کنید که چگونه کنترلر را به دست می آوریم و سپس برنامه اجرا کننده را به متد setRunner ارسال می کنیم ، مهم نکته این کد متد change است ، این متد هر بار به کلی UI را از اول خوانده آن را در scene قرار می دهد ، در اولین بار (یعنی زمانی که برنامه اجرا می شود) شی scene برابر با null است پس باید با یک شرط null بودن آن را بررسی کنیم و در دفعه اول آن را new کنیم.

چون Stage دیگر بخشی از FXML نیست ، مجبوریم این یک مورد را مانند روش قبل به صورت دستی تغییر دهیم.

دقت کنید که چون هر بار که متد change فراخوانی می شود fxmlLoader مجدداً load می شود برای همین باید هر بار کنترلر از نو ساخته می شود و مجبوریم هر بار متد setRunner را بر روی کنترلر جدید فراخوانی کنیم.

نکته : عیب مهم این روش این است که مصرف حافظه زیادی دارد و هر بار UI به کلی از اول بارگذاری می شود و هنگامی که برنامه پیچیده باشد زمان بر بوده و چندان جالب نیست.

نکته : روش اول (یعنی استفاده از متد setText هم از لحاظ حافظه بهینه تر است و هم سرعت بیشتری دارد)

 

ResourceBundle با پشتیبانی از UTF-8 :

اگر روش دوم در FXML را ترجیح می دهد راحت تر است به جای تبدیل فایل های properties ، یک ResourceBundle اختصاصی ایجاد کنید که رشته ها را به صورت UTF-8 بر می گرداند ، در ادامه این روش را می بینیم :

فایل mystrings_fa.properties را مجدداً به UTF-8 بر می گردانیم. (دقت کنید که در یک ویرایشگر مثل ++Notepad باید بتوانید شکل درست فارسی عبارات را ببینید).

کلاس جدیدی به نام MyUTF8ResourceBundle ایجاد می کنیم ، در داخل این فایل کد زیر را می نویسیم :


import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.ResourceBundle;

public class MyUTF8ResourceBundle extends ResourceBundle {
	ResourceBundle bundle;

	public MyUTF8ResourceBundle(ResourceBundle bundle) {
		this.bundle = bundle;
	}

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

	@Override
	protected Object handleGetObject(String key) {
		final String value = bundle.getString(key);
		if (value == null)
			return null;
		try {
			return new String(value.getBytes("ISO-8859-1"), "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException("Encoding not supported", e);
		}
	}

}


درک این کلاس چندان دشوار نیست ، این کلاس خود یک ResourceBundle است پس FXMLLoader می تواند از آن استفاده کند ، چون کلید ها لاتین هستند متد getKeys همان خروجی getKeys شی bundle را بر می گرداند ولی در داخل handleGetObject اوضاع فرق می کند ، در این متد ابتدا با استفاده از کلید مقدار مورد نظر را از bundle استخراج می کنیم و سپس آن را به یک String در قالب UTF-8 تبدیل می کنیم.

آماده ایم تا کنترلر را به صورت زیر تغییر دهیم :


import java.net.URL;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;

public class FXMLMultilingualController implements Initializable {
	FXMLMultilingualExample runner;
	ResourceBundle enBundle;
	ResourceBundle faBundle;

	@FXML
	private void toEnglish(ActionEvent event) {
		runner.change(enBundle);
	}

	@FXML
	private void toFarsi(ActionEvent event) {
		runner.change(faBundle);
	}

	public void setRunner(FXMLMultilingualExample fme) {
		runner = fme;
	}

	@Override
	public void initialize(URL url, ResourceBundle rb) {

		Locale enLocale = new Locale("en", "US");
		Locale faLocale = new Locale("fa", "IR");

		enBundle = new MyUTF8ResourceBundle(ResourceBundle.getBundle("mystrings", enLocale));
		faBundle = new MyUTF8ResourceBundle(ResourceBundle.getBundle("mystrings", faLocale));
	}

}

 

نکته : اگر مثل من روش اول (چه در کد جاوا چه در FXML) را ترجیح می دهید می توانید به جای ResourceBundle از Properties استفاده کنید ، کلاس Properties این قابلیت را دارد که مستقیماً از UTF-8 پشتیبانی کند.(در مورد این کلاس بعداً در یک آموزش جداگانه صحبت خواهم کرد).

 

افزودن دیدگاه جدید

Plain text

  • تگ‌های HTML مجاز نیستند.
  • نشانی صفحه‌ها وب و پست الکترونیک بصورت خودکار به پیوند تبدیل می‌شوند.
  • خطوط و پاراگراف‌ها بطور خودکار اعمال می‌شوند.