65.9K
CodeProject 正在变化。 阅读更多。
Home

运行时本地化应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.28/5 (6投票s)

2015年4月27日

LGPL3

18分钟阅读

viewsIcon

17659

downloadIcon

134

运行时“即时”应用程序本地化。

引言

您可能也已经遇到过如何本地化应用程序的问题。

实际上,完成这项任务非常简单,因为所有现代编程语言都提供了必要的工具来完成这项工作。

本文将指导您完成 Java 项目的设置,创建一个小型虚拟 SWT 应用程序,并使应用程序能够在运行时进行本地化。

个人事项说明:

随本文附带的源代码**不**包含项目文件,您可以直接在 Java IDE 中打开。它只包含本文使用的目录/包结构中的 .java 源代码文件。原因是 Java 的平台独立性、不同的 IDE(以及它们的项目定义文件)、Java 库的异构安装位置等。所以我决定只包含源文件,而不包含项目定义开销。无论如何:在您喜欢的 IDE 中将所有内容整合起来使其成为一个工作项目应该是一项非常简单的任务。

背景

什么是“本地化”?嗯,实际上它只不过是将所有文本以用户选择的语言显示给用户。一个相当简单的任务,不是吗?至少人们应该这么认为。但为什么市面上有这么多应用程序,只需要在更改用户界面语言后重新启动?我实话实说:我讨厌这种应用程序!

完成本文后,我们将拥有一个小型应用程序,该应用程序允许用户在不重新启动的情况下,随意切换当前语言。远非如此!他只会注意到语言已更改,因为文本看起来不同了——其他所有内容,如输入框、当前打开的窗口、工具栏等,都将保持不变;用户可以立即继续使用该软件。

一般性考虑

现在我们知道我们的目标是什么。如何实现这一点?让我们看看我们将要实现的程序逻辑。

任务:当用户选择一种语言时,应用程序应翻译当前屏幕上显示的所有文本。

我们面临的问题是,在复杂的用户界面中,用户可能打开了多个窗口和工具箱。我们设置当前语言的应用程序部分可能不知道所有这些用户界面部分——通常每个部分只应照顾好自己,而不要干扰其他部分。否则,代码很快就会变得几乎无法阅读,并且非常难以维护或扩展。

第一步:跟踪需要翻译的内容

如前所述,我们用户界面的每一个部分都应该只照顾好自己。例如:工具箱不关心“网络状态”窗口——它在浮动,不关心主窗口状态栏上发生的事情,主窗口不知道用户正在输入数据的表单的任何信息。

为了跟踪需要翻译的内容,我们必须创建一个“总部”来告诉用户界面的各个部分它们应该自己翻译。

在这个“总部”中,各个部分订阅以在语言更改时得到通知,并在不再需要此通知时取消订阅——例如,窗口已关闭。

这不是火箭科学,对吧?

示例 UI:为进一步思考,让我们定义应用程序的用户界面。它应该包含一个带有菜单栏的主窗口,包含多个菜单和子菜单,以及一个显示一些状态信息的第二个窗口。可以通过在主窗口中选择相应的菜单项来选择语言。状态窗口可以打开和关闭,当用户切换语言时,它也应该能够即时本地化。

第二步:调用翻译

当我们的“总部”发出呼叫时,应用程序的所有订阅部分都应该响应。因此,当用户在应用程序的主窗口中切换语言时,这会触发“总部”向所有订阅者发出通知。

您可以看到,主窗口不关心状态窗口;它不关心该窗口当前是否显示,甚至不必知道状态窗口的存在。主窗口只需要知道有我们的“总部”负责其余部分,就像我们的状态窗口只需要知道这个事实一样。

使用代码

当我们回顾一下我们最近考虑的步骤并将它们结合起来时,创建我们的应用程序将变得非常容易。

  • 我们之前描述的“总部”字面上就是对单例设计模式的呼唤,因为它在整个应用程序中应该只存在一次。
  • 任何代码元素都可以订阅到这个“总部”的事实,可以通过创建一个接口来轻松处理,我们应用程序的各个部分可以实现该接口。

实现

