Cookbook

Practical Lokalized patterns for common application concerns, from choosing a failure policy to publishing fresh translations without interrupting callers.

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.