Failed Lookup Handling
Missing or malformed translations can expose raw lookup keys or unintended fallback copy to users, hiding content defects until production. By default, a failed lookup returns the key with caller-supplied placeholders interpolated. Applications that must fail rather than continue with fallback copy can configure TranslationFailureHandler.throwException().
// Start with the locale to use when no loaded locale matches Strings strings = Strings.withFallbackLocale(FALLBACK_LOCALE) // Load and validate the application's localized strings .localizedStringSupplier(() -> LocalizedStringLoader .loadFromFilesystem(Paths.get("my-directory"))) // Resolve lookups against a loaded locale .localeSupplier(matcher -> matcher.bestMatchFor(FALLBACK_LOCALE)) // Throw instead of returning the interpolated key after lookup failure .translationFailureHandler(TranslationFailureHandler.throwException()) // Validate the configuration and create the immutable instance .build();
Locale fallback and final failure handling remain separate decisions. Use a TranslationFallbackPolicy when you also need to control whether Lokalized tries another locale first.
Immutable Reloads
Translation updates can arrive while requests are in flight. Replacing data piecemeal risks mixing old and new copy or exposing an incomplete locale. Strings instances are immutable and thread-safe, so build an entire replacement before publishing it. Every caller then sees either the old complete snapshot or the new complete snapshot - never a partially reloaded state.
final class StringsProvider { private static final Locale FALLBACK_LOCALE = Locale.forLanguageTag("pt-BR"); // Atomically publishes one immutable Strings snapshot to every caller private final AtomicReference<Strings> currentStrings = new AtomicReference<>(load()); // Returns the current snapshot. Callers can safely retain and share it public Strings get() { return currentStrings.get(); } // Builds the replacement first, then exposes it with a single atomic swap // If loading fails, callers continue to use the previous snapshot public void reload() { currentStrings.set(load()); } private static Strings load() { return Strings.withFallbackLocale(FALLBACK_LOCALE) .localizedStringSupplier(() -> LocalizedStringLoader .loadFromFilesystem(Paths.get("my-directory"))) .localeSupplier(matcher -> matcher.bestMatchFor(FALLBACK_LOCALE)) .build(); } }
Call reload() from a file watcher, deployment hook, or administrative endpoint. If loading or validation fails, the atomic reference is never updated and callers continue using the previous snapshot.
Load Localized Strings from a database
Teams that manage translations independently from application releases need a durable source of truth and a safe way to publish edits without restarting the application. Store one complete localized strings document per locale. A primary key prevents duplicate locale rows, while the JSONB check rejects non-object documents before they reach the application.
This example uses Pyranid, a JDBC interface that works directly with SQL instead of hiding it behind an ORM, and PostgreSQL, an open source relational database with native JSONB support.
Database schema
CREATE TABLE localized_strings ( language_tag TEXT PRIMARY KEY, strings_json JSONB NOT NULL CHECK (jsonb_typeof(strings_json) = 'object'), updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP );
Load, parse, publish, and reload
import com.lokalized.LocalizedString; import com.lokalized.LocalizedStringLoader; import com.lokalized.Strings; import com.pyranid.Database; import java.io.StringReader; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; // Publishes complete database-backed Strings snapshots to application callers public final class DatabaseLocalizedStrings { private static final Locale FALLBACK_LOCALE = Locale.ENGLISH; private final Database database; private final Locale requestedLocale; private final AtomicReference<Strings> currentStrings; // Maps one PostgreSQL row returned by Pyranid public record LocalizedStringsRow(String languageTag, String stringsJson) {} // Loads and publishes the initial immutable snapshot public DatabaseLocalizedStrings(Database database, Locale requestedLocale) { this.database = database; this.requestedLocale = requestedLocale; this.currentStrings = new AtomicReference<>(buildSnapshot()); } // Returns the current immutable snapshot public Strings get() { return currentStrings.get(); } // Builds the replacement first, then publishes it with one atomic swap public void reload() { currentStrings.set(buildSnapshot()); } // Loads every locale row and parses its localized strings document private Map<Locale, Set<LocalizedString>> loadLocalizedStrings() { List<LocalizedStringsRow> rows = database.query(""" SELECT language_tag, strings_json::text FROM localized_strings ORDER BY language_tag """) .fetchList(LocalizedStringsRow.class); Map<Locale, Set<LocalizedString>> localizedStrings = new LinkedHashMap<>(); for (LocalizedStringsRow row : rows) { Locale locale = new Locale.Builder().setLanguageTag(row.languageTag()).build(); Set<LocalizedString> parsed = LocalizedStringLoader.parse( new StringReader(row.stringsJson()), locale, // Identify this database row in parse errors and diagnostics "postgres:" + row.languageTag() ); localizedStrings.put(locale, parsed); } return Collections.unmodifiableMap(localizedStrings); } // Builds and validates one complete database-backed snapshot private Strings buildSnapshot() { // Start with the locale to use when no database row matches return Strings.withFallbackLocale(FALLBACK_LOCALE) // Load every locale from PostgreSQL for this snapshot .localizedStringSupplier(this::loadLocalizedStrings) // Resolve the caller's locale against the rows that were loaded .localeSupplier(matcher -> matcher.bestMatchFor(requestedLocale)) // Validate the configuration and create the immutable snapshot .build(); } }
The source label passed to LocalizedStringLoader.parse(...) appears in diagnostics, which makes an invalid database row traceable to its locale. Call the example's reload() method after database-backed translations change.