好的,让我们开始在您喜欢的 IDE 中创建一个名为“LocalizationExample”的空项目。由于我们的示例应用程序将使用 SWT 进行 GUI,请在您的机器上添加对 SWT 库的引用。现在我们将在项目源文件夹(通常是“src”)中创建以下包:

  • com.hidensity.example
  • com.hidensity.example.localize
  • com.hidensity.example.ui

在“.localize”包中,我们将实现本地化逻辑。首先,让我们从前面提到的接口开始。

接口 ILocalizable

继续创建一个新接口,并将其命名为“ILocalizable”,位于“com.hidensity.example.localize”包中。接口的优点是我们引用实现该接口的类,并且确信它们已经实现了接口中描述的方法。因此,我们不需要知道它是一个窗口、一个工具箱还是其他什么。我们知道它是一个 ILocalizable 实例,因此我们使用它。让实例负责处理我们“总部”的“战斗号角”。

该接口仅描述一个方法:doLocalize。因此,我们的接口如下所示:

package com.hidensity.example.localize;

/**
 * Interface defining methods for localization.
 *
 * Created by HiDensity on 2015-04-24
 *
 * @author HiDensity
 * @version 1.0
 */
public interface ILocalizable {

    /**
     * Does the localization.
     */
    void doLocalize();

}

Localizer 类

接下来我们要做的就是实现我们的“总部”。从现在开始,我们将它称为 Localizer。因此,让我们在与接口相同的包中创建一个名为“Localizer”的类。主要地,我们的 Localizer 包含一个 HashMap,其中包含 ILocalizable 实例的引用。此外,它还包含注册和注销这些实例的方法,以及调用一个或多个已注册实例的 doLocalize 方法。

让我们实现这个类,看看它的实现。

package com.hidensity.example.localize;

import java.util.HashMap;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * Class containing methods for localization.
 *
 * Created by HiDensity on 2015-04-24
 *
 * @author HiDensity
 * @version 1.0
 */
public class Localizer {

    /**
     * Single instance of Localizer.
     */
    private static Localizer instance = null;

    /**
     * HashMap containing objects to be localized.
     */
    private HashMap<String, ILocalizable> localizeObjects = null;

    /**
     * Resources used for localization.
     */
    private ResourceBundle resources = null;

    /**
     * Creates a new Localizer instance.
     */
    private Localizer() {
        localizeObjects = new HashMap<>();
    }

    /**
     * Gets the single instance of Localizer.
     * @return Localizer instance.
     */
    public static synchronized Localizer getInstance() {
        if (Localizer.instance == null)
            Localizer.instance = new Localizer();
        return Localizer.instance;
    }

我认为这些代码的前几行或多或少是自明的。我们有私有的类成员——特别是我们前面提到的 HashMap,它充当需要本地化的对象的目录,以及我们的单个 Localizer 实例。可以通过 getInstance 方法访问此实例,如果实例尚未创建,该方法还会调用类的私有构造函数。此外,我们的 Localizer 实例还包含 ResourceBundle,其中包含每种语言的本地化文本。

我们继续实现注册和注销 ILocalizable 实例到 Localizer 的方法。

    /**
     * Registers an ILocalizable instance for localization.
     * @param localizeObjKey String holding key the instance is identified by.
     * @param localizeObj Instance of ILocalizable.
     */
    public void register(String localizeObjKey, ILocalizable localizeObj) {
        if (!localizeObjects.containsKey(localizeObjKey))
            localizeObjects.put(localizeObjKey, localizeObj);
        else
            localizeObjects.replace(localizeObjKey, localizeObj);
    }

    /**
     * Registers an ILocalizable instance for localization.
     * @param localizeObj Instance of ILocalizable.
     */
    public void register(ILocalizable localizeObj) {
        register(localizeObj.getClass().getName(), localizeObj);
    }

    /**
     * Unregisters an ILocalizable instance for localization.
     * @param localizeObjKey String holding key the instance is identified by.
     */
    public void unregister(String localizeObjKey) {
        if (localizeObjects.containsKey(localizeObjKey))
            localizeObjects.remove(localizeObjKey);
    }

