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.