Design Principles
The javai18n libraries are built around a small set of explicit decisions. This page explains what those decisions are and why they were made.
1. The locale-event source is separate from the components
Section titled “1. The locale-event source is separate from the components”In the standard Swing and JavaFX APIs there is no built-in concept of “change the locale and have components update themselves.” The usual pattern is either to rebuild the UI after a locale change, or to scatter setTitle(bundle.getString("title")) calls throughout the application.
javai18n uses a different model: a Localizable object (typically the top-level frame or stage) holds the current locale and fires a LocaleEvent to every registered LocaleEventListener when setBundleLocale() is called. Each Resourceful component is a listener. One call propagates everywhere.
This decoupling has several consequences:
- The application can switch locales without rebuilding any UI
- Components register and unregister themselves; there is no central registry to manage
- The same propagation model works for any UI toolkit —
i18n-corehas no UI dependency
2. Bundles follow the class hierarchy
Section titled “2. Bundles follow the class hierarchy”Standard ResourceBundle lookup is flat: each class loads its own bundle, and there is no automatic connection between a subclass bundle and its parent’s bundle. This means a MyButton that extends JButton must either duplicate the base entries or forego inheritance entirely.
javai18n’s LocalizationDelegate walks the class hierarchy from the concrete class up through all Localizable superclasses, loading a bundle for each class it finds. These bundles are linked into a NestedResourceBundle chain. Key lookup searches the concrete class bundle first, then falls up the chain. A subclass bundle needs only the entries it overrides.
This mirrors how Java method dispatch works, and it means a deep hierarchy of reusable components can share a base bundle without duplication.
3. getBundleLocale / setBundleLocale, not getLocale / setLocale
Section titled “3. getBundleLocale / setBundleLocale, not getLocale / setLocale”java.awt.Component has a getLocale() / setLocale() pair. javafx.scene.Node has getLocale(). If Localizable used the same names, any class that both extends a Swing or JavaFX component and implements Localizable would have a naming conflict.
The getBundleLocale / setBundleLocale naming sidesteps this entirely. It also makes the intent clearer: this locale governs resource bundle selection, not the component’s rendering locale (which is typically controlled by the UI toolkit separately).
4. Typed bundles without code generation
Section titled “4. Typed bundles without code generation”Many internationalization frameworks require an annotation processor, a code generation step, or a build plugin to produce typed accessors for bundle entries. This creates a compile-time dependency on the toolchain and means that adding a new localized property requires modifying generated source.
javai18n uses AttributeCollection instead. An AttributeCollection is a plain Java class with a public no-arg constructor and a setAttribute(String, Object) method. The JSON or XML deserializer calls setAttribute() for each entry in the bundle file. Adding a new property means adding a field and a case in setAttribute() — no build plugin, no generated source, no additional compile step.
The trade-off is that setAttribute() is stringly-typed at the deserialization layer. In practice this is bounded: each AttributeCollection subclass defines a fixed set of known keys, and unknown keys are ignored rather than throwing.
5. All locale-event dispatch happens on the correct UI thread
Section titled “5. All locale-event dispatch happens on the correct UI thread”Every Resourceful component’s updateLocaleSpecificValues() method sets component properties — setText(), setIcon(), setToolTipText(), and so on. These calls must happen on the Event Dispatch Thread (Swing) or the JavaFX Application Thread.
javai18n enforces this in the delegate layer:
SwingResourcefulDelegatewraps locale-event delivery inSwingUtilities.invokeLater()FXResourcefulDelegatewraps it inPlatform.runLater()
Application code never needs to think about thread marshalling. Calling setBundleLocale() from any thread is safe.
6. The library never calls Locale.setDefault()
Section titled “6. The library never calls Locale.setDefault()”Locale.setDefault() is a JVM-wide operation. Calling it from inside a library would be an undocumented global side effect: it could silently break DateFormat, NumberFormat, ResourceBundle lookups, and third-party components anywhere in the same JVM that depend on Locale.getDefault().
javai18n operates entirely with per-object bundle locales. The application locale is stored in each LocalizationDelegate instance, not in a global. Multiple independent windows can have different locales simultaneously.
This is also why ColorPicker and DatePicker are absent from i18n-fx: their JavaFX skins call Locale.getDefault() internally, and the only way to influence them would be Locale.setDefault().
7. JPMS is a first-class deployment target
Section titled “7. JPMS is a first-class deployment target”Many libraries treat the Java Platform Module System as an afterthought — something to bolt on after the classpath version works. javai18n was designed with JPMS from the start.
Both classpath and module-path deployments share the same source code and are tested in CI. The key divergence is bundle loading: on the classpath, AssociativeResourceBundleControl handles loading via ResourceBundle.Control. On the module path, AssociativeResourceBundleProvider implements AbstractResourceBundleProvider, and each module registers its bundles via provides directives in module-info.java.
The GetResourceBundleCallback / GetResourceBundleRegistrar mechanism exists specifically to handle a restriction in JPMS: ResourceBundle.getBundle(name, locale, module) does not behave as expected across module boundaries. Each module registers a callback that performs its own bundle lookups, and LocalizationDelegate calls the registered callback rather than calling ResourceBundle.getBundle() directly.
8. One component, one resource key
Section titled “8. One component, one resource key”Each Resourceful component is constructed with a single Resource — a (Localizable source, String key) pair. The key maps to one AttributeCollection in the bundle, which carries all the localizable properties of that component (text, icon, mnemonic, tooltip, etc.).
This means the bundle file is the authoritative inventory of every localizable string in the application. Adding a component requires adding one entry to the bundle. Translators see one entry per component, not scattered fragments.