    /**
     * Unregisters an ILocalizable instance for localization.
     * @param localizeObj Instance of ILocalizable.
     */
    public void unregister(ILocalizable localizeObj) {
        unregister(localizeObj.getClass().getName());
    }

正如您所见,我们有两种注册和注销 ILocalizable 实例的方法签名。这使得我们很容易同时在 HashMap 中拥有一个 ILocalizable 实现的多个实例,因为它们由不同的字符串标识。当仅使用 ILocalizable 实例进行注册时,它会使用其完全限定类名进行注册。否则,您可以分配任何您想使用的字符串。重要:只需确保它是一个唯一的字符串,否则现有的注册将被覆盖。注销 ILocalizable 实例以类似的方式完成。

接下来我们要实现的 Localizer 类的方法是调用已注册到 Localizer 的单个 ILocalizable 实例的本地化的方法。

    /**
     * Localizes all registered ILocalizable instances.
     */
    public void localizeAll() {
        localizeObjects.keySet().forEach((key) -> {
            // Try to localize the current instance.
            if (localizeObjects.get(key) != null)
                localizeObjects.get(key).doLocalize();
        });
    }

    /**
     * Localizes a single ILocalizable instance.
     * @param localizeObjKey String holding key the instance is identified by.
     */
    public void localize(String localizeObjKey) {
        if (localizeObjects.containsKey(localizeObjKey) && localizeObjects.get(localizeObjKey) != null)
            localizeObjects.get(localizeObjKey).doLocalize();
    }

    /**
     * Localizes a single ILocalizable instance.
     * @param localizeObj Instance of ILocalizable.
     */
    public void localize(ILocalizable localizeObj) {
        localize(localizeObj.getClass().getName());
    }

您可以看到,我们有一个 localizeAll 方法,它与其名称所说的完全一致:它通过调用每个实例的 doLocalize 方法来本地化所有已注册的 ILocalizable 实例。此外,我们还有另外两个 localize 方法,它们接受 String 或 ILocalizable 实例作为参数,并调用相应实例的 doLocalize 方法。

现在我们将实现 Localizer 类中的最后一个方法。

    /**
     * Sets the locale used for localization.
     * @param languageTag String containing the locale's language tag.
     */
    public void setLocale(String languageTag) {
        getResources(Locale.forLanguageTag(languageTag));
    }

    /**
     * Sets the locale used for localization.
     * @param locale Locale to use.
     */
    public void setLocale(Locale locale) {
        setLocale(locale.toLanguageTag());
    }

    /**
     * Gets the currently used Locale.
     * @return Locale object.
     */
    public Locale getLocale() {
        return resources.getLocale();
    }

    /**
     * Gets a localized String from the current resources.
     * @param key String holding the key of the localized String.
     * @return String with localized text.
     */
    public String getString(String key) {
        return resources.getString(key);
    }

    /**
     * Gets the resources used for localization.
     * @param locale Locale to use for localization.
     */
    private void getResources(Locale locale) {
        resources = ResourceBundle.getBundle("LocalizationExample", locale);
    }
}

在这里,我们有了设置 Localizer 的区域设置(语言)的方法。当使用 setLocale 方法设置语言时,会调用 getResources 方法,该方法从相应的资源文件加载资源。实际上,这些文件还没有存在,所以我们现在就创建它们。

资源文件

在我们的源根文件夹(通常是“src”)中,我们将创建三个 .properties 文件,命名为:

  • LocalizationExample.properties
  • LocalizationExample_de_DE.properties
  • LocalizationExample_es_ES.properties

这些文件构成了我们本地化的ResourceBundle,并在我们的 Localizer 类中的 getResources 方法中引用。通过这些ResourceBundle,我们将支持德语、西班牙语和“默认”语言,后者可以是任何语言——在本例中,我们将把英语本地化放在我们的默认资源文件中。资源文件的命名约定,以便 Java 的 ResourceBundle.getBundle 方法能够自动访问它们,是:“您选择的名称_[语言 ISO 639-1]_[国家/地区 ISO 3166-1a2].properties”。

现在,我们将在每个文件只插入一行。

  • (默认): application.title=LocalizeExample
  • de-DE: application.title=LocalizeExample de-DE
  • es-ES: application.title=LocalizeExample es-ES

我们本地化机制的核心已经准备就绪。现在,我们必须实现一些东西来测试和演示它。

示例 GUI

主用户界面

如本文开头所述,我们将实现一个小型 SWT 应用程序来演示我们刚刚实现的本地化。为此,我们首先在“com.hidensity.example.ui”包中创建一个新类“MainUI”。

