UI Localizer — tool for simplifying localization of Java applications. It's just an annotation processor that captures fields annotated with @Localizable
or @LocalizationProperty
in the compile-time.
When you use @Localizable
annotation it performs two actions:
- Through bytecode manipulations, replaces initialization of
@Localizable
strings by reading value from.propreties
file (usingResourceBundle
) - Generates
.properties
template file with default values from source code
In case of @LocalizationProperty annotation, the tool just collects values to .properties file from source code.
You should just add uilocalizer.jar to compiler classpath and mark all localizable strings with @Localizable
. Example:
@Localizable("order.dialog.title") private static final String TITLE = "Order Details";
@Localizable("order.dialog.comfirm") private static final String CONFIRM = "Confirm and Exit";
The only one annotation parameter should contain a name of property file (before the first dot) and property key (after the first dot).
Compilation of the example above will generate the following .properties
template file:
dialog.title=Order Details
dialog.confirm=Confirm and Exit
If there are some strings requiring more complex localization logic (for instance, you want to change them after loading
of the class) you may use annotation @LocalizationProperty
:
@LocalizationProperty
public final static SomeClass complexVar = new SomeClass("order.dialog.someText", "Some text");
@LocalizationProperty
public final static SomeClass anotherComplexVar = SomeStaticClass.getSpecificValue("order.dialog.someText", "Some text");
This way you could automatically store such strings in same .properties
files.
To add support for any language in your application, you should:
- Take the generated template file
- Translate all properties values into the desired language
- Rename file as
originalname_LANGUAGETAG.properties
- Add resulting
.properties
file to the application runtime classpath
Example of resulting French translation file:
dialog.title=Détails de la Commande
dialog.confirm=Confirmer et Quitter
You can choose the current language in your application using java property ui.dialogs.locale
. Syntax for the java property is the same as syntax for argument of java.util.Locale.forLanguageTag method:
-Dui.dialogs.locale=fr-fr
UI Localizer tool is fail-safe. That means if anything of mentioned above is missing (java property, key in a file for concrete language or a file for the language itself), default string value from initial source code will be used.
If you use UI Localizer with Gradle incremental compilation (or using the same property file for multiple modules), properties from each processing phase will be merged with previously existed in the file; new properties will replace old ones. Old properties that do not have a corresponding mention in the code will remain in place, so clean up all template files before compilation with UI Localizer to avoid stale options.
You can define a folder where output files could be found. Use the option com.devexperts.uilocalizer.outputFolder
set to path to a folder, e.g. -Acom.devexperts.uilocalizer.outputFolder=C:\project\build
.
The folder would be created by the UI Localizer if it is absent.
You can check that only a specific bundle is used for localization in processed files. Use the option com.devexperts.uilocalizer.requireBundleName
, e.g., -Acom.devexperts.uilocalizer.requireBundleName=user
,
and a compilation will fail with an exception if @Localizable("com.devexperts.user.Panel.Label")
is used instead of @Localizable("user.Panel.Label")
.
There is a way to store the language properties in one place. This method will guarantee language consistency of your application. Also using such a controller you can change language "on the fly" by setting language. To use it you should:
-
Create a class with
public static Locale getLanguage()
method. For example:public class SingletonLanguageController { private static final Object lock = new Object(); private static volatile Locale lang; private static final String LANG_PROP = "ui.dialogs.locale"; private static final String DEFAULT_LOCALE_TAG = "en-US"; private SingletonLanguageController() { } public static Locale getLanguage() { if (lang == null) { synchronized (lock) { if (lang == null) { lang = Locale.forLanguageTag(System.getProperty(LANG_PROP, DEFAULT_LOCALE_TAG)); } } } return lang; } public static void updateLocale() { synchronized (lock) { lang = Locale.forLanguageTag(System.getProperty(LANG_PROP, DEFAULT_LOCALE_TAG)); } } public static void setLanguage(String localeTag) { synchronized (lock) { System.setProperty(LANG_PROP, localeTag); updateLocale(); } } }
-
Pass an argument to an annotation processor with option name
com.devexperts.uilocalizer.languageControllerPath
, value - fully qualified name:
-Acom.devexperts.uilocalizer.languageControllerPath=com.example.SingletonLanguageController
In case you want to use your own implementation of localization mechanism, you should provide an interface to it
as a public
static
method and tell UI Localizer to use it with com.devexperts.uilocalizer.localizationMethod
option.
Your localization method shall get a localized property key and a default value as parameters:
package my.app;
public class MyLocalizationClass {
public static String myLocalizationMethod(String key, String defaultStr) {
// some code
}
}
Option for localizer to use method above would be
-Acom.devexperts.uilocalizer.localizationMethod=my.app.MyLocalizationClass.myLocalizationMethod
The tool just replaces each localizable string literal with a static method call, that attempts to read property with needed locale. If the attempt fails, the method returns initial value from the code. An example of annotated class and resulting bytecode:
Initial code:
public class TestClass {
@Localizable("scope.key")
private static final String KUKU = "cucumber";
}
Transformed code without language controller:
public class TestClass {
@Localizable(value = "scope.key")
private static final String KUKU = getString_u("scope.key", "cucumber");
private static volatile java.util.Locale LOCALE_u;
private static String getString_u(String key, String defaultString) {
try {
if (LOCALE_u == null)
LOCALE_u = java.util.Locale.forLanguageTag(System.getProperty("ui.dialogs.locale", "en-US"));
return java.util.ResourceBundle.getBundle(key.substring(0, key.indexOf(46)), LOCALE_u)
.getString(key.substring(key.indexOf(46) + 1));
} catch (Exception e) {
return defaultString;
}
}
}
Transformed code with language controller:
public class TestClass {
@Localizable(value = "scope.key")
private static final String KUKU = getString_u("scope.key", "cucumber");
private static String getString_u(String key, String defaultString) {
try {
return java.util.ResourceBundle.getBundle(key.substring(0, key.indexOf(46)),
SingletonLanguageController.getLanguage()).getString(key.substring(key.indexOf(46) + 1));
} catch (Exception e) {
return defaultString;
}
}
}
Transformed code with custom method:
public class TestClass {
@Localizable(value = "scope.key")
private static final String KUKU = my.app.MyLocalizationClass.myLocalizationMethod("scope.key", "cucumber");
}
- Smart build systems may omit part of classes during repeated build. To avoid missing properties in the template file, perform cleanup first.
- Template files will be created in the current directory of javac process. If you use IDEA, it's likely ~/.IntellijideaXX/system/compile-server.
- You can put not only files with translations into runtime classpath, but template files too. It's not necessary but allows you to change human readable strings dynamically, without project rebuild.