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.

JavaScript

Applications that must fail rather than continue with fallback copy can give createStrings a translationFailureHandler function that returns THROW_EXCEPTION. A missing translation then throws a MissingTranslationError.

Java

Applications that must fail rather than continue with fallback copy can configure TranslationFailureHandler.throwException().

Swift

Configure TranslationFailureHandler.throwException() when a final failed lookup must throw. Missing translations produce MissingTranslationError; resolution failures rethrow the retained cause.

JavaScript

import { createStrings } from "lokalized";
import { THROW_EXCEPTION } from "lokalized/core";
import { readStringsFromDirectory } from "lokalized/node";

const FALLBACK_LOCALE = "en";

const strings = createStrings({
  // Load and validate the application's localized strings
  localizedStringSupplier: () => (readStringsFromDirectory("my-directory").catalogs),
  // The locale to use when no loaded locale matches
  fallbackLocale: FALLBACK_LOCALE,
  // Resolve lookups against a loaded locale
  localeSupplier: () => FALLBACK_LOCALE,
  // Throw instead of returning the interpolated key after lookup failure
  translationFailureHandler: () => THROW_EXCEPTION,
});

Java

// 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();

Swift

import Foundation
import Lokalized

let locale = try LocaleTag("en")
// Run this during startup, then retain and share strings throughout the app or tool.
let files = try LocalizedStringLoader.loadFromDirectory(
  URL(fileURLWithPath: "strings", isDirectory: true))
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
let strings = try DefaultStrings(configuration: StringsConfiguration(
  localizedStringSupplier: { catalogs },
  localeSupplier: { _ in locale },
  fallbackLocale: locale,
  translationFailureHandler: .throwException()
))

do {
  _ = try strings.get("A missing key")
  preconditionFailure("Expected a missing translation error")
} catch is MissingTranslationError {
  // Present an application error, reject this operation, or report the missing key.
}

JavaScript

Locale fallback and final failure handling remain separate decisions. Use the translationFallbackPolicy option when you also need to control whether Lokalized tries another locale first.

Java

Locale fallback and final failure handling remain separate decisions. Use a TranslationFallbackPolicy when you also need to control whether Lokalized tries another locale first.

Swift

Final failure handling and locale fallback are separate. Use TranslationFallbackPolicy when you also need to control whether another locale is tried 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.

JavaScript

A Strings instance is frozen, so build an entire replacement before publishing it; a single assignment then publishes it. Every caller sees either the old complete snapshot or the new complete snapshot - never a partially reloaded state.

Java

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.

Swift

DefaultStrings snapshots are immutable and Sendable. Build a replacement completely, then publish it through an actor. Concurrent callers retain a complete snapshot.

JavaScript

import { createStrings } from "lokalized";
import { readStringsFromDirectory } from "lokalized/node";

const FALLBACK_LOCALE = "pt-BR";

function load() {
  return createStrings({
    localizedStringSupplier: () => (readStringsFromDirectory("my-directory").catalogs),
    fallbackLocale: FALLBACK_LOCALE,
    localeSupplier: () => FALLBACK_LOCALE,
  });
}

// One frozen Strings snapshot, shared by every caller
let currentStrings = load();

// Returns the current snapshot. Callers can safely retain and share it
export function getStrings() {
  return currentStrings;
}

// Builds the replacement first, then publishes it with a single assignment
// If loading fails, callers continue to use the previous snapshot
export function reload() {
  currentStrings = load();
}

Java

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();
  }
}

Swift

import Foundation
import Lokalized

actor LocalizedStringsStore {
  private var snapshot: DefaultStrings
  private let directory: URL
  private let fallback: LocaleTag

  init(directory: URL, fallback: LocaleTag) throws {
    self.directory = directory
    self.fallback = fallback
    self.snapshot = try Self.load(directory, fallback: fallback)
  }

  func current() -> DefaultStrings { snapshot }

  func reload() throws {
    // Build and validate the complete replacement before publishing it.
    let replacement = try Self.load(directory, fallback: fallback)
    snapshot = replacement
  }

  private static func load(_ directory: URL, fallback: LocaleTag) throws -> DefaultStrings {
    let files = try LocalizedStringLoader.loadFromDirectory(directory)
    let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
    return try DefaultStrings(configuration: StringsConfiguration(
      localizedStringSupplier: { catalogs }, localeSupplier: { _ in fallback },
      fallbackLocale: fallback
    ))
  }
}