package com.hidensity.example.ui;

import com.hidensity.example.localize.ILocalizable;
import com.hidensity.example.localize.Localizer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;

import java.util.Locale;

/**
 * Main UI for localization example.
 *
 * Created by HiDensity on 2015-04-24
 *
 * @author HiDensity
 * @version 1.0
 */
public class MainUI implements ILocalizable {

    private Display display = new Display();
    private Shell shell = new Shell(display);

    // Menus.
    Menu menuBar = null;
    Menu fileMenu = null;
    Menu extrasMenu = null;
    Menu extrasLanguageMenu = null;
    MenuItem fileMenuHeader = null;
    MenuItem fileNewItem = null;
    MenuItem fileExitItem = null;
    MenuItem extrasMenuHeader = null;
    MenuItem extrasLanguageItem = null;
    MenuItem extrasLanguage_enUS_Item = null;
    MenuItem extrasLanguage_deDE_Item = null;
    MenuItem extrasLanguage_esES_Item = null;
    MenuItem separator = null;

正如您所见,它看起来很像一个通用的 SWT 应用程序的源代码。我们有 DisplayShell 对象以及单个 GUI 元素(在本例中是 Menus 和 MenuItems)。唯一**大**的区别是,我们的 MainUI 类实现了我们之前创建的 ILocalizable 接口。

让我们仔细看看接下来发生了什么。

    /**
     * Creates a new instance of MainUI.
     */
    public MainUI() {
        // Register instance for localization.
        Localizer.getInstance().register(this);

        // Initialize the UI.
        initUI();

        // Localize all registered ILocalizable instances.
        Localizer.getInstance().localizeAll();

        // Show the window.
        shell.open();

        // Add listener for "Close" event.
        // When the "Close" event occurs, we do unregister the instance
        // from our Localizer.
        shell.addListener(SWT.Close, event -> Localizer.getInstance().unregister(this));

        // Run the event loop as long as the window is open.
        while (!shell.isDisposed()) {
            // Read the next OS event queue and transfer it to a SWT event.
            if (!display.readAndDispatch())
            {
                // If there are currently no other OS event to process
                // sleep until the next OS event is available.
                display.sleep();
            }
        }

        // Disposes all associated windows and their components.
        display.dispose();
    }

在我们的 MainUI 构造函数中实现了一些本地化逻辑。正如我们在其第一行代码中所见,该实例使用 register 方法在 Localizer 中注册。这里我们不需要传递一个自定义名称,因为应用程序运行时 MainUI 只会存在一次——所以我们可以依赖我们的 Localizer,它在这种情况下选择完全限定类名来标识 ILocalizer 实例——目前是 MainUI 对象。

在此之后,我们有一个 initUI 方法来初始化我们的应用程序的用户界面——我们稍后会简要回顾一下。我们也在几分钟后回到调用 LocalizerlocalizeAll 方法的那一行。

我们还应该关注的另一行代码是为 shell 的关闭事件添加侦听器的行。在这里,我们从 Localizer注销 MainUI。实际上,在这里这样做不是必需的,因为我们的应用程序在这里将被销毁,Localizer 也将被销毁,但我认为通过整个应用程序严格实现逻辑流程是一个好习惯,这样也可以使我们将来更容易扩展应用程序。

初始化用户界面

    /**
     * Initializes the user interface.
     * <p>Note: All texts are initialized with dummy values.
     * In our case we set them to the identifiers we are also using in
     * the ResourceBundle; but we could also use any other String, as
     * they are only placeholders.
     * </p>
     */
    private void initUI() {
        shell.setSize(640, 480);

        shell.setText("application.title");

        initMenu();
    }

此方法只做设置窗口的初始大小和标题。它还调用初始化窗口菜单的方法。

正如方法注释中所述,文本“application.title”只是一个占位符值。到目前为止,它**不**属于我们之前创建的资源文件。我只喜欢将资源名称设置为占位符文本,这使得查找缺失的本地化比使用“真实”文本更容易——因为占位符文本在运行的应用程序中看起来非常奇怪。

initMenu 方法也正如您所期望的那样——它初始化菜单。

