Tentackle I18N — Database-backed Resource Bundles¶
Overview and Motivation¶
The tentackle-i18n module lets an application serve its localized texts from the database
instead of — or in addition to — the *.properties files baked into its jars. Translators can then
edit texts at runtime (typically through the BundleMonkey tool) and the change takes effect
immediately, across all running clients and servers, without rebuilding or redeploying anything.
It does so without changing a single line of calling code. Application code keeps using the framework's bundle lookups; this module transparently intercepts bundle loading and, when a translation exists in the database, returns it instead of the one from the property file. If nothing is stored, it falls back to the property file, so adding the module to a project is non-intrusive.
tentackle-i18n is the runtime half of Tentackle's database i18n story. Its build-time
counterpart is the tentackle-i18n-maven-plugin,
which pushes the property files into the database and pulls translator edits back into the sources.
Both sides talk to the same two PDOs — StoredBundle
and StoredBundleKey.
The module depends on tentackle-persistence and
tentackle-domain (both declared optional so they are not forced
onto downstream artifacts transitively). It contributes a ModuleHook service and exports the
org.tentackle.i18n, org.tentackle.i18n.pdo and org.tentackle.i18n.pdo.trip packages.
The Data Model¶
Translations live in two tables, modeled as a Tentackle aggregate (a composite root with its components):
| PDO | Table | Class id | Holds |
|---|---|---|---|
StoredBundle |
bundle |
8 | one bundle for one locale (name + locale) |
StoredBundleKey |
bundlekey |
9 | one key → value entry of a bundle |
A StoredBundle is identified by its unique domain key (name, locale) — name is the
fully-qualified bundle base name (e.g. org.myapp.ui.Texts), locale is the locale suffix
(de, de_DE, …). A StoredBundle whose locale is null carries the default / fall-back
translations.
StoredBundle is a composite root: its StoredBundleKey entries are components that belong to it,
are always loaded eagerly with the bundle, and are edited only through the bundle. Use
StoredBundle.setTranslation(key, value)
to add, change (or, with a null value, remove) a translation, and getTranslation(key) to read one
— never add or mutate a StoredBundleKey directly.
Because they are normal PDOs, the bundles are remoting-capable: the pdo.trip package provides the
TRIP delegates so they can be loaded across a Tentackle server just like any other entity.
How Bundle Loading Is Intercepted¶
Tentackle locates resource bundles through the BundleFactory service in tentackle-common. By
default, that is the DefaultBundleFactory. This module ships a replacement, registered via the
service SPI:
StoredBundleFactory—@Service(BundleFactory.class), extendsDefaultBundleFactory. It first tries to load a bundle from the database and only falls back to the superclass (property-file) behavior when nothing is stored. This is the path used in modular (JPMS) applications, where the JDK'sResourceBundleControlProviderSPI is not honored.StoredBundleControlProvider—@Service(ResourceBundleControlProvider.class). For non-modular (classpath) applications it hooks straight into the JDK'sResourceBundle.getBundle(...)machinery, so even plain JDK bundle lookups go through the stored-bundle control.
Both ultimately delegate to
StoredBundleControl, a
ResourceBundle.Control whose newBundle(...):
- Requires a current
Session. IfSession.getCurrentSession()isnull(e.g., very early at startup, or in a context without a database) it behaves like the standard control and just reads the property file. So no database round-trip happens before persistence is up. - Otherwise, it builds the
(name, locale)domain key, loads the matchingStoredBundlefrom the database (via a cached unique-domain-key select), and wraps it in aStoredResourceBundle. - If no stored bundle is found and
fallbackToPropertiesis enabled (the default), it loads thejava.propertiesbundle as usual and logs that it fell back.
StoredResourceBundle is a thin ResourceBundle over the bundle's key/value map. Locale fall-back
(Texts_de_DE → Texts_de → Texts) is reproduced by chaining stored bundles as
ResourceBundle parents, so a more specific locale only needs to store the keys that actually
differ from its parent.
Caching and Live Updates¶
Stored bundles are cached in the factory (and backed by a preloading PdoCache in the persistence
layer), so repeated lookups don't hit the database. To keep that cache correct when a translator
changes a text, StoredBundleControl registers a modification listener on StoredBundle with the
ModificationTracker. Whenever a StoredBundle changes — including
changes made on another node and propagated through the modification tracker — the listener calls
BundleFactory.clearCache(), so the new translation becomes visible everywhere without a restart.
The listener is registered lazily on first use (not in the constructor), because the modification
tracker is not yet running at application startup. Loading uses a thread-local
DomainContext created on demand.
Configuration Switches¶
The behavior can be tuned through two static switches:
StoredBundleFactory.setEnabled(false)— turn off database loading entirely; the factory then behaves exactly likeDefaultBundleFactory(property files only). Useful to disable the feature globally without removing the module.StoredBundleControl.setFallbackToProperties(false)— make a missing stored bundle a hard error: the lookup throwsMissingResourceExceptioninstead of silently reading the property file. This is handy in environments where every text must come from the database.
Both default to the convenient, non-intrusive behavior (enabled, with property-file fallback).
Putting It Together¶
- Add
tentackle-i18nto the application and make sure thebundle/bundlekeytables exist (they are part of the model, so the SQL plugin creates them like any other entity). - Seed the database from the property files with
mvn tentackle-i18n:push. - At runtime, the
StoredBundleFactory/StoredBundleControlserve those texts transparently; translators edit them live throughBundleMonkey, and the modification tracker propagates the changes to every client and server. - Periodically run
mvn tentackle-i18n:pullto bring translator edits back into the property files so they are committed with the sources.
Further Reading¶
- Tentackle I18N Maven Plugin — the build-time counterpart that synchronizes property files and the database
- Tentackle Checker Maven Plugin — build-time verification that bundle keys referenced in the sources actually resolve
- Tentackle Persistence and Tentackle PDO —
the PDO/aggregate mechanics behind
StoredBundleandStoredBundleKey - Tentackle Session — the
SessionandModificationTrackerthat gate loading and drive cache invalidation