JavaScript

Call reload() from a file watcher, deployment hook, or administrative endpoint. If loading or validation fails, reload() throws before the assignment, and callers continue using the previous snapshot.

Java

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.

Swift

Call reload() on the actor from an application-owned file watcher or update hook. If loading or construction throws, the previous snapshot remains available. Fetch a snapshot with await store.current() before translating.

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.

This example uses node-postgres (pg), the standard PostgreSQL client for Node.js, 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

JavaScript

import pg from "pg";
import { createStrings } from "lokalized";
import { parseStrings } from "lokalized/parse";

const FALLBACK_LOCALE = "en";

// Loads every locale row and parses its localized strings document
async function loadSnapshot(pool) {
  const { rows } = await pool.query(`
    SELECT language_tag, strings_json::text AS strings_json
    FROM localized_strings
    ORDER BY language_tag
  `);

  const files = rows.map((row) => parseStrings(row.strings_json, {
    locale: row.language_tag,
    // Appears in diagnostics, so an invalid row is traceable to its locale
    source: `localized_strings/${row.language_tag}`,
  }));

  return createStrings({
    localizedStringSupplier: () => (Object.fromEntries(files.map((file) => [file.locale, file]))),
    fallbackLocale: FALLBACK_LOCALE,
    localeSupplier: () => FALLBACK_LOCALE,
  });
}

// Publishes complete database-backed Strings snapshots to application callers
export class DatabaseLocalizedStrings {
  #pool;
  #currentStrings;

  // Loads and publishes the initial snapshot
  static async create(pool) {
    const localizedStrings = new DatabaseLocalizedStrings(pool);
    await localizedStrings.reload();
    return localizedStrings;
  }

  constructor(pool) {
    this.#pool = pool;
  }

  // Returns the current snapshot
  get() {
    return this.#currentStrings;
  }

  // Builds the replacement first, then publishes it with one assignment
  async reload() {
    this.#currentStrings = await loadSnapshot(this.#pool);
  }
}

const localizedStrings = await DatabaseLocalizedStrings.create(new pg.Pool());

Java

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 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) {
    this.database = database;
    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)
      // Use English when a lookup does not specify a locale
      .localeSupplier(matcher -> FALLBACK_LOCALE)
      // Validate the configuration and create the immutable snapshot
      .build();
  }
}

Swift

import Foundation
import Lokalized

// The application's database layer supplies locale tags and JSON strings.
// Lokalized itself has no database dependency.
func stringsFromRows(_ rows: [(locale: String, json: String)]) throws -> DefaultStrings {
  var loaded: [LocaleTag: LocalizedCatalog] = [:]
  for row in rows {
    let locale = try LocaleTag(row.locale)
    let file = try LocalizedStringLoader.parse(row.json, locale: row.locale,
      source: "database:\(row.locale)")
    guard loaded[locale] == nil else {
      throw NSError(domain: "MyApp.Translations", code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Duplicate locale: \(row.locale)"])
    }
    loaded[locale] = LocalizedCatalog(strings: file.strings)
  }
  let catalogs = loaded // Freeze the values captured by @Sendable callbacks.
  let en = try LocaleTag("en")
  return try DefaultStrings(configuration: StringsConfiguration(
    localizedStringSupplier: { catalogs }, localeSupplier: { _ in en }, fallbackLocale: en
  ))
}

JavaScript

The source label passed to parseStrings(...) appears in diagnostics, which makes an invalid database row traceable to its locale. Call the example's reload() method after database-backed translations change.

Java

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.

Swift

Your database layer supplies locale tags and JSON strings to LocalizedStringLoader.parse(...). The source label identifies a database row in diagnostics. Build the complete replacement before publishing it through the actor shown above.