    private void initMenu() {
        menuBar = new Menu(shell, SWT.BAR);

        // Set up "File" menu.
        fileMenuHeader = new MenuItem(menuBar, SWT.CASCADE);
        fileMenuHeader.setText("menu.file");
        fileMenu = new Menu(shell, SWT.DROP_DOWN);
        fileMenuHeader.setMenu(fileMenu);
        fileNewItem = new MenuItem(fileMenu, SWT.PUSH);
        fileNewItem.setText("menu.file.new");
        fileNewItem.setAccelerator(SWT.F2);
        separator = new MenuItem(fileMenu, SWT.SEPARATOR);
        fileExitItem = new MenuItem(fileMenu, SWT.PUSH);
        fileExitItem.setText("menu.file.exit");

        // Set up "Extras" menu.
        extrasMenuHeader = new MenuItem(menuBar, SWT.CASCADE);
        extrasMenuHeader.setText("menu.extras");
        extrasMenu = new Menu(shell, SWT.DROP_DOWN);
        extrasMenuHeader.setMenu(extrasMenu);
        extrasLanguageItem = new MenuItem(extrasMenu, SWT.CASCADE);
        extrasLanguageItem.setText("menu.extras.language");
        extrasLanguageMenu = new Menu(shell, SWT.DROP_DOWN);
        extrasLanguageItem.setMenu(extrasLanguageMenu);
        extrasLanguage_deDE_Item = new MenuItem(extrasLanguageMenu, SWT.PUSH);
        extrasLanguage_deDE_Item.setText("menu.extras.language.deDE");
        extrasLanguage_enUS_Item = new MenuItem(extrasLanguageMenu, SWT.PUSH);
        extrasLanguage_enUS_Item.setText("menu.extras.language.enUS");
        extrasLanguage_esES_Item = new MenuItem(extrasLanguageMenu, SWT.PUSH);
        extrasLanguage_esES_Item.setText("menu.extras.language.esES");

        // Set up listeners.
        fileNewItem.addListener(SWT.Selection, event -> new StatusWindow(shell));
        fileExitItem.addListener(SWT.Selection, event -> shell.close());
        extrasLanguage_deDE_Item.addListener(SWT.Selection, event -> {
            Localizer.getInstance().setLocale(new Locale("de", "DE"));
            Localizer.getInstance().localizeAll();
        });
        extrasLanguage_enUS_Item.addListener(SWT.Selection, event -> {
            Localizer.getInstance().setLocale(new Locale("en", "US"));
            Localizer.getInstance().localizeAll();
        });
        extrasLanguage_esES_Item.addListener(SWT.Selection, event -> {
            Localizer.getInstance().setLocale(new Locale("es", "ES"));
            Localizer.getInstance().localizeAll();
        });

        // Assign menu bar.
        shell.setMenuBar(menuBar);
    }

您可以看到,这是 SWT 应用程序的常见 Java 代码。我们的菜单设置,使用占位符文本,如“menu.file”、“menu.file.new”等。

设置侦听器。”注释下的几行代码发生了一些有趣的事情。

  1. 当用户从“文件”菜单中选择“新建”项时,将创建一个新的“StatusWindow”类实例(我们将在下一步实现它)。
  2. 退出”菜单项会导致 shell(MainUI)关闭,这也触发了 shell 的关闭事件,我们在构造函数中设置了一个侦听器,导致 MainUI 从我们的 Localizer 中注销。
  3. 单个语言菜单项的选择事件被侦听,并导致为我们的 Localizer 设置选定的区域设置并调用 Localizer.localizeAll 方法。您可能已经注意到,我们的“English”菜单项将“en-US”的区域设置分配给了我们的 Localizer。但是不存在此区域设置的资源文件。不用担心,在这种情况下,Java 会回退到默认资源文件,在本例中,它包含英语本地化——一切正常。
MainUI - 最后一步

此时编译器将无法编译我们的应用程序——不仅因为缺少“StatusWindow”实现,更重要的是:ILocalizable.doLocalize 方法尚未实现——但我们必须这样做,因为接口要求我们这样做。所以,开始吧!

    /**
     * Does the localization.
     */
    @Override
    public void doLocalize() {
        shell.setText(Localizer.getInstance().getString("application.title"));
        fileMenuHeader.setText(Localizer.getInstance().getString("menu.file"));
        fileNewItem.setText(Localizer.getInstance().getString("menu.file.new"));
        fileExitItem.setText(Localizer.getInstance().getString("menu.file.exit"));
        extrasMenuHeader.setText(Localizer.getInstance().getString("menu.extras"));
        extrasLanguageItem.setText(Localizer.getInstance().getString("menu.extras.language"));
        extrasLanguage_deDE_Item.setText(Localizer.getInstance().getString("menu.extras.language.deDE"));
        extrasLanguage_enUS_Item.setText(Localizer.getInstance().getString("menu.extras.language.enUS"));
        extrasLanguage_esES_Item.setText(Localizer.getInstance().getString("menu.extras.language.esES"));

        // Update layouts.
        shell.layout(true);
    }
}

让我们看看其中的单行——随意选择一行,无论哪一行,因为它们看起来几乎都一样。我们调用单个控件的 setText 方法,该方法为其分配任何显示在屏幕上的文本。在此赋值方法中,我们获取 Localizer 的单个实例并调用其 getString 方法。此方法从当前所选语言的资源文件中检索字符串。

到目前为止,这些文件只包含“application.title”条目。现在让我们也添加其他条目。完成后,文件应如下所示:

  • LocalizationExample.properties
    • application.title=LocalizeExample
    • menu.file=&File
    • menu.file.new=&New\tF2
    • menu.file.exit=E&xit
    • menu.extras=&Extras
    • menu.extras.language=&Language
    • menu.extras.language.deDE=&Deutsch
    • menu.extras.language.enUS=&English
    • menu.extras.language.esES=E&spa\u00f1ol
    • statusWindow.title=Status
    • label.identifier=Internal name
    • label.processors=Number of CPUs
    • label.freeMemory=Free memory (bytes)
    • label.totalMemory=Total memory (bytes)
  • LocalizationExample_de_DE.properties
    • application.title=LocalizeExample de-DE
    • menu.file=&Datei
    • menu.file.new=&Neu\tF2
    • menu.file.exit=B&eenden
    • menu.extras=&Extras
    • menu.extras.language=&Sprache
    • menu.extras.language.deDE=&Deutsch
    • menu.extras.language.enUS=&English
    • menu.extras.language.esES=E&spa\u00f1ol
    • statusWindow.title=Status de-DE
    • label.identifier=Interner Name
    • label.processors=CPU-Anzahl
    • label.freeMemory=Freier Speicher (Bytes)
    • label.totalMemory=Gesamt-Speicher (Bytes)
  • LocalizationExample_es_ES.properties
    • application.title=LocalizeExample es-ES
    • menu.file=&Fichero
    • menu.file.new=&Nuevo\tF2
    • menu.file.exit=&Terminar
    • menu.extras=&Extras
    • menu.extras.language=&Idioma
    • menu.extras.language.deDE=&Deutsch
    • menu.extras.language.enUS=&English
    • menu.extras.language.esES=E&spa\u00f1ol
    • statusWindow.title=Estado es-ES
    • label.identifier=Nombre interno
    • label.processors=N\u00famero de CPU
    • label.freeMemory=Almac\u00e9n libre (byte)
    • label.totalMemory=Toda el almac\u00e9n (byte)

现在我们的本地化资源文件已经准备就绪,包含了我们所需的一切。它们也已经包含了我们现在要实现的示例“StatusWindow”类的本地化文本。

示例 StatusWindow

让我们在项目“com.hidensity.example.ui”包中创建一个名为“StatusWindow”的新类。此类代码看起来很像 MainUI 类的源代码,但有一些细微的差别。

package com.hidensity.example.ui;

import com.hidensity.example.localize.ILocalizable;
import com.hidensity.example.localize.Localizer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import java.util.UUID;

/**
 * Status tool window for localization example.
 *
 * Created by HiDensity on 2015-04-24
 *
 * @author HiDensity
 * @version 1.0
 */
public class StatusWindow implements ILocalizable {

    private String internalName = null;
    private Shell shell = null;

    // GUI elements.
    Label labelIdentifier = null;
    Text textIdentifier = null;
    Label labelProcessors = null;
    Text textProcessors = null;
    Label labelFreeMemory = null;
    Text textFreeMemory = null;
    Label labelTotalMemory = null;
    Text textTotalMemory = null;

    /**
     * Creates a new instance of the status toolbox window.
     * @param parentShell Shell the toolbox belongs to.
     */
    public StatusWindow(Shell parentShell) {
        // Generate internal name.
        // Necessary for localizing a bunch of instances of the class.
        internalName = String.format("%s[%s]", this.getClass().getName(), UUID.randomUUID().toString());

        // Register this instance with the generated internal name.
        Localizer.getInstance().register(internalName, this);

        // Assign this shell to its parent.
        shell = new Shell(parentShell, SWT.SHELL_TRIM | SWT.TOOL);

        // Initialize the UI.
        initUI();

        // Do the localization for this shell.
        Localizer.getInstance().localize(internalName);

        // Display the shell.
        shell.open();
        
        // Add listener for "Close" event.
        // When the "Close" event occurs, we do unregister the instance
        // from our Localizer.
        shell.addListener(SWT.Close, event -> Localizer.getInstance().unregister(internalName));
    }

MainUI 一样,我们有一些控件(标签和文本框)的成员定义,还有一个 Shell 对象。一个重要的区别是成员 internalName。由于我们应用程序的用户可能会打开许多 Status 窗口——我们没有实现限制为只有一个实例的逻辑——仅凭完全限定类名注册状态窗口将导致第二个实例覆盖第一个实例,第三个实例覆盖第二个实例,依此类推……结果是所有可见的实例中只有最后一个会被本地化。这是我们不希望出现的情况——因此所有实例都应该被本地化。因此,我们生成一个内部名称。它包含完全限定的类名,后跟方括号中的随机生成的UUID

现在,当用户打开我们的 StatusWindow 类的第一个实例时,它的名称可能是(简化后)“com.hidensity.example.ui.StatusWindow[79]”,而第二个实例可能是“com.hidensity.example.ui.StatusWindow[84]”——这两个名称都是唯一的,因此在我们的 Localizer 中不会出现命名冲突。

我们还为窗口的关闭事件添加了一个侦听器,使其从 Localizer 中注销。

Shell 对象的创建方式与 MainUI 类中的有所不同,因为我们将其绑定到一个 parentShell——在本例中是 MainUI,因为它们属于一起。

我们还有一个初始化窗口的方法——initUI,然后我们调用 Localizerlocalize 方法来处理当前窗口的实例。嗯,当然,我们可以直接调用本地 doLocalize 方法,而不必通过 Localizer——但我更喜欢这种方式,并且也建议您这样做,因为本地化的副作用可以在单个位置完成,而不是分散在您的代码中。

用户界面初始化的代码和 ILocalizable.doLocalize 方法应该被提及并保持注释,因为我认为现在这些机制应该相当清楚了。

    /**
     * Initializes the user interface.
     */
    private void initUI() {
        shell.setSize(400, 200);

        GridLayout layout = new GridLayout(2, false);
        shell.setLayout(layout);

        labelIdentifier = new Label(shell, SWT.NONE);
        labelIdentifier.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
        labelIdentifier.setText("label.identifier");
        textIdentifier = new Text(shell, SWT.NONE | SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.WRAP);
        textIdentifier.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        textIdentifier.setText(internalName);
        labelProcessors = new Label(shell, SWT.NONE);
        labelProcessors.setText("label.processors");
        textProcessors = new Text(shell, SWT.NONE | SWT.READ_ONLY | SWT.BORDER);
        textProcessors.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        textProcessors.setText(String.valueOf(Runtime.getRuntime().availableProcessors()));
        labelFreeMemory = new Label(shell, SWT.NONE);
        labelFreeMemory.setText("label.freeMemory");
        textFreeMemory = new Text(shell, SWT.NONE | SWT.READ_ONLY | SWT.BORDER);
        textFreeMemory.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        textFreeMemory.setText(String.valueOf(Runtime.getRuntime().freeMemory()));
        labelTotalMemory = new Label(shell, SWT.NONE);
        labelTotalMemory.setText("label.totalMemory");
        textTotalMemory = new Text(shell, SWT.NONE | SWT.READ_ONLY | SWT.BORDER);
        textTotalMemory.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        textTotalMemory.setText(String.valueOf(Runtime.getRuntime().totalMemory()));
    }

    /**
     * Does the localization.
     */
    @Override
    public void doLocalize() {
        shell.setText(Localizer.getInstance().getString("statusWindow.title"));
        labelIdentifier.setText(Localizer.getInstance().getString("label.identifier"));
        labelProcessors.setText(Localizer.getInstance().getString("label.processors"));
        labelFreeMemory.setText(Localizer.getInstance().getString("label.freeMemory"));
        labelTotalMemory.setText(Localizer.getInstance().getString("label.totalMemory"));

        // Update layouts.
        shell.layout(true);
    }
}

这里我特别想提的是 doLocalize 方法中的最后一行代码,它会导致 shell 的布局更新,以便所有本地化文本都能完全显示。

主方法

我们快完成了。唯一还缺少的是我们应用程序的 main 方法。让我们在“com.hidensity.example”包中创建一个名为“LocalizationExample”的新类。这个类实际上并没有多少代码。

package com.hidensity.example;

import com.hidensity.example.localize.Localizer;
import com.hidensity.example.ui.MainUI;

import java.util.Locale;

/**
 * LocalizationExample main class.
 *
 * Created by HiDensity on 2015-04-24
 *
 * @author HiDensity
 * @version 1.0
 */
public class LocalizationExample {

    public static void main(String[] args) {
        // Set default Locale for localization.
        Localizer.getInstance().setLocale(Locale.getDefault());

        // Run the application.
        new MainUI();
    }

}

我们在这里所做的只是将系统默认区域设置设置为我们的 Localizer 实例,并运行 MainUI 类的一个新实例。除此之外没有别的。

结论

如果您已经正确设置了一切,您现在应该有一个看起来相似的项目:

(注意:我的 IDE(IntelliJ IDEA)中 ResourceBundle 显示如下,也许您的 IDE 显示方式不同。)

设置您的 IDE 来运行您的项目——选择 LocalizationExample 类作为主类,然后尝试一下。您现在应该会看到类似以下的内容:

通过按 F2 键或从“文件”菜单中选择“新建”来打开一个新的“StatusWindow”,您应该会看到我们辉煌的状态工具箱窗口。现在稍微玩一下本地化菜单,您应该会看到,当选择相应的语言菜单项时,状态窗口会立即本地化。下面的屏幕截图显示它总是相同的状态窗口(查看相同的唯一内部名称)。

英语区域设置

德语区域设置

西班牙语区域设置

我们完成了!我希望我能向您展示,本地化 Java 应用程序既不是火箭科学也不是脑部手术。而且,说实话,让现有应用程序支持多种语言也非常简单。只需要几行代码。

我也希望您觉得本文有用,并期待您的回复。

关注点

Lambda 表达式

正如您在代码中看到的,我使用了一些Lambda 表达式(例如

localizeObjects.keySet().forEach(key) -> ...

此源代码使用 Java 1.8 开发,但也可在早期版本中使用。要将其与 Java 1.6 或 1.7 一起使用,您只需要重写 Lambda 表达式。通用逻辑不受影响。

资源文件 - 1

如上所述,在缺少所选区域设置的资源文件的情况下,Java 会回退到应用程序的默认资源文件。在本例中,我们将“en-US”区域设置分配给我们的 Localizer 以进行英语翻译,Java 选择包含英语翻译的默认文件。但是,当您在一台默认语言为英语的机器上工作,但应用程序的默认语言应该是中文时,会发生什么?嗯,也相当简单地完成了这项任务。

Locale.getDefault 方法返回您正在工作的系统的默认区域设置。在之前设想的环境中,这将是“en-US”。所以,我们需要为我们的本地化ResourceBundle创建以下文件:

  • 中文(默认):[resourceBundleName].properties
  • 英文:[resourceBundleName]_en_US.properties
  • ...

现在,当用户在默认区域设置为“en-US”的系统上工作时,将使用英语本地化,因为 Locale.getDefault 将返回“en-US”。在所有其他情况下,将使用中文本地化,因为这是默认的。

资源文件 - 2

为了使用户界面正确显示特殊字符,有必要使用 Unicode 字符表示法对其进行编码,如资源文件所示。您可以在示例中的西班牙语翻译中尤其看到这一点。

历史

  • 2015-04-27 初始版本 1.0
© . All rights reserved.