Lokalized: make your applications sound natural in any language.

Move complex grammar and language rules out of application code and into the hands of your translators.

Lokalized supports JavaScript, Java, and Swift.

Proudly powering production systems since 2017.

en.json Localized Strings File (English)
{
  "I read {{bookCount}} books.": {
    "translation": "I read {{bookCount}} {{books}}.",
    "placeholders": {
      "books": {
        "value": "bookCount",
        "translations": {
          "CARDINALITY_ONE": "book",
          "CARDINALITY_OTHER": "books"
        }
      }
    },
    "alternatives": [
      {
        "bookCount == 0": "I didn't read any books."
      }
    ]
  }
}

JavaScript

// Your JavaScript code
const message = strings.get(
  "I read {{bookCount}} books.",
  { bookCount: 3 }
);

Java

// Your Java code
String message = strings.get(
  "I read {{bookCount}} books.",
  Map.of("bookCount", 3)
);

Swift

// Your Swift code
let message = try strings.get(
  "I read {{bookCount}} books.",
  placeholders: ["bookCount": .integer(3)]
)
assert(message == "I read 3 books.")

bookCount = 3“I read 3 books.”OTHER

bookCount = 1“I read 1 book.”ONE

bookCount = 0“I didn't read any books.”alternative

Why Lokalized?

  • Keep language rules out of application code: locale-specific grammar and wording live with the translations instead of being scattered through conditionals
  • Give translators expressive control: placeholders, language forms, and ordered alternatives can rewrite a fragment or an entire message when natural copy requires it
  • Model more than simple plurals: cardinality, ordinality, ranges, gender, grammatical case, definiteness, classifiers, formality, clusivity, animacy, and phonetics are first-class concepts
  • Solve agreement problems many localization formats do not model directly: a small but powerful expression language gives translators the freedom to author the natural, idiomatic phrasing each situation requires. See how Lokalized compares
  • Match locales predictably: BCP 47 tags, CLDR parent locales, likely scripts, weighted Accept-Language preferences, and explicit tiebreakers are handled deterministically, with the same IANA language-range equivalents in every runtime; see the matching order
  • Fail safely: bounded loading and evaluation, explicit fallback policies, and structured diagnostics make malformed or incomplete translations observable; see fallback and failure handling
  • One file format for JavaScript, Java, and Swift: all implementations read the same localized strings files and offer the same translation capabilities
  • Stay lightweight: immutable, thread-safe design. Lokalized requires no runtime dependencies

Non-Goals

  • JavaScript

    Date/time, number, percentage, and currency formatting or parsing - use Intl for formatting and handle parsing separately

    Java

    Date/time, number, percentage, and currency formatting or parsing - use the JDK's formatters and parsers

    Swift

    Date/time, number, percentage, and currency formatting or parsing - use Foundation format styles and formatters
  • JavaScript

    Collation - use JavaScript's Intl.Collator

    Java

    Collation - use the JDK's Collator

    Swift

    Collation - use Foundation string comparison with an explicit locale
  • JavaScript

    Support Node.js before 20. The JavaScript package targets Node.js 20+ and modern browsers

    Java

    Support Java 8 and below. Lokalized targets Java 9+

    Swift

    Support older Apple systems. Lokalized targets Swift 6.2+, iOS 15+, and macOS 12+

License

The Lokalized libraries are open source under the commercially friendly Apache License 2.0.

Each includes generated language data derived from Unicode CLDR under Unicode License v3 and language-range equivalence data generated from the IANA Language Subtag Registry. The Java library also embeds MIT-licensed minimal-json source, and this website's language reference uses MIT-licensed flag artwork.

See the Licensing page for full terms, attribution, and third-party notices.

Installation

JavaScript

npm (Node.js or a browser with a bundler)

npm install lokalized@1.0.0

Use the package in Node.js 20+ or bundle it for a modern browser. The npm entry points use ES modules, with TypeScript declarations included. Installing with npm does not mean your application must run on Node.js.

Browser without a bundler

No npm installation or Node.js server is needed. Use a plain script tag, or import the prebuilt browser module from a versioned CDN URL inside a module script:

<script type="module">
  import { createStrings } from "https://cdn.jsdelivr.net/npm/lokalized@1.0.0/dist/browser/lokalized.js";
</script>

Java

Maven

<dependency>
  <groupId>com.lokalized</groupId>
  <artifactId>lokalized</artifactId>
  <version>3.1.2</version>
</dependency>

Gradle

dependencies {
  implementation("com.lokalized:lokalized:3.1.2")
}

Android

The same Java artifact supports Android 8.0 (API 26) and later, with no additional dependencies:

android {
  defaultConfig { minSdk = 26 }
}
dependencies {
  implementation("com.lokalized:lokalized:3.1.2")
}

See Android asset loading and app locales below. An example integration is also available.

Direct Download

If you don't use Maven or Gradle, you can drop lokalized-3.1.2.jar directly into your project. No other dependencies are required.

Swift

Xcode: iOS and macOS apps

In Xcode, choose File → Add Package Dependencies, enter https://github.com/lokalized/lokalized-swift, select Up to Next Major Version starting at 1.0.0, and add the Lokalized product to your app target.

Swift Package Manager

Use Swift 6.2 or later, with iOS 15 and macOS 12 deployment targets or later. The library has zero external runtime dependencies. This package example copies a folder of locale-named JSON files into the resource-owning target. Choose the app, package, or tool setup below:

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
  name: "MyApp",
  platforms: [.iOS(.v15), .macOS(.v12)],
  dependencies: [
    .package(url: "https://github.com/lokalized/lokalized-swift", from: "1.0.0")
  ],
  targets: [
    .executableTarget(name: "MyApp", dependencies: [
      .product(name: "Lokalized", package: "lokalized-swift")
    ], resources: [.copy("Lokalized")])
  ],
  swiftLanguageModes: [.v6]
)

Choose How You Use JavaScript

The translation API works in both a browser and Node.js. The way you load the library and your localized strings depends on where your code runs:

Browser: plain script tag

The https://cdn.jsdelivr.net/npm/lokalized@1.0.0/dist/browser/lokalized.global.js script defines window.lokalized; call its methods directly. No module imports, npm installation, Node.js server, or build step is required:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Lokalized example</title>
  </head>
  <body>
    <p id="welcome"></p>
    <script src="https://cdn.jsdelivr.net/npm/lokalized@1.0.0/dist/browser/lokalized.global.js"></script>
    <script>
      const strings = lokalized.createStrings({
        localizedStringSupplier: () => ({
          en: { welcome: "Hello, {{name}}!" },
          fr: { welcome: "Bonjour, {{name}}!" },
        }),
        fallbackLocale: "en",
        localeSupplier: () => document.documentElement.lang || "en",
      });

      // Start with the browser's preferred language among the loaded translations
      document.documentElement.lang =
        lokalized.chooseBrowserLocale(strings.getLocaleConfiguration());
      document.querySelector("#welcome").textContent = strings.get("welcome", { name: "Ada" });
    </script>
  </body>
</html>

The example selects an initial language from navigator.languages, with English as the fallback, and sets <html lang="…"> accordingly. If your app has a saved language preference, use that instead of the browser's choice. localeSupplier reads the page language on each lookup, so later lookups use the updated language after your app's language picker changes it. The example displays Hello, Ada! in English and Bonjour, Ada! in French.

You can also copy dist/browser/lokalized.global.js from the npm package and serve it from your own site. The single file includes all browser-safe functionality: root methods such as lokalized.createStrings, plus namespaces such as lokalized.load, lokalized.negotiate, and lokalized.ssr.

Browser: ES modules without a build step

Save these two files on any static web host and open index.html over HTTP. The example keeps its small translations in JavaScript, so no application server or manifest is required. Pin the CDN URL to a package version.

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <p id="greeting"></p>
    <script type="module" src="./greeting.js"></script>
  </body>
</html>

greeting.js

import {
  createStrings, chooseBrowserLocale,
} from "https://cdn.jsdelivr.net/npm/lokalized@1.0.0/dist/browser/lokalized.js";

// Keep the translations inline and start with English as the fallback
let locale = "en";
const strings = createStrings({
  localizedStringSupplier: () => ({
    en: { Welcome: "Hello, {{name}}!" },
    fr: { Welcome: "Bonjour, {{name}}!" },
  }),
  fallbackLocale: "en",
  localeSupplier: () => locale,
});

// Match the browser's preferred languages against the loaded translations
locale = chooseBrowserLocale(strings.getLocaleConfiguration());
// Mark the page's language and render the greeting as text
document.documentElement.lang = locale;
document.querySelector("#greeting").textContent = strings.get("Welcome", { name: "Ada" });

localizedStringSupplier provides the translations once when createStrings builds the instance. localeSupplier provides the current language each time a translation is requested.

The browser's preferred languages choose among the loaded translations, with English as the fallback. The result is inserted as text; use your application's normal rendering and escaping rules for translated output.

Browser: using a bundler

Install with npm as shown above. Use the same greeting.js, replacing only its CDN import with a package import:

import { createStrings, chooseBrowserLocale } from "lokalized";

Your bundler resolves the import for the browser. The rest of the example, including locale selection, stays the same.

Node.js and server applications

Use the file-backed walkthrough below. It loads a directory with lokalized/node and resolves the locale for each request. Browser code cannot import lokalized/node.

Some later feature examples use lokalized/node to supply translations. In a browser, supply inline translations as above or fetch published localized strings files; the translation API then works the same way.

Do Zero-Dependency Libraries Interest You?

Similarly-flavored commercially-friendly OSS libraries are available.

Swift: Choose Your Application Setup

Use the same JSON files on iOS and macOS. The library uses Apple system frameworks and supports Swift 6.2+, iOS 15+, and macOS 12+. Linux, watchOS, and tvOS are not currently supported platforms.

Create one Strings instance at startup and reuse it app-wide. Loading reads the files, and constructing DefaultStrings validates and compiles the translations. Retain the resulting any Strings in your app's dependency container or application state, and pass it to views and services. Keep loading and construction out of view bodies, view initializers, and per-translation helpers.

iOS and macOS apps: bundled localized strings files

Add a Lokalized folder with en.json and fr.json to Copy Bundle Resources and preserve its folder structure. These are ordinary JSON resources; load all the files together and let Lokalized choose the locale. Apple .lproj selection and .xcstrings resources are separate systems.

en.json Localized Strings File (English)
{
  "welcome": "Hello, {{name}}!"
}
fr.json Localized Strings File (French)
{
  "welcome": "Bonjour, {{name}}!"
}
import Foundation
import Lokalized

// Copy Lokalized/en.json and Lokalized/fr.json into the app bundle.
// Supply a getter that reads the app's current language settings.
func loadAppStrings(
  currentLocale: @escaping @Sendable () throws -> LocaleTag
) throws -> any Strings {
  let fallbackLocale = try LocaleTag("en")
  let files = try LocalizedStringLoader.loadFromBundle(.main)
  let catalogs = files.mapValues { file in
    LocalizedCatalog(strings: file.strings)
  }
  return try DefaultStrings(configuration: StringsConfiguration(
    localizedStringSupplier: { catalogs },
    localeSupplier: { _ in try currentLocale() },
    fallbackLocale: fallbackLocale
  ))
}

The factory returns any Strings, the public translation and locale-matching protocol, while constructing a DefaultStrings implementation. App consumers such as the SwiftUI view below can accept the protocol. Its values are Sendable.

Call loadAppStrings once during app startup and retain its result. This example puts it in an AppServices container owned by the app; appSettings is your existing thread-safe language settings object:

import Foundation
import Lokalized

// Store this container in your app's existing application state.
struct AppServices: Sendable {
  let strings: any Strings
}

// Run once at app startup. appSettings is your thread-safe language settings object.
let services = AppServices(
  strings: try loadAppStrings(currentLocale: { appSettings.currentLocale })
)

// Views and services all use services.strings for subsequent lookups.
let message = try services.strings.get("welcome", placeholders: ["name": .text("Ada")])

Pass services.strings to every view or service that needs translations. Changing the selected language uses this same instance through its locale supplier or per-call locale options. Construct a replacement only when the localized strings files or construction settings change, then let your app replace the stored instance. If startup loading throws, handle that startup failure before presenting UI that depends on translations.

Pass a getter for your app's language settings as currentLocale. The localeSupplier reads it on each lookup without a per-call override, so language changes use the same loaded translations. English is the fallback language, independently of the app's selected language. The getter must be synchronous and safe for concurrent calls.

For ordered Apple language preferences, configure localeMatchSupplier: { matcher in try PreferredLanguageChooser.chooseAppleLocale(using: matcher) } instead of localeSupplier. This samples the preferences on each lookup and retains the negotiation result.

SwiftPM libraries and tools: owned resources

Place the files in Sources/MyApp/Lokalized and declare .copy("Lokalized"), as in the installation example. Access Bundle.module from the target that owns those resources. For a framework, pass its resource bundle explicitly.

import Foundation
import Lokalized

// This code belongs to the SwiftPM target that owns the copied resources.
let en = try LocaleTag("en")
// Construct once during startup and retain strings for subsequent lookups.
let files = try LocalizedStringLoader.loadFromBundle(.module)
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
let strings = try DefaultStrings(configuration: StringsConfiguration(
  localizedStringSupplier: { catalogs }, localeSupplier: { _ in en }, fallbackLocale: en
))
let message = try strings.get("welcome", placeholders: ["name": .text("Ada")])
assert(message == "Hello, Ada!")

Retain strings in the app's shared services or the tool's startup state and reuse it for every lookup. Load and compile the resources once.

macOS command-line tools and services: local files

Load locale-named files from a caller-chosen directory. This example assumes strings/en.json contains {"welcome":"Hello, {{name}}!"} and strings/fr.json contains {"welcome":"Bonjour, {{name}}!"}. Preferred languages are explicit inputs, so the tool need not use the host's language.

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
))

// For a command-line tool, the caller chooses where localized strings files live.
let match = try PreferredLanguageChooser.chooseLocaleForPreferredLanguages(
  ["fr-CA", "en"], using: strings)
let message = try strings.get("welcome", placeholders: ["name": .text("Ada")],
  options: .forLocaleMatch(match))
assert(message == "Bonjour, Ada!")

Loading and lookups are synchronous and can throw. Load once during startup or on an appropriate worker, retain the immutable Strings instance, and reuse it across the tool's operations or the service's requests. If your app obtains JSON from a remote service, acquire it yourself and pass its Data or String to the parser.

SwiftUI: use the app's language settings

Supply the app-wide instance to the view with GreetingView(strings: services.strings). SwiftUI can recreate view values and evaluate body repeatedly; the Strings instance remains owned by your app's AppServices container. Ordinary lookups use the current language setting automatically. Have your app's observable language state trigger view updates when that setting changes; the supplier alone does not redraw SwiftUI views. Handle thrown errors explicitly at the UI boundary; this example chooses an ellipsis when a lookup throws.

import SwiftUI
import Lokalized

struct GreetingView: View {
  let strings: any Strings

  var body: some View {
    Text(verbatim: greeting())
  }

  private func greeting() -> String {
    do {
      return try strings.get("welcome", placeholders: ["name": .text("Ada")])
    } catch {
      // The app chooses its error presentation at the UI boundary.
      return "..."
    }
  }
}

Text(verbatim:) displays the resolved string without treating it as an Apple localization key. UIKit and AppKit can assign the same result to a label's text or string value.

For settings isolated to the main actor, read the selected LocaleTag on that actor and pass options: .forLocale(locale) for the lookup. A shared @Sendable supplier needs a getter that can be called safely from any thread.

Getting Started

For Android, use the asset-loading example below and the Android Gradle configuration.

This walkthrough reads localized strings files from disk in Node.js. For a browser-only app, begin with the browser example above; you can later fetch published files when inline translations no longer suit your app.

1. Create Localized Strings Files

Filenames must be IETF BCP 47 language tags, optionally suffixed by .json.

pt-BR.json Localized Strings File (Brazilian Portuguese)
{
  "I read {{bookCount}} books.": {
    "translation": "Li {{bookCount}} {{books}}.",
    "placeholders": {
      "books": {
        "value": "bookCount",
        "translations": {
          "CARDINALITY_ONE": "livro",
          "CARDINALITY_OTHER": "livros"
        }
      }
    },
    "alternatives": [
      {
        "bookCount == 0": "Não li nenhum livro."
      }
    ]
  }
}

2. Create a Strings Instance

JavaScript

On a Node.js server, choose a fallback locale, load localized strings from disk, and give the instance a resolver: a function it asks for the language of each lookup. A single frozen Strings instance can serve every request the server handles.

Java

Choose a fallback locale, provide localized strings, and resolve the caller's locale. A single immutable Strings instance can serve concurrent and multitenant applications.

Swift

Choose a fallback locale, load local JSON files, and construct one immutable DefaultStrings instance during startup. Retain it as any Strings in your app's shared services and reuse it for every lookup. Its @Sendable suppliers can serve concurrent callers; changing the selected language does not require a new instance. The example below uses a macOS directory; an iOS or macOS app uses loadFromBundle(.main), and a SwiftPM resource-owning target uses loadFromBundle(.module). See the app-wide ownership example above.

JavaScript

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

// Load localized strings files from the application directory
const { catalogs } = readStringsFromDirectory("my-directory");

const strings = createStrings({
  localizedStringSupplier: () => (catalogs),
  // The locale to use when no loaded locale matches
  fallbackLocale: "pt-BR",
  // Asked on every lookup: the language of the request being served
  localeSupplier: () => currentRequest().language,
});

Java

final Locale FALLBACK_LOCALE = Locale.forLanguageTag("pt-BR");

// Start the builder with the locale to use when no loaded locale matches
Strings strings = Strings.withFallbackLocale(FALLBACK_LOCALE)
  // Load localized strings files from the application directory
  .localizedStringSupplier(() -> LocalizedStringLoader.loadFromFilesystem(Paths.get("my-directory")))
  // Match the current web request's locale to a loaded file
  .localeSupplier((matcher) -> {
    Locale locale = MyWebContext.getHttpServletRequest().getLocale();
    return matcher.bestMatchFor(locale);
  })
  // Validate the configuration and create an immutable, thread-safe instance
  .build();

Swift

import Foundation
import Lokalized

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

Curious about failed lookup handling or immutable reloads? Check out the Cookbook.

3. Ask the Strings Instance for Translations

Raw values drive the language rule. Lokalized selects the matching form or alternative and then interpolates the supplied values.

JavaScript

strings.get("I read {{bookCount}} books.", { bookCount: 3 });   // => "Li 3 livros."
strings.get("I read {{bookCount}} books.", { bookCount: 1 });   // => "Li 1 livro."
strings.get("I read {{bookCount}} books.", { bookCount: 0 });   // => "Não li nenhum livro."

Java

String message = strings.get("I read {{bookCount}} books.",
  Map.of("bookCount", 3));

assertEquals("Li 3 livros.", message);

message = strings.get("I read {{bookCount}} books.",
  Map.of("bookCount", 1));

assertEquals("Li 1 livro.", message);

message = strings.get("I read {{bookCount}} books.",
  Map.of("bookCount", 0));

assertEquals("Não li nenhum livro.", message);

Swift

let threeBooks = try strings.get("I read {{bookCount}} books.", placeholders: ["bookCount": .integer(3)])
assert(threeBooks == "Li 3 livros.")
let oneBook = try strings.get("I read {{bookCount}} books.", placeholders: ["bookCount": .integer(1)])
assert(oneBook == "Li 1 livro.")
let noBooks = try strings.get("I read {{bookCount}} books.", placeholders: ["bookCount": .integer(0)])
assert(noBooks == "Não li nenhum livro.")

Formatting Placeholder Values

JavaScript

Lokalized selects translations and interpolates values; it does not format dates, times, numbers, percentages, or currencies. Use Intl.NumberFormat, Intl.DateTimeFormat, and the other Intl formatters.

Java

Lokalized selects translations and interpolates values; it does not format dates, times, numbers, percentages, or currencies. Use NumberFormat, DateTimeFormatter, and other JDK formatters.

Swift

Lokalized selects translations and interpolates values; it does not format dates, times, numbers, percentages, or currencies. Use Foundation format styles or NumberFormatter and DateFormatter. Pass the raw number for language-form selection and formatted text for display:

When one number affects language selection and also needs formatted display, pass the raw and formatted values separately:

en-US.json Localized Strings File (American English)
{
  "You have {{formattedCount}} items.": {
    "translation": "You have {{formattedCount}} {{items}}.",
    "placeholders": {
      "items": {
        "value": "count",
        "translations": {
          "CARDINALITY_ONE": "item",
          "CARDINALITY_OTHER": "items"
        }
      }
    }
  }
}

Usage and expected results

JavaScript

const count = 12345;
const message = strings.get("You have {{formattedCount}} items.", {
  count,
  formattedCount: new Intl.NumberFormat("en-US").format(count),
});

message;   // => "You have 12,345 items."

Java

int count = 12_345;
Locale locale = Locale.forLanguageTag("en-US");

// Produces "You have 12,345 items."
String message = strings.get("You have {{formattedCount}} items.", Map.of(
  "count", count,
  "formattedCount", NumberFormat.getIntegerInstance(locale).format(count)
));

Swift

import Foundation
import Lokalized

let locale = try LocaleTag("en-US")
// 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
))

let count: Int32 = 12345
let displayLocale = Foundation.Locale(identifier: "en_US")
let formattedCount = count.formatted(.number.locale(displayLocale))
let message = try strings.get("You have {{formattedCount}} items.", placeholders: [
  "count": .integer(count), "formattedCount": .text(formattedCount)
])
assert(message == "You have 12,345 items.")

4. Ensure Determinism via Tiebreakers

If both en-US and en-GB are loaded and a caller requests Canadian English (en-CA), neither an exact match nor a CLDR parent locale is present. Both files are compatible en-Latn candidates, so the configured order decides which translation wins:

JavaScript

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

const strings = createStrings({
  localizedStringSupplier: () => (readStringsFromDirectory("my-directory").catalogs),
  fallbackLocale: "en-US",
  localeSupplier: () => currentRequest().language,
  // Prefer US English to British English when both are equally good matches
  tiebreakerLocalesByLanguageCode: { en: ["en-US", "en-GB"] },
});

Java

Strings strings = Strings.withFallbackLocale(FALLBACK_LOCALE)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromFilesystem(Paths.get("my-directory")))
  .localeSupplier(matcher -> matcher.bestMatchFor(
    MyWebContext.getHttpServletRequest().getLocale()))
  .tiebreakerLocalesByLanguageCode(Map.of(
    // Prefer US English to British English when both are equally good matches
    "en", List.of(
      Locale.forLanguageTag("en-US"),
      Locale.forLanguageTag("en-GB")
    )
  ))
  .build();

Swift

import Foundation
import Lokalized

let us = try LocaleTag("en-US")
let gb = try LocaleTag("en-GB")
let requested = try LocaleTag("en-CA")
let files = try LocalizedStringLoader.loadFromDirectory(URL(fileURLWithPath: "strings"))
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
let strings = try DefaultStrings(configuration: StringsConfiguration(
  localizedStringSupplier: { catalogs }, localeSupplier: { _ in requested },
  fallbackLocale: us, tiebreakerLocalesByLanguageCode: ["en": [us, gb]]
))
let matches5 = try strings.get("Color") == "color"
assert(matches5)

JavaScript

The tiebreakerLocalesByLanguageCode keys are primary BCP 47 language subtags. Each list must contain every loaded locale for that language exactly once; canonical aliases such as he and iw cannot be configured as separate keys.

Lokalized fails fast. Lokalized detects multiple loaded locales with the same primary language. createStrings throws a RangeError that names the language unless each ambiguous language has a tiebreaker list containing every loaded locale exactly once.

Java

The tiebreakerLocalesByLanguageCode(...) map keys are primary BCP 47 language subtags. Each list must contain every loaded locale for that language exactly once; canonical aliases such as he and iw cannot be configured as separate keys.

Lokalized fails fast. Lokalized detects multiple loaded locales with the same primary language. If an application constructs Strings during startup, build() throws IllegalArgumentException unless each ambiguous language has a tiebreaker list containing every loaded locale exactly once.

Swift

The tiebreakerLocalesByLanguageCode keys are primary BCP 47 language subtags. Each list contains every loaded locale for that language exactly once; canonical aliases cannot be separate keys.

Lokalized fails fast. DefaultStrings(configuration:) throws ConfigurationError when an ambiguous language lacks a complete tiebreaker list.

5. Respect User Language Preferences

JavaScript

In a browser, chooseBrowserLocale(...) matches the ordered navigator.languages list against the loaded locales, returning the configured fallback when none match. With a plain script tag, call lokalized.chooseBrowserLocale(strings.getLocaleConfiguration()); with ES modules, import chooseBrowserLocale from lokalized. Use the browser's choice as the initial language when your app has no saved language preference, then let the app's language settings control later lookups.

On a server, an Accept-Language value such as en-GB;q=1.0,en;q=0.75,fr-FR;q=0.25 describes a weighted preference list. Combine repeated field lines in received order, then pass the value to forAcceptLanguage(...) from lokalized/negotiate to compare it with the loaded locales. A malformed header never throws: it falls back. The example below uses a server request:

Java

An Accept-Language value such as en-GB;q=1.0,en;q=0.75,fr-FR;q=0.25 describes a weighted preference list. Combine repeated field lines in received order, then pass the value to bestMatchForAcceptLanguage(...) to compare it with the loaded locales:

Swift

On iOS and macOS, explicitly sample ordered preferences with PreferredLanguageChooser.chooseAppleLocale(using:), or pass an app-owned list to chooseLocaleForPreferredLanguages(_:using:). A macOS service can also process an Accept-Language value with TranslationOptions.forAcceptLanguage(_:using:). Combine repeated header values in received order. This helper falls back for unusable input; the HTTP layer belongs to your application.

JavaScript

import { createStrings } from "lokalized";
import { createLocaleMatcher, forAcceptLanguage } from "lokalized/negotiate";
import { readStringsFromDirectory } from "lokalized/node";

const { catalogs } = readStringsFromDirectory("my-directory");
const negotiator = createLocaleMatcher({ supportedLocales: Object.keys(catalogs), fallbackLocale: "en" });

const strings = createStrings({
  localizedStringSupplier: () => (catalogs),
  fallbackLocale: "en",
  // Match the combined Accept-Language header of the request being served
  localeMatchSupplier: () =>
    forAcceptLanguage(negotiator, currentRequest().headers["accept-language"]).localeMatchResult,
});

Java

Strings strings = Strings.withFallbackLocale(FALLBACK_LOCALE)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromFilesystem(Paths.get("my-directory")))
  .localeSupplier(matcher -> matcher.bestMatchForAcceptLanguage(
    MyWebContext.getCombinedAcceptLanguageHeader()))
  .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
))

// Apple apps: an ordered list sampled by the caller, or chooseAppleLocale(using:).
let appleMatch = try PreferredLanguageChooser.chooseLocaleForPreferredLanguages(
  ["fr-CA", "en"], using: strings)
let matches6 = try strings.get("Hello", options: .forLocaleMatch(appleMatch)) == "Bonjour"
assert(matches6)

// A macOS service can process a header its own HTTP layer supplied.
let options = try TranslationOptions.forAcceptLanguage("fr-CA, en;q=0.8", using: strings)
let matches7 = try strings.get("Hello", options: options) == "Bonjour"
assert(matches7)

Locale Matching Behavior

Locale matching is deterministic. It applies these rules in order:

  • Exact language tags, then CLDR-canonical equivalents and legacy aliases.
  • CLDR parent locales before looser language-only matches - for example, en-AU can prefer en-001 before en.
  • Likely-script matching, so zh-TW can match zh-Hant while sr-Latn remains distinct from sr-Cyrl.
  • A compatibility bridge between Norwegian no and Bokmål nb, after exact matches.
  • Configured tiebreakers when multiple loaded locales still share the requested language.
  • Language-range quality weights, including q=0 exclusions.
  • The configured fallback for the root locale (und), wildcards, empty preferences, or unmatched requests.

The examples below assume that no exact file for the requested language tag is loaded unless the row says otherwise. An exact loaded file always wins.

Script-aware matching

Script-aware locale matching examples
Request CLDR interpretation Preferred compatible file
zh-TWTraditional Chinesezh-Hant
zh-HKTraditional Chinesezh-Hant
zh-CNSimplified Chinesezh-Hans
zhSimplified Chinese by defaultzh-Hans
srCyrillic Serbian by defaultsr-Cyrl
sr-LatnLatin Serbiansr-Latn
shLegacy tag associated with Latin Serbiansr-Latn

Parent locales and tiebreakers

Parent locale and tiebreaker examples
Request Loaded files Result
en-AUen-001, enen-001
en-AUenen
en-CAen-US, en-GBFirst configured English tiebreaker
fr-BEfr-FR, fr-CAFirst configured French tiebreaker

Norwegian compatibility bridge

Norwegian locale compatibility examples
Request Candidate order
no-NOno-NO → no → nb-NO → nb
nb-NOnb-NO → nb → no → no-NO

Norwegian Nynorsk (nn) is independent and does not participate in this bridge.

IANA language-range equivalents

Parsing a header adds the ranges the IANA Language Subtag Registry declares equivalent, so a request for iw also considers he. By default Lokalized takes these equivalents from the registry snapshot it bundles (File-Date: 2026-09-17), so a request for yol also considers enm wherever Lokalized runs.

JavaScript

The JavaScript package always uses this snapshot. When your application parses a header itself, use parseLanguageRanges(...) from lokalized/negotiate, which has the same grammar, result order, and errors as the Java library.

The forAcceptLanguage(...) helper and a negotiator's bestMatchForAcceptLanguage(...) bound raw input before parsing and fall back for unusable values without truncating preferences. The parsed-list methods remain strict and accept at most 32 ranges; that count applies to the parsed list, which may hold IANA-equivalent ranges beyond those written in the header.

Java

The JDK's LanguageRange.parse(...) uses the table bundled in the running JDK instead, which differs between JDK releases. When your application parses a header itself, use strings.parseLanguageRanges(...), which has the same grammar, result order, and exceptions.

To keep Lokalized 3.0.0's behavior of using the running JDK's table, configure languageRangeEquivalents(...) with LanguageRangeEquivalents.JDK. The setting governs parseLanguageRanges(...), bestMatchForAcceptLanguage(...), and the equivalents matching recognizes for each requested range; the default is LanguageRangeEquivalents.IANA_REGISTRY.

The bestMatchForAcceptLanguage(...) method bounds raw input before parsing and falls back for unusable values without truncating preferences. The parsed-list APIs remain strict and accept at most 32 ranges; that count applies to the parsed list, which may hold IANA-equivalent ranges beyond those written in the header.

Swift

The Swift port uses the bundled IANA registry snapshot. Use strings.parseLanguageRanges(...) for strict parsing, or TranslationOptions.forAcceptLanguage(_:using:) for bounded input with fallback for unusable headers. The strict parsed-list APIs accept at most 32 ranges, including generated IANA equivalents.

Loading Localized Strings

JavaScript

Node.js can read localized strings files from a directory; the browser can fetch published files through a manifest. The directory loader scans only the directory it is given and does not recurse into children.

Java

Use the filesystem during development and a namespaced classpath package in packaged applications. Both loaders scan only the requested directory or package; they do not recurse into children.

Swift

Load immediate files from a local directory for macOS tools, or a named resource folder in an iOS/macOS app bundle. Both paths load the same locale-named JSON format. SwiftPM targets that own copied resources pass .module.

JavaScript

Node.js: read files from disk

import { readStringsFromDirectory } from "lokalized/node";

// In Node: read every localized strings file in one directory
const { catalogs, warnings } = readStringsFromDirectory("strings");

Browser: fetch published files

When inline translations grow, move them into localized strings files. At publish time in Node.js, generate a manifest and upload it alongside the JSON files. The browser fetches that manifest and only the files needed for its locale:

import { writeFileSync } from "node:fs";
import { createStringsManifestFromDirectory } from "lokalized/node";

// At publish time, in Node: record each file's URL and the digest of its bytes
const publishedManifest = await createStringsManifestFromDirectory("strings", {
  catalogVersion: "2026-09-27",
  fallbackLocale: "en",
  publicationBaseUrl: "https://cdn.example.com/strings/",
});
writeFileSync("strings/manifest.json", JSON.stringify(publishedManifest));
// Publish strings/manifest.json with strings/en.json and strings/fr.json
import { createStrings, chooseBrowserLocale } from "lokalized";
import { loadStrings, localeConfigurationForManifest, parseStringsManifest } from "lokalized/load";

// In a browser or edge worker: fetch the manifest from your trusted origin
const response = await fetch("https://cdn.example.com/strings/manifest.json");
if (!response.ok) throw new Error("Could not load the strings manifest");
const manifest = parseStringsManifest(await response.text());
const locale = chooseBrowserLocale(localeConfigurationForManifest(manifest));

// Fetch only the files this locale needs, each checked against its digest
const loaded = await loadStrings(manifest, locale);
const strings = createStrings({ loaded, localeSupplier: () => locale });

strings.get("Hello");   // => "Bonjour"

Java

// Load editable localized strings from a filesystem directory
Strings filesystemStrings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromFilesystem(Paths.get("strings")))
  .localeSupplier(matcher -> matcher.bestMatchFor(Locale.US))
  .build();

// Load packaged localized strings from a classpath package
Strings classpathStrings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromClasspath("com/example/myapp/strings"))
  .localeSupplier(matcher -> matcher.bestMatchFor(Locale.US))
  .build();

Swift

import Foundation
import Lokalized

// macOS tools: load the immediate files in a local directory.
let files = try LocalizedStringLoader.loadFromDirectory(
  URL(fileURLWithPath: "strings", isDirectory: true))
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }

// iOS/macOS apps: use .main; SwiftPM resource owners: use .module.
// Pass an explicit Bundle for a framework's resources.
let bundledFiles = try LocalizedStringLoader.loadFromBundle(.module)

JavaScript

  • The manifest is generated once, at publish time, and lists every file, the URL it is published at, and the SHA-256 digest of its bytes. A file whose bytes do not match is refused.
  • The manifest is what the digests are checked against, so serve it from an origin you control.
  • A load reads the requested locale's fallback chain and nothing else, and hands back a record that createStrings takes as loaded.
  • Browser network loading needs WebCrypto: use HTTPS or localhost. If the files are on another origin, that origin must allow cross-origin requests. An edge worker can use the same loader with a locale selected from its request instead of chooseBrowserLocale(...).
  • The und.json file is the localized strings filename for the root locale.
  • Files use strict UTF-8. Blank or BOM-only files are invalid; use {} for an intentionally empty file.

Java

  • Use slash-separated classpath names such as com/example/myapp/strings.
  • Use an explicit ClassLoader overload in containers, plugin systems, and test harnesses.
  • Leave exhaustiveClasspathSearch disabled unless a JAR omits package directory entries; enabling it scans every visible filesystem and JAR classpath root.
  • The META-INF/versions directory is reserved from package discovery; use loadFromClasspathResources(...) when an application intentionally needs an exact resource beneath it.
  • The und.json file is the localized strings filename for Locale.ROOT.
  • Input streams use strict UTF-8. Blank or BOM-only files are invalid; use {} for an intentionally empty file.
  • Classpath discovery skips unrelated .json resources with invalid locale filenames and warns; filesystem loading remains strict.

Swift

  • Preserve a Lokalized folder containing en.json, fr.json, and other locale-named files. SwiftPM uses .copy("Lokalized"); Xcode apps preserve the folder in Copy Bundle Resources.
  • Pass Bundle.main for an app, Bundle.module from the SwiftPM target that owns resources, or an explicit framework bundle.
  • Use resourcePathsByLocale with loadFromBundle, or loadFromResources with local file URLs, for explicit resource mapping.
  • Parse caller-owned String, Data, or InputStream values when your application supplies localized strings contents. Lokalized does not download them.

Default loading limits

Default localized strings loading limits
ResourceDefault
One file or stream8 MiB
One text input8,388,608 UTF-16 code units
JSON nesting64 levels; hard maximum 128
Aggregate input32 MiB
Localized strings files256
Translation nodes100,000
Warnings1,000
Discovery entries, when scanning a directory or package100,000; hard maximum 1,000,000

JavaScript

import { readStringsFromDirectory } from "lokalized/node";

// Tighten the resource limits enforced while parsing localized strings
const { catalogs } = readStringsFromDirectory("strings", {
  limits: {
    maximumInputBytes: 4 * 1024 * 1024,
    maximumReaderCharacters: 4 * 1024 * 1024,
    maximumTotalInputBytes: 16 * 1024 * 1024,
    maximumLocalizedStringsFiles: 100,
    maximumTranslationNodes: 25_000,
    maximumWarnings: 500,
    maximumJsonNestingDepth: 32,
  },
});

Java

// Customize the resource limits enforced while parsing localized strings
LocalizedStringLoadingOptions limits = LocalizedStringLoadingOptions.builder()
  .maximumInputBytes(4 * 1024 * 1024)
  .maximumReaderCharacters(4 * 1024 * 1024)
  .maximumTotalInputBytes(16L * 1024L * 1024L)
  .maximumLocalizedStringsFiles(100)
  .maximumTranslationNodes(25_000)
  .maximumWarnings(500)
  .maximumJsonNestingDepth(32)
  .build();

Map<Locale, Set<LocalizedString>> localizedStringsByLocale =
  LocalizedStringLoader.loadFromClasspath("strings", limits);

Swift

import Foundation
import Lokalized

let limits = try LocalizedStringLoadingOptions(
  maximumInputBytes: 4 * 1024 * 1024,
  maximumReaderCharacters: 4 * 1024 * 1024,
  maximumJsonNestingDepth: 32,
  maximumTotalInputBytes: 16 * 1024 * 1024,
  maximumLocalizedStringsFiles: 100,
  maximumTranslationNodes: 25_000,
  maximumWarnings: 500
)
let files = try LocalizedStringLoader.loadFromDirectory(
  URL(fileURLWithPath: "strings"), loadingOptions: limits)
Explicit resources for constrained classloaders

Some plugin classloaders can open known resources but cannot enumerate their package. Map locales to exact resource paths in those environments:

Explicit locale-to-resource maps and single-resource parsing enumerate no candidates and therefore do not consume the discovery-entry budget.

Map<Locale, Set<LocalizedString>> localizedStringsByLocale =
  LocalizedStringLoader.loadFromClasspathResources(
    pluginClassLoader,
    Map.of(
      Locale.ROOT, "myapp/strings/und.json",
      Locale.ENGLISH, "myapp/strings/en.json",
      Locale.FRENCH, "myapp/strings/fr.json"
    ),
    limits
  );

Android: Assets and App Locales

Use one localized strings file per locale for the whole app, such as src/main/assets/strings/en.json and fr.json. Load the files once, close each stream, and share a single immutable Strings instance across all screens. Load and validate large localized strings files off the UI thread.

Use the app-wide locale

For an app that uses one language across its screens, keep the current locale in a shared AtomicReference. Configure localeSupplier(...) to read it on each lookup. Initialize these objects once and keep both in the application's shared state:

// Initialize shared app state once; context is a configured UI Context.
Map<Locale, Set<LocalizedString>> localizedStringsByLocale = new LinkedHashMap<>();
for (String tag : new String[] { "en", "fr" }) {
  String path = "strings/" + tag + ".json";
  Locale locale = Locale.forLanguageTag(tag);
  try (InputStream stream = context.getAssets().open(path)) {
    localizedStringsByLocale.put(locale, LocalizedStringLoader.parse(stream, locale, path));
  }
}

AtomicReference<Locale> currentLocale = new AtomicReference<>(
  context.getResources().getConfiguration().getLocales().get(0));

Strings strings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> localizedStringsByLocale)
  .localeSupplier(matcher -> matcher.bestMatchFor(currentLocale.get()))
  .build();

Initialize the locale from a configured UI context, as above. Refresh the same holder from the current Activity's UI context after onCreate(...) calls its superclass, including when the activity is recreated. If the activity handles configuration changes itself, also refresh after onConfigurationChanged(...) calls its superclass:

// context is the current Activity's configured UI Context.
currentLocale.set(context.getResources().getConfiguration().getLocales().get(0));

Ordinary lookups now use the app's current locale automatically. The holder shares updates with UI and background threads, and the translations remain loaded:

String message = strings.get("I read {{bookCount}} books.",
  Collections.singletonMap("bookCount", 3));

Override the locale for one lookup

For fine-grained control, use TranslationOptions.forLocale(...) to choose a locale for an individual call. This example requests French without changing the shared app locale:

String message = strings.get("I read {{bookCount}} books.",
  Collections.singletonMap("bookCount", 3), TranslationOptions.forLocale(Locale.FRENCH));

For placeholder values on API 26–29, use Collections.singletonMap(...) or a regular map. The Map.of(...), List.of(...), and Set.of(...) calls in other Java examples require API 30 or later.

Resources.openRawResource(...) can supply the same parser with an input stream. Use explicit locale-to-resource mappings for raw resources. Classpath directory and JAR discovery are desktop loading mechanisms; use Android's resource APIs for packaged translations. Use Android/Java formatters for dates and numbers, and portable ASCII placeholder names when localized strings files must work across device Unicode versions.

Per-Invocation Options

JavaScript

A resolver that reads the current request is convenient for web applications. Async work, tests, batch jobs, and administrative tools can override one call with a TranslationOptions object, the third argument to get:

Java

Request-scoped locale suppliers are convenient for web applications. Async work, tests, batch jobs, and administrative tools can override one call with TranslationOptions:

Swift

Pass an app-owned language setting for each lookup with TranslationOptions.forLocale(...). The same immutable instance can serve different users or windows. Read actor-isolated state on its actor and pass the resulting value into the lookup:

JavaScript

// A per-call locale overrides the instance's resolver for this one lookup
const message = strings.get("I read {{bookCount}} books.", { bookCount: 1 }, { locale: "fr-CA" });

message;   // => "J'ai lu 1 livre."

Java

// With a matching fr-CA translation, this might return "J'ai lu 1 livre."
String message = strings.get(
  "I read {{bookCount}} books.",
  Map.of("bookCount", 1),
  TranslationOptions.forLocale(Locale.forLanguageTag("fr-CA"))
);

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
))

let french = try TranslationOptions.forLocale("fr-CA")
let message = try strings.get("I read {{bookCount}} books.",
  placeholders: ["bookCount": .integer(1)], options: french)
assert(message == "J'ai lu 1 livre.")

JavaScript

Per-call options can provide a locale or a localeMatchResult, bidi isolation behavior, a fallback policy, a failure handler, or a fallback observer. Using bidiIsolation: "always" protects caller-supplied right-to-left text inside left-to-right translations as well as the inverse case; "rtl-locales" remains the default. The same matching and tiebreaker rules still apply.

Java

Per-call options can provide a locale or language ranges, bidi isolation behavior, a fallback policy, a failure handler, or a fallback observer. Using BidiIsolation.ALWAYS protects caller-supplied right-to-left text inside left-to-right translations as well as the inverse case; RTL_LOCALES remains the default. The same matching and tiebreaker rules still apply.

Swift

Per-call options can provide a locale, language ranges, or an existing locale match; these three inputs are mutually exclusive. They can also override bidi isolation, fallback policy, failure handler, and fallback observer. Use .always for bidirectional text in either direction; .rtlLocales is the default and .disabled disables isolation.

A More Complex Example

Lokalized is most useful when a sentence must be rewritten across more than one grammatical dimension.

English

He was one of the 3 best baseball players.

She was the best baseball player.

This person was one of the 3 best baseball players.

Spanish

Fue uno de los 3 mejores jugadores de béisbol.

Ella era la mejor jugadora de béisbol.

Esta persona estaba entre las 3 personas que mejor jugaban al béisbol.

English Localized Strings File

en.json Localized Strings File (English)
{
  "{{heOrShe}} was one of the {{groupSize}} best baseball players.": {
    "translation": "{{heOrShe}} was one of the {{groupSize}} best baseball players.",
    "placeholders": {
      "heOrShe": {
        "value": "heOrShe",
        "translations": {
          "GENDER_MASCULINE": "He",
          "GENDER_FEMININE": "She",
          "GENDER_COMMON": "This person"
        }
      }
    },
    "alternatives": [
      {
        "groupSize <= 1": "{{heOrShe}} was the best baseball player."
      }
    ]
  }
}

Spanish Localized Strings File

es.json Localized Strings File (Spanish)
{
  "{{heOrShe}} was one of the {{groupSize}} best baseball players.": {
    "translation": "Fue {{uno}} de {{los}} {{groupSize}} mejores {{jugadores}} de béisbol.",
    "placeholders": {
      "uno": {
        "value": "heOrShe",
        "translations": {
          "GENDER_MASCULINE": "uno",
          "GENDER_FEMININE": "una"
        }
      },
      "los": {
        "value": "heOrShe",
        "translations": {
          "GENDER_MASCULINE": "los",
          "GENDER_FEMININE": "las"
        }
      },
      "jugadores": {
        "value": "heOrShe",
        "translations": {
          "GENDER_MASCULINE": "jugadores",
          "GENDER_FEMININE": "jugadoras"
        }
      }
    },
    "alternatives": [
      {
        "heOrShe == GENDER_COMMON && groupSize <= 1": "Esta persona era quien mejor jugaba al béisbol."
      },
      {
        "heOrShe == GENDER_COMMON": "Esta persona estaba entre las {{groupSize}} personas que mejor jugaban al béisbol."
      },
      {
        "heOrShe == GENDER_MASCULINE && groupSize <= 1": "Él era el mejor jugador de béisbol."
      },
      {
        "heOrShe == GENDER_FEMININE && groupSize <= 1": "Ella era la mejor jugadora de béisbol."
      }
    ]
  }
}

The Rules, Exercised

Application code supplies typed facts. The selected locale owns agreement and complete-phrase rewrites.

JavaScript

import { GENDER_COMMON, GENDER_FEMININE } from "lokalized";

const key = "{{heOrShe}} was one of the {{groupSize}} best baseball players.";

strings.get(key, { heOrShe: GENDER_FEMININE, groupSize: 3 });   // => "Fue una de las 3 mejores jugadoras de béisbol."
strings.get(key, { heOrShe: GENDER_COMMON, groupSize: 1 });     // => "Esta persona era quien mejor jugaba al béisbol."

Java

String message = strings.get(
  "{{heOrShe}} was one of the {{groupSize}} best baseball players.",
  Map.of("heOrShe", Gender.FEMININE, "groupSize", 3)
);

assertEquals("Fue una de las 3 mejores jugadoras de béisbol.", message);

message = strings.get(
  "{{heOrShe}} was one of the {{groupSize}} best baseball players.",
  Map.of("heOrShe", Gender.COMMON, "groupSize", 1)
);

assertEquals("Esta persona era quien mejor jugaba al béisbol.", message);

Swift

import Foundation
import Lokalized

let locale = try LocaleTag("es")
// 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
))

let key: ExactString = "{{heOrShe}} was one of the {{groupSize}} best baseball players."
let matches8 = try strings.get(key, placeholders: [
  "heOrShe": .languageForm(.gender(.feminine)), "groupSize": .integer(3)
]) == "Fue una de las 3 mejores jugadoras de béisbol."
assert(matches8)
let matches9 = try strings.get(key, placeholders: [
  "heOrShe": .languageForm(.gender(.common)), "groupSize": .integer(1)
]) == "Esta persona era quien mejor jugaba al béisbol."
assert(matches9)

Cardinality Ranges

A range such as 1-3 hours has a locale-specific form derived from its start and end cardinalities. CLDR's applicable English span mappings generally select OTHER; French can select ONE or OTHER. Unmapped pairs follow the library's documented end-category fallback.

fr.json Localized Strings File (French)
{
  "The meeting will be {{minHours}}-{{maxHours}} hours long.": {
    "translation": "La réunion aura une durée de {{minHours}} à {{maxHours}} {{heures}}.",
    "placeholders": {
      "heures": {
        "range": {
          "start": "minHours",
          "end": "maxHours"
        },
        "translations": {
          "CARDINALITY_ONE": "heure",
          "CARDINALITY_OTHER": "heures"
        }
      }
    }
  }
}

Usage and expected results

JavaScript

import { createStrings } from "lokalized";
import { cardinalRangeData } from "lokalized/data/ranges";

// Range rules are an optional import, so pages that never render a range skip the table
const strings = createStrings({
  localizedStringSupplier: () => ({ fr: frenchStrings }),
  fallbackLocale: "fr",
  localeSupplier: () => "fr",
  pluralData: { ranges: cardinalRangeData },
});

const key = "The meeting will be {{minHours}}-{{maxHours}} hours long.";

strings.get(key, { minHours: 1, maxHours: 3 });   // => "La réunion aura une durée de 1 à 3 heures."
strings.get(key, { minHours: 0, maxHours: 1 });   // => "La réunion aura une durée de 0 à 1 heure."

Java

String message = strings.get(
  "The meeting will be {{minHours}}-{{maxHours}} hours long.",
  Map.of("minHours", 1, "maxHours", 3)
);

assertEquals("La réunion aura une durée de 1 à 3 heures.", message);

message = strings.get(
  "The meeting will be {{minHours}}-{{maxHours}} hours long.",
  Map.of("minHours", 0, "maxHours", 1)
);

assertEquals("La réunion aura une durée de 0 à 1 heure.", message);

Swift

import Foundation
import Lokalized

let locale = try LocaleTag("fr")
// 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
))

let key: ExactString = "The meeting will be {{minHours}}-{{maxHours}} hours long."
let matches10 = try strings.get(key, placeholders: ["minHours": .integer(1), "maxHours": .integer(3)])
  == "La réunion aura une durée de 1 à 3 heures."
assert(matches10)
let matches11 = try strings.get(key, placeholders: ["minHours": .integer(0), "maxHours": .integer(1)])
  == "La réunion aura une durée de 0 à 1 heure."
assert(matches11)

Range rules are CLDR data, not a simple “use the ending number” heuristic. The language reference exposes each locale's mappings and provenance.

Ordinal Forms

Ordinal categories express ranks such as 1st, 2nd, and 3rd. English has four CLDR categories; Spanish has only ORDINALITY_OTHER, but application-specific alternatives can still model a first birthday or quinceañera.

en.json Localized Strings File (English)
{
  "{{hisOrHer}} {{year}}th birthday party is next week.": {
    "translation": "{{hisOrHer}} {{year}}{{ordinal}} birthday party is next week.",
    "placeholders": {
      "hisOrHer": {
        "value": "hisOrHer",
        "translations": {
          "GENDER_MASCULINE": "His",
          "GENDER_FEMININE": "Her"
        }
      },
      "ordinal": {
        "value": "year",
        "translations": {
          "ORDINALITY_ONE": "st",
          "ORDINALITY_TWO": "nd",
          "ORDINALITY_FEW": "rd",
          "ORDINALITY_OTHER": "th"
        }
      }
    }
  }
}

Usage and expected results

JavaScript

import { GENDER_FEMININE, GENDER_MASCULINE, createStrings } from "lokalized";
import { ordinalData } from "lokalized/data/ordinal";

// Ordinal rules are an optional import, like range rules
const strings = createStrings({
  localizedStringSupplier: () => ({ en: englishStrings }),
  fallbackLocale: "en",
  localeSupplier: () => "en",
  pluralData: { ordinal: ordinalData },
});

const key = "{{hisOrHer}} {{year}}th birthday party is next week.";

strings.get(key, { hisOrHer: GENDER_MASCULINE, year: 18 });   // => "His 18th birthday party is next week."
strings.get(key, { hisOrHer: GENDER_FEMININE, year: 21 });    // => "Her 21st birthday party is next week."

Java

String message = strings.get(
  "{{hisOrHer}} {{year}}th birthday party is next week.",
  Map.of("hisOrHer", Gender.MASCULINE, "year", 18)
);

assertEquals("His 18th birthday party is next week.", message);

message = strings.get(
  "{{hisOrHer}} {{year}}th birthday party is next week.",
  Map.of("hisOrHer", Gender.FEMININE, "year", 21)
);

assertEquals("Her 21st birthday party is next week.", message);

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
))

let key: ExactString = "{{hisOrHer}} {{year}}th birthday party is next week."
let matches12 = try strings.get(key, placeholders: ["hisOrHer": .languageForm(.gender(.masculine)), "year": .integer(18)])
  == "His 18th birthday party is next week."
assert(matches12)
let matches13 = try strings.get(key, placeholders: ["hisOrHer": .languageForm(.gender(.feminine)), "year": .integer(21)])
  == "Her 21st birthday party is next week."
assert(matches13)

Language Forms

Language forms are typed caller values used by localized strings to choose words or phrases. The supported families follow the README's order below.

Direct API Helpers

JavaScript

Applications normally pass raw values to strings.get(...), but the same CLDR-backed rules are available directly. Visible decimals are significant only when the input preserves them: a JavaScript number cannot hold 1.0 apart from 1, so pass decimal("1.0") or explicit pluralOperands(...) to keep the displayed scale. Ordinal and range helpers live with their optional data, in lokalized/data/ordinal and lokalized/data/ranges.

Java

Applications normally pass raw values to Strings, but the same CLDR-backed rules are available directly. Visible decimals are significant only when the input preserves them: an ordinary double value of 1.0 is normalized like 1, while BigDecimal("1.0") or explicit PluralOperands retains the displayed scale.

Swift

Pass raw NumericValue values or PluralOperands for CLDR selection. A floating-point value does not retain visible trailing zeros; NumericValue.forDecimal("1.0") does. All cardinal, ordinal, and range data is included in the Lokalized product.

JavaScript

import {
  PHONETIC_OTHER, PHONETIC_STRESSED_A,
  cardinalityForNumber, cardinalityForOperands, createStrings, decimal, pluralOperands,
} from "lokalized";
import { ordinalityForNumber } from "lokalized/data/ordinal";
import { readStringsFromDirectory } from "lokalized/node";

cardinalityForNumber(1, "en").name;   // => "CARDINALITY_ONE"

// A JavaScript number cannot carry "1.0"; decimal() keeps the visible decimal
cardinalityForNumber(decimal("1.0"), "en").name;   // => "CARDINALITY_OTHER"

// The same display intent can be supplied explicitly with pluralOperands()
const operands = pluralOperands("1", { visibleDecimalPlaces: 1 });
cardinalityForOperands(operands, "en").name;   // => "CARDINALITY_OTHER"

ordinalityForNumber(21, "en").name;   // => "ORDINALITY_ONE"

const spanishResolver = (term, locale) => {
  if (locale.split("-")[0] !== "es")
    return PHONETIC_OTHER;

  return ["acta", "arma", "hacha"].includes(term.toLowerCase())
    ? PHONETIC_STRESSED_A
    : PHONETIC_OTHER;
};

const strings = createStrings({
  localizedStringSupplier: () => (readStringsFromDirectory("strings").catalogs),
  fallbackLocale: "es",
  localeSupplier: () => "es",
  phoneticResolver: spanishResolver,
});

Java

Locale english = Locale.forLanguageTag("en");

Cardinality cardinality = Cardinality.forNumber(1, english);
assertEquals(Cardinality.ONE, cardinality);

// BigDecimal preserves the visible decimal in "1.0"
cardinality = Cardinality.forNumber(new BigDecimal("1.0"), english);
assertEquals(Cardinality.OTHER, cardinality);

// The same display intent can be supplied explicitly with PluralOperands
PluralOperands operands = PluralOperands.forNumber(1)
  .visibleDecimalPlaces(1)
  .build();
cardinality = Cardinality.forOperands(operands, english);
assertEquals(Cardinality.OTHER, cardinality);

Ordinality ordinality = Ordinality.forNumber(21, english);
assertEquals(Ordinality.ONE, ordinality);

Locale spanish = Locale.forLanguageTag("es");
PhoneticResolver spanishResolver = (term, locale) -> {
  if (!"es".equals(locale.getLanguage()))
    return Phonetic.OTHER;

  String normalized = term.toLowerCase(Locale.ROOT);
  return Set.of("acta", "arma", "hacha").contains(normalized)
    ? Phonetic.STRESSED_A
    : Phonetic.OTHER;
};

Strings strings = Strings.withFallbackLocale(spanish)
  .localizedStringSupplier(() -> LocalizedStringLoader.loadFromClasspath("strings"))
  .phoneticResolver(spanishResolver)
  .localeSupplier(matcher -> spanish)
  .build();

Swift

import Foundation
import Lokalized

let matches14 = try Cardinality.forNumber(.integer(1), locale: "en") == .one
assert(matches14)
// A Double cannot preserve visible decimal places; NumericValue can.
let decimal = try NumericValue.forDecimal("1.0")
let matches15 = try Cardinality.forNumber(decimal, locale: "en") == .other
assert(matches15)
let matches16 = try Ordinality.forNumber(.integer(21), locale: "en") == .one
assert(matches16)

let spanishResolver: PhoneticResolver = { term, locale in
  guard locale.tag == "es" else { return .other }
  return ["acta", "arma", "hacha"].contains(term.lowercased()) ? .stressedA : .other
}
let es = try LocaleTag("es")
let files = try LocalizedStringLoader.loadFromDirectory(URL(fileURLWithPath: "strings"))
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
let strings = try DefaultStrings(configuration: StringsConfiguration(
  localizedStringSupplier: { catalogs }, localeSupplier: { _ in es },
  fallbackLocale: es, phoneticResolver: spanishResolver
))
let matches17 = try spanishResolver("Arma", es) == .stressedA
assert(matches17)

Gender

Chooses agreement by grammatical gender. Common gender covers languages that merge masculine and feminine; neuter remains distinct.

GENDER_MASCULINE  // Masculine agreement
GENDER_FEMININE   // Feminine agreement
GENDER_COMMON     // Common masculine-feminine agreement
GENDER_NEUTER     // Neuter or unspecified agreement

Example: English selects He, She, or This person; Spanish may rewrite several agreeing words.

Grammatical Case

Changes nouns or pronouns according to syntactic role. The enum is intentionally broad, though not exhaustive for every language.

CASE_NOMINATIVE     // Citation or subject form
CASE_ACCUSATIVE     // Direct-object form
CASE_GENITIVE       // Possession, source, or partitive-adjacent form
CASE_DATIVE         // Indirect-object or recipient form
CASE_INSTRUMENTAL   // Instrument or accompaniment form
CASE_LOCATIVE       // Location or place form
CASE_PREPOSITIONAL  // Preposition-governed form
CASE_VOCATIVE       // Direct-address form
CASE_ABLATIVE       // Source, motion-away-from, or separation form

Russian: CASE_DATIVE can select Ивану in Отправить сообщение Ивану.

Definiteness

Distinguishes definite, indefinite, and construct or bound noun phrases.

DEFINITENESS_DEFINITE    // Definite reference, such as "the book"
DEFINITENESS_INDEFINITE  // Indefinite reference, such as "a book"
DEFINITENESS_CONSTRUCT   // Construct or bound-state form

Arabic: a definite document can select الكتاب, while the indefinite form selects كتابًا.

Classifiers

Selects measure words and counters. The categories are generic semantic buckets; language-specific inventories may need dedicated keys.

CLASSIFIER_GENERAL    // General-purpose classifier
CLASSIFIER_PERSON     // People or human referents
CLASSIFIER_ANIMAL     // Animals or living creatures
CLASSIFIER_LONG_THIN  // Long, thin, or cylindrical objects
CLASSIFIER_FLAT       // Flat, thin, or sheet-like objects
CLASSIFIER_BOUND      // Bound volumes such as books
CLASSIFIER_MACHINE    // Machines, devices, or large equipment
CLASSIFIER_VEHICLE    // Vehicles

Japanese: CLASSIFIER_BOUND selects the book counter 冊.

Formality

Selects casual, informal, formal, humble, or honorific register.

FORMALITY_CASUAL     // Casual register
FORMALITY_INFORMAL   // Informal register
FORMALITY_FORMAL     // Formal register
FORMALITY_HUMBLE     // Humble register
FORMALITY_HONORIFIC  // Honorific register

Example: one greeting key can produce Hey, Sam., Hello, Sam., or Greetings, Dr. Smith.

Clusivity

Distinguishes whether first-person plural includes or excludes the addressee.

CLUSIVITY_INCLUSIVE  // "We/us" includes the addressee
CLUSIVITY_EXCLUSIVE  // "We/us" excludes the addressee

Malay: kita includes the listener; kami excludes them.

Animacy

Distinguishes animate and inanimate referents when a language's agreement or case system requires it.

ANIMACY_ANIMATE    // Animate or sentient referent
ANIMACY_INANIMATE  // Inanimate referent

Russian: masculine accusative forms can differ for a brother (брата) and a table (стол).

Plural Cardinality

CLDR maps numeric operands - not just integers - to locale-specific plural categories. Category names do not necessarily mean only the number they name.

CARDINALITY_ZERO   // Locale-specific CLDR zero category
CARDINALITY_ONE    // Locale-specific CLDR one category
CARDINALITY_TWO    // Locale-specific CLDR two category
CARDINALITY_FEW    // Locale-specific CLDR few category
CARDINALITY_MANY   // Locale-specific CLDR many category
CARDINALITY_OTHER  // Catchall category for remaining values

Examples: Japanese uses OTHER for every number; English uses ONE/OTHER; Russian also uses FEW and MANY. Preserved visible decimals mean 1 and BigDecimal("1.0") can resolve differently.

Plural Cardinality Ranges

Combines the start and end categories through CLDR range mappings.

JavaScript

cardinalityForRange(start, end, locale), from lokalized/data/ranges

Swift

try Cardinality.forRange(start, end, locale: locale)

Examples: Every CLDR-listed English range pair resolves to OTHER, but the unlisted ONE-ONE pair follows Lokalized's end-category fallback and resolves to ONE. French ONE-ONE resolves to ONE; Latvian has distinct mappings across ZERO, ONE, and OTHER.

Phonetics

Allows an application-provided phonetic resolver to choose forms from the sound of a term, such as English a/an or Spanish stressed a.

PHONETIC_VOWEL       // Begins with a vowel sound
PHONETIC_CONSONANT   // Begins with a typical consonant sound
PHONETIC_H_SILENT    // Begins with a silent H
PHONETIC_H_ASPIRATED // Begins with a pronounced or aspirated H
PHONETIC_S_IMPURE    // Italian s + consonant onset
PHONETIC_Z           // Italian Z or /ts/, /dz/ onset
PHONETIC_GN          // Italian GN onset
PHONETIC_PS          // Italian PS onset
PHONETIC_PN          // Italian PN onset
PHONETIC_X           // Italian X or /ks/ onset
PHONETIC_GLIDE_Y     // Consonantal Y glide
PHONETIC_GLIDE_W     // Consonantal W glide
PHONETIC_STRESSED_A  // Stressed initial A or HA sound
PHONETIC_SOLAR       // Arabic sun-letter behavior
PHONETIC_LUNAR       // Arabic moon-letter behavior
PHONETIC_OTHER       // Fallback for other onset patterns

English: a resolver can select an honor and a gift from the same localized string.

Ordinals

CLDR maps numbers to rank categories. Like cardinalities, category names can cover many values.

ORDINALITY_ZERO   // Locale-specific CLDR zero category
ORDINALITY_ONE    // Locale-specific CLDR one category
ORDINALITY_TWO    // Locale-specific CLDR two category
ORDINALITY_FEW    // Locale-specific CLDR few category
ORDINALITY_MANY   // Locale-specific CLDR many category
ORDINALITY_OTHER  // Catchall category for remaining values

English: 1 and 21 are ONE, 2 is TWO, 3 is FEW, and 12 is OTHER.

Translation Failure Handling

JavaScript

Locale fallback and final failure handling are separate decisions. The translationFallbackPolicy option decides whether to try another locale; only after fallback stops does the translationFailureHandler function decide what to return or throw.

What translationFailureHandler returns

  • With no translationFailureHandler, a failed lookup returns the key with caller placeholders interpolated.
  • RETURN_KEY returns that key, after the function has recorded the structured failure it was given.
  • THROW_EXCEPTION throws a MissingTranslationError for missing translations and rethrows resolution failures.
  • returnString(text) returns the given text verbatim.

Built-in fallback policies

  • The "missing-or-no-match" policy is the safe default.
  • The "any-failure" policy also falls back after resolution failures.
  • The "never" policy stops after the first failed locale.
  • A function can decide for each failure, and see why.

Java

Locale fallback and final failure handling are separate decisions. A TranslationFallbackPolicy decides whether to try another locale; only after fallback stops does a TranslationFailureHandler decide what to return or throw.

Built-in handlers

  • The returnKey() handler silently returns the key with caller placeholders interpolated.
  • The returnKey(consumer) handler reports the structured failure to an application observer, then returns the key.
  • The throwException() handler throws for missing translations and rethrows resolution failures.

Built-in fallback policies

Swift

TranslationFallbackPolicy decides whether a failed candidate advances to another locale. The final TranslationFailureHandler runs only after translation attempts finish. The default policy tries the next candidate for missing translations and unmatched alternatives, but stops for resolution failures. The default handler returns the key; .throwException() throws for a missing translation or rethrows the retained resolution cause. Callback errors propagate directly.

Use .neverFallback() or .fallbackOnAnyFailure() when the application needs a different policy. These choices remain separate from locale negotiation.

JavaScript

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

const strings = createStrings({
  localizedStringSupplier: () => (readStringsFromDirectory("strings").catalogs),
  fallbackLocale: "en",
  localeSupplier: () => "en-US",
  translationFailureHandler: (failure) => {
    exampleMetrics.increment(`lokalized.translation.${failure.reason}`);
    return RETURN_KEY;
  },
});

Java

Strings strings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromClasspath("strings"))
  .localeSupplier(matcher -> matcher.bestMatchFor(Locale.US))
  .translationFailureHandler(TranslationFailureHandler.returnKey(failure ->
    exampleMetrics.increment("lokalized.translation." + failure.getReason())))
  .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: .returnKey(observer: { failure in
    // Send safe diagnostic metadata to the application's thread-safe telemetry.
    print(failure.reason.rawValue, failure.lookupLocale.tag)
  })
))

let matches18 = try strings.get("A missing key") == "A missing key"
assert(matches18)

JavaScript

Failure reasons distinguish "missing-translation", "no-matching-alternative", and "resolution-failure". A failure's message contains only the key, lookup locale, reason, and attempted locales; it omits caller placeholder values and runtime-cause messages. The failure also carries placeholders and cause as separate fields.

Java

Failure reasons distinguish MISSING_TRANSLATION, NO_MATCHING_ALTERNATIVE, and RESOLUTION_FAILURE. The TranslationFailure.getMessage() method contains only the key, lookup locale, reason, and attempted locales; it omits caller placeholder values and runtime-cause messages. The cause remains available separately through getCause().

Swift

Failure reasons are .missingTranslation, .noMatchingAlternative, and .resolutionFailure. TranslationFailure.message includes the key, lookup locale, reason, and attempted locales while omitting placeholder values and cause messages. Placeholder values and the retained cause remain separately available.

Translation Diagnostics

JavaScript

Most callers use get(...). Applications that need telemetry, audit trails, or negotiation details can request a structured result with getResult(...):

Java

Most callers use get(...). Applications that need telemetry, audit trails, or negotiation details can request a structured TranslationResult:

Swift

Most callers use try strings.get(...). Use getResult(...) for a structured TranslationResult with translation and negotiation diagnostics:

JavaScript

import { createLocaleMatcher, forLanguageRanges, parseLanguageRanges } from "lokalized/negotiate";

const negotiator = createLocaleMatcher(strings.getLocaleConfiguration());
const translationResult = strings.getResult(
  "I read {{bookCount}} books.",
  { bookCount: 3 },
  forLanguageRanges(negotiator, parseLanguageRanges("pt-PT,pt;q=0.8"))
);

translationResult.translation;             // => "Li 3 livros."
translationResult.lookupLocale;            // => "pt-PT"
translationResult.resolvedLocale;          // => "pt-PT"
translationResult.attemptedLocales;        // => ["pt-PT"]
translationResult.isFallback;              // => false
translationResult.localeMatchResult.matchType;   // => "exact"

Java

TranslationResult translationResult = strings.getResult(
  "I read {{bookCount}} books.",
  Map.of("bookCount", 3),
  TranslationOptions.forLanguageRanges(
    strings.parseLanguageRanges("pt-PT,pt;q=0.8"))
);

// Returned text, e.g. "Li 3 livros."
String message = translationResult.getTranslation();

// Negotiated locale used to begin lookup, e.g. pt-PT
Locale lookupLocale = translationResult.getLookupLocale();

// Locale that supplied the translation, e.g. Optional[pt-PT]
Optional<Locale> resolvedLocale = translationResult.getResolvedLocale();

// Ordered locales actually tried, e.g. [pt-PT]
List<Locale> attemptedLocales = translationResult.getAttemptedLocales();

// Whether negotiation or per-key resolution fell back, e.g. false
Boolean usedFallback = translationResult.isFallback();

// Negotiation details when available, e.g. an exact pt-PT match
Optional<LocaleMatchResult> localeMatchResult = translationResult.getLocaleMatchResult();

Swift

import Foundation
import Lokalized

let locale = try LocaleTag("pt-PT")
// 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
))

let ranges = try strings.parseLanguageRanges("pt-PT,pt;q=0.8")
let result = try strings.getResult("I read {{bookCount}} books.",
  placeholders: ["bookCount": .integer(3)], options: .forLanguageRanges(ranges))
assert(result.translation == "Li 3 livros.")
assert(result.lookupLocale.tag == "pt-PT")
assert(result.resolvedLocale?.tag == "pt-PT")
assert(result.attemptedLocales.map(\.tag) == ["pt-PT"])
assert(!result.isFallback)
assert(result.localeMatchResult?.matchType == .exact)

JavaScript

Results expose status, resolved locale, attempted locales, whether fallback occurred, the locale match, and any failure reason. Use a localeMatchSupplier, or pass localeMatchResult per call, when the original language ranges and match type must survive into diagnostics.

Java

Results expose status, resolved locale, attempted locales, whether fallback occurred, the locale-match result, and any failure reason. Use localeMatchSupplier(...) when the original language ranges and match type must survive into diagnostics.

Swift

Results expose status, resolved locale, attempted locales, fallback status, the locale-match result, and any failure reason. Supply a localeMatchSupplier or TranslationOptions.forLocaleMatch(...) to retain a match already produced by the same locale configuration.

Observing successful fallback

JavaScript

The translationFallbackObserver callback reports when an earlier locale candidate fails and a later candidate supplies the translation. Configure it when creating your Strings instance:

Java

Since Java 3.1.1, TranslationFallbackObserver can report when an earlier locale candidate fails and a later candidate supplies the translation. Configure it with Strings.Builder.translationFallbackObserver(...):

Swift

A TranslationFallbackObserver reports successful translations supplied by a later locale candidate. It runs once before that translation is returned, so an error thrown by the observer propagates to the caller.

JavaScript

import { createStrings } from "lokalized";

const strings = createStrings({
  localizedStringSupplier: () => ({
    en: { Welcome: "Hello!" },
    fr: {},
  }),
  fallbackLocale: "en",
  localeSupplier: () => "fr",
  translationFallbackObserver: (translationFallbackEvent) => {
    // Record successful translations supplied by another locale
    console.log(translationFallbackEvent.key,
      translationFallbackEvent.lookupLocale, translationFallbackEvent.resolvedLocale);
  },
});

// Reports fallback from French to English
strings.get("Welcome");   // => "Hello!"

Java

Strings strings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> Map.of(
    Locale.ENGLISH, List.of(new LocalizedString.Builder("Welcome")
      .translation("Hello!").build()),
    Locale.FRENCH, List.of()))
  .localeSupplier(localeMatcher -> Locale.FRENCH)
  .translationFallbackObserver(translationFallbackEvent -> {
    // Record successful translations supplied by another locale
    System.out.printf("Fallback for %s: %s -> %s%n",
      translationFallbackEvent.getKey(),
      translationFallbackEvent.getLookupLocale(),
      translationFallbackEvent.getResolvedLocale());
  })
  .build();

// Returns Hello! and reports fallback from French to English
String message = strings.get("Welcome");

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,
  translationFallbackObserver: TranslationFallbackObserver { event in
    print(event.key, event.lookupLocale.tag, event.resolvedLocale.tag)
  }
))

// Reports a successful fallback from French to English.
let matches19 = try strings.get("Welcome", options: .forLocale("fr")) == "Hello!"
assert(matches19)

JavaScript

The event includes key, lookupLocale, localeMatchResult, attemptedLocales, resolvedLocale, and precedingFailures. Each preceding failure records that candidate's locale, reason, and optional runtime cause.

Java

The TranslationFallbackEvent includes the key, lookup locale, locale-match result, attempted locales, resolved locale, and precedingFailures. Each preceding failure records that candidate's locale, failure reason, and optional runtime cause.

Swift

TranslationFallbackEvent includes the key, lookup locale, locale match, attempted locales, resolved locale, and precedingFailures. Each preceding failure records its locale, reason, and optional retained runtime cause. The event contains no placeholder values or rendered translation.

The observer runs once, synchronously, before the successful lookup returns. It does not run when the first candidate succeeds, when negotiation alone selects a fallback locale, or when lookup ultimately fails. Observer exceptions propagate to the caller without triggering another locale attempt or the translation failure handler.

JavaScript

Pass translationFallbackObserver in the third argument to get(...) or getResult(...) to replace the instance observer for one call. Omitting it inherits the instance observer; an empty callback suppresses its effects for that call.

Java

Use TranslationOptions.Builder.translationFallbackObserver(...) to replace the instance observer for one call. Omitting it or passing null inherits the instance observer; an empty callback suppresses its effects for that call. Observers shared by concurrent lookups must be thread-safe.

Swift

Set TranslationOptions.translationFallbackObserver to replace the instance observer for one call. nil inherits it; an empty callback suppresses its effects for that call. Observer callbacks are @Sendable, so shared telemetry must be safe for concurrent calls.

Events and their lists are immutable snapshots. They contain no rendered translation or caller placeholder values; runtime causes are retained by reference.

Localized Strings File Format

Structure

  • Files are UTF-8 and named with an IETF BCP 47 language tag, optionally followed by .json.
  • A blank or BOM-only file is invalid; use {} for an intentionally empty file.
  • The top level is one JSON object whose keys are translation keys.
  • A value can be a string shorthand or an object with translation, commentary, placeholders, and alternatives.
  • An object must provide a default translation or at least one alternative.
en-GB.json Localized Strings File (British English)
{
  "I am going on vacation.": {
    "commentary": "Shown as an option in the user's status menu.",
    "translation": "I am going on holiday."
  }
}

JSON Schema and Validation Warnings

The packaged JSON Schema validates file structure, placeholder shapes, and known language-form names. The loader separately parses expressions and produces structured warnings for incomplete locale-specific form maps.

JavaScript

Warnings never fail a load: they come back with the result, and a warningHandler receives each one as it is found. To fail fast, refuse to start when warnings is not empty.

Java

Warnings are silently ignored unless the application supplies a handler.

Swift

Parsed files expose warnings; the optional warningHandler receives them as they are found. A warning alone does not fail loading. An error thrown by your handler propagates; to enforce warning-free localized strings files, inspect the returned warnings during a build or startup check.

JavaScript

import { readStringsFromDirectory } from "lokalized/node";

// Warnings come back with the result; a load with warnings still succeeds
const { warnings } = readStringsFromDirectory("strings");
warnings.map((warning) => warning.type);   // => ["INCOMPLETE_CARDINALITY_TRANSLATIONS"]

// Receive each warning as it is found, for CI or application telemetry
readStringsFromDirectory("strings", { warningHandler: (warning) => telemetry.push(warning) });

Java

// Default: load successfully without emitting warning callbacks
Map<Locale, Set<LocalizedString>> strings =
  LocalizedStringLoader.loadFromClasspath("strings");

// Receive structured warnings directly for CI or application telemetry
List<LocalizedStringWarning> warnings = new ArrayList<>();
LocalizedStringLoader.loadFromClasspath("strings", warning -> {
  warnings.add(warning);
});

// Fail fast when a localized strings file is incomplete
LocalizedStringLoader.loadFromClasspath(
  "strings", LocalizedStringWarningHandler.throwException());

Swift

import Foundation
import Lokalized

let file = try LocalizedStringLoader.parse(
  #"{"items":{"translation":"{{count}} {{word}}","placeholders":{"word":{"value":"count","translations":{"CARDINALITY_ONE":"item"}}}}}"#,
  locale: "en", source: "database:en",
  warningHandler: { warning in print(warning.type.rawValue, warning.source) }
)
assert(file.warnings.map { $0.type } == [.incompleteCardinalityTranslations])

Commentary

The commentary field stores translator-facing context and is never rendered at runtime. It is particularly useful for contextual keys, strings whose meaning depends on their product surface, and documenting the names and types of application-supplied placeholder values.

Placeholders

Caller placeholders use {{name}}. Generated placeholders can select by one language form, by a cardinality range, or by ordered expressions. Names are Unicode-aware, built-in language-form constants are reserved, and generated definitions may depend on other generated values.

  • Escape an opening delimiter as \\{{name}} in JSON to render the literal text {{name}} instead of resolving it.
  • Each generated definition has one mutually exclusive shape: value plus translations, cardinality range plus translations, or translation with optional expression-selected alternatives.
  • JavaScript

    Language-form inputs accept numbers, decimal(...) and pluralOperands(...) values, the exported language-form constants, or strings resolved through a configured phoneticResolver. One translations map cannot mix form families.

    Java

    Language-form inputs accept numbers, PluralOperands, the typed language-form enums, or strings resolved through a configured PhoneticResolver. One translations map cannot mix form families.

    Swift

    Language-form inputs use explicit PlaceholderValue cases: numbers, plural operands, typed .languageForm values, or .text classified by a configured PhoneticResolver. One translations map cannot mix form families.
  • Caller values are the immutable predicate input. Generated values never become expression operands.
  • Only reachable generated placeholders are evaluated.
  • Cycles, excessive depth, missing forms, and failed custom resolution become resolution failures.
  • JavaScript

    Caller-supplied text is isolated with Unicode FSI/PDI by default when inserted into right-to-left output. Select bidiIsolation: "always" when right-to-left values can appear in left-to-right translations, or "none" for sinks that cannot accept bidi controls.

    Java

    Caller-supplied text is isolated with Unicode FSI/PDI by default when inserted into right-to-left output. Select BidiIsolation.ALWAYS when right-to-left values can appear in left-to-right translations, or disable isolation for sinks that cannot accept bidi controls.

    Swift

    Caller-supplied text is isolated with Unicode FSI/PDI by default in right-to-left output. Select BidiIsolation.always when right-to-left values can appear in left-to-right output, or .disabled for sinks that cannot accept bidi controls.
  • Caller values remain opaque during interpolation, even if their text itself contains placeholder syntax.

Alternatives

A single generated fragment can choose a phrase from its own expressions while the rest of the message resolves independently:

en.json Localized Strings File (English)
{
  "Search completed.": {
    "translation": "Found {{resultSummary}} {{timing}}.",
    "placeholders": {
      "resultSummary": {
        "translation": "{{formattedResultCount}} {{resultNoun}}",
        "alternatives": [
          {
            "resultCount == 0": "no results"
          },
          {
            "resultCount >= resultLimit": "at least {{formattedResultLimit}} results"
          }
        ]
      },
      "timing": {
        "translation": "in {{formattedDuration}}",
        "alternatives": [
          {
            "elapsedMilliseconds < 1000": "instantly"
          }
        ]
      },
      "resultNoun": {
        "value": "resultCount",
        "translations": {
          "CARDINALITY_ONE": "result",
          "CARDINALITY_OTHER": "results"
        }
      }
    }
  }
}

Placeholder Scope and Inheritance

Root definitions and definitions from each selected alternative are inherited by descendants. The nearest same-named definition replaces its ancestor as a complete unit. Raw caller values keep precedence for predicates.

Recursive Alternatives

A selected alternative can refine itself with additional rules. Alternatives are evaluated in list order and stop at the first match. Nested alternatives run before their branch's default translation, and a selected branch never falls through to a later sibling.

In this recruiter notification, gender matters only when exactly one applicant is present. Nesting writes applicantCount == 1 once, groups the gender-specific wording beneath it, and retains a neutral default for common or neuter gender.

A flat ordered list using && would be equivalent. Recursion adds hierarchy, branch-local defaults, and inherited definitions rather than additional Boolean power. This complete Spanish entry expects applicantCount and applicantGender as a gender value, such as GENDER_FEMININE.

es.json Localized Strings File (Spanish)
{
  "You have {{applicantCount}} new applicants.": {
    "translation": "Tienes {{applicantCount}} candidaturas nuevas.",
    "alternatives": [
      {
        "applicantCount == 0": "No tienes candidaturas nuevas."
      },
      {
        "applicantCount == 1": {
          "translation": "Tienes una candidatura nueva.",
          "alternatives": [
            {
              "applicantGender == GENDER_MASCULINE": "Tienes un candidato nuevo."
            },
            {
              "applicantGender == GENDER_FEMININE": "Tienes una candidata nueva."
            }
          ]
        }
      }
    ]
  }
}

Expression Language

Fragment alternatives and recursive alternatives use the same bounded expression language:

Expressions support

  • Parenthesized infix expressions with && and ||.
  • Use <, >, <=, and >= for numeric operands; use == and != for numbers and language forms.
  • Numbers, caller variables, and built-in language-form constants.
  • Recursive alternatives and inherited placeholder definitions.

Expressions do not support

  • Unary !.
  • String, Boolean, or explicit null literals.
  • Functions or arbitrary return values.
  • A range operand inside the expression grammar.

Expression grammar

The formal grammar spells out precedence, every built-in language-form constant, numeric syntax, and valid caller-variable names.

View the complete expression grammar
EXPRESSION = OR_EXPRESSION ;
OR_EXPRESSION = AND_EXPRESSION { "||" AND_EXPRESSION } ;
AND_EXPRESSION = PRIMARY_EXPRESSION { "&&" PRIMARY_EXPRESSION } ;
PRIMARY_EXPRESSION = COMPARISON | "(" EXPRESSION ")" ;
COMPARISON = OPERAND COMPARISON_OPERATOR OPERAND ;
OPERAND = VARIABLE | LANGUAGE_FORM | NUMBER ;
LANGUAGE_FORM = CARDINALITY | ORDINALITY | GENDER | GRAMMATICAL_CASE
              | DEFINITENESS | CLASSIFIER | FORMALITY | CLUSIVITY
              | ANIMACY | PHONETIC ;
CARDINALITY = "CARDINALITY_ZERO" | "CARDINALITY_ONE" | "CARDINALITY_TWO"
            | "CARDINALITY_FEW" | "CARDINALITY_MANY" | "CARDINALITY_OTHER" ;
ORDINALITY = "ORDINALITY_ZERO" | "ORDINALITY_ONE" | "ORDINALITY_TWO"
           | "ORDINALITY_FEW" | "ORDINALITY_MANY" | "ORDINALITY_OTHER" ;
GENDER = "GENDER_MASCULINE" | "GENDER_FEMININE"
       | "GENDER_COMMON" | "GENDER_NEUTER" ;
GRAMMATICAL_CASE = "CASE_NOMINATIVE" | "CASE_ACCUSATIVE"
                 | "CASE_GENITIVE" | "CASE_DATIVE" | "CASE_INSTRUMENTAL"
                 | "CASE_LOCATIVE" | "CASE_PREPOSITIONAL"
                 | "CASE_VOCATIVE" | "CASE_ABLATIVE" ;
DEFINITENESS = "DEFINITENESS_DEFINITE" | "DEFINITENESS_INDEFINITE"
             | "DEFINITENESS_CONSTRUCT" ;
CLASSIFIER = "CLASSIFIER_GENERAL" | "CLASSIFIER_PERSON" | "CLASSIFIER_ANIMAL"
           | "CLASSIFIER_LONG_THIN" | "CLASSIFIER_FLAT" | "CLASSIFIER_BOUND"
           | "CLASSIFIER_MACHINE" | "CLASSIFIER_VEHICLE" ;
FORMALITY = "FORMALITY_CASUAL" | "FORMALITY_INFORMAL" | "FORMALITY_FORMAL"
          | "FORMALITY_HUMBLE" | "FORMALITY_HONORIFIC" ;
CLUSIVITY = "CLUSIVITY_INCLUSIVE" | "CLUSIVITY_EXCLUSIVE" ;
ANIMACY = "ANIMACY_ANIMATE" | "ANIMACY_INANIMATE" ;
PHONETIC = "PHONETIC_VOWEL" | "PHONETIC_CONSONANT"
         | "PHONETIC_H_SILENT" | "PHONETIC_H_ASPIRATED"
         | "PHONETIC_S_IMPURE" | "PHONETIC_Z" | "PHONETIC_GN" | "PHONETIC_PS"
         | "PHONETIC_PN" | "PHONETIC_X"
         | "PHONETIC_GLIDE_Y" | "PHONETIC_GLIDE_W"
         | "PHONETIC_STRESSED_A" | "PHONETIC_SOLAR" | "PHONETIC_LUNAR"
         | "PHONETIC_OTHER" ;
NUMBER = [ SIGN ],
         ( DIGITS, [ ".", { DIGIT } ] | ".", DIGITS ),
         [ EXPONENT ] ;
EXPONENT = ( "e" | "E" ), [ SIGN ], DIGITS ;
SIGN = "+" | "-" ;
DIGITS = DIGIT, { DIGIT } ;
DIGIT = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
VARIABLE = ( Unicode letter | "_" )
           { Unicode letter | Unicode number | Unicode combining mark
           | "_" | "-" } ;
COMPARISON_OPERATOR = "<" | ">" | "<=" | ">=" | "==" | "!=" ;

Comparison operators bind more tightly than &&, which binds more tightly than ||. Parentheses override that precedence.

Expressions ignore ASCII space, horizontal tab, carriage return, line feed, and form feed between tokens. Other Unicode whitespace and separator characters are rejected.

Built-in language-form constants are reserved and cannot be used as placeholder names.

Inspection

JavaScript

A Strings instance exposes read-only helpers for audits, coverage tools, and build checks:

Java

A Strings instance exposes read-only helpers for audits, coverage tools, and build checks:

Swift

A Strings instance exposes read-only helpers for audits, coverage tools, and build checks:

JavaScript

strings.getSupportedLocales();        // => ["en", "fr"]
strings.getKeysForLocale("en");       // => ["Goodbye", "Hello"]
strings.getMissingKeys("en", "fr");   // => ["Goodbye"]

Java

// Find the locales currently available for lookup
Set<Locale> supportedLocales = strings.getSupportedLocales();

// List every translation key in the English localized strings file
Set<String> englishKeys =
  strings.getKeysForLocale(Locale.forLanguageTag("en"));

// Find English keys that still need a French translation
Set<String> missingFrenchKeys = strings.getMissingKeys(
  Locale.forLanguageTag("en"),
  Locale.forLanguageTag("fr")
);

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
))

let fr = try LocaleTag("fr")
assert(strings.supportedLocales.map(\.tag).sorted() == ["en", "fr"])
let matches20 = try strings.getKeysForLocale(locale).map(\.string).sorted() == ["Goodbye", "Hello"]
assert(matches20)
let matches21 = try strings.getMissingKeys(sourceLocale: locale, targetLocale: fr).map(\.string).sorted() == ["Goodbye"]
assert(matches21)

JavaScript

The getKeysForLocale(...) and getMissingKeys(...) methods are strict: an unsupported locale throws an UnsupportedLocaleError. Probe getSupportedLocales() first when availability is uncertain.

Java

The getKeysForLocale(...) and getMissingKeys(...) methods are strict: unsupported locales throw. Probe getSupportedLocales() first when availability is uncertain.

Swift

getKeysForLocale(...) and getMissingKeys(sourceLocale:targetLocale:) throw UnsupportedLocaleError for unsupported locales. Check supportedLocales when availability is uncertain. Keys are ExactString values, preserving distinct Unicode spellings.

Runtime Safety Limits

JavaScript

Localized strings compilation and lookup run under fixed limits. They bound numeric work, expression parsing, recursive placeholder generation, interpolation, and cumulative expansion.

Java

Localized strings compilation and lookup use immutable TranslationRuntimeLimits. Defaults bound numeric work, expression parsing, recursive placeholder generation, interpolation, and cumulative expansion.

Swift

Immutable TranslationRuntimeLimits bound numeric work, expression parsing, recursive placeholder generation, interpolation, and cumulative expansion. Supply stricter limits through StringsConfiguration.runtimeLimits:

Default and hard runtime safety limits
WorkDefaultHard ceiling
Numeric precision, scale, visible decimals1,0244,096
Compact exponent644,096
Expression characters2,0484,096
Expression tokens256512
Nested expression groups3264
Generated-placeholder depth3264
One interpolated result262,144 UTF-16 code units1,048,576
Cumulative generated expansion per locale attempt1,048,576 UTF-16 code units8,388,608

JavaScript

The JavaScript package fixes these limits at the defaults above. They cannot be configured: createStrings refuses a runtimeLimits option. The hard ceilings still apply while localized strings are loaded.

Java

TranslationRuntimeLimits runtimeLimits = TranslationRuntimeLimits.builder()
  .maximumExpressionCharacters(1_024)
  .maximumExpressionTokens(128)
  .maximumInterpolatedOutputCharacters(128 * 1_024)
  .maximumGeneratedExpansionCharacters(512 * 1_024)
  .build();

Strings strings = Strings.withFallbackLocale(Locale.ENGLISH)
  .localizedStringSupplier(() -> LocalizedStringLoader
    .loadFromClasspath("com/example/myapp/strings"))
  .localeSupplier(matcher -> Locale.ENGLISH)
  .runtimeLimits(runtimeLimits)
  .build();

Swift

import Foundation
import Lokalized

let limits = try TranslationRuntimeLimits(
  maximumGeneratedPlaceholderDepth: 16,
  maximumInterpolatedOutputCharacters: 32_768,
  maximumGeneratedExpansionCharacters: 131_072
)
let locale = try LocaleTag("en")
let files = try LocalizedStringLoader.loadFromDirectory(URL(fileURLWithPath: "strings"))
let catalogs = files.mapValues { LocalizedCatalog(strings: $0.strings) }
let strings = try DefaultStrings(configuration: StringsConfiguration(
  localizedStringSupplier: { catalogs }, localeSupplier: { _ in locale },
  fallbackLocale: locale, runtimeLimits: limits
))

JavaScript

The loader checks the expression and numeric-literal hard ceilings; createStrings then compiles the loaded data under the fixed limits. Locale fallback starts a fresh generated-expansion budget for each candidate.

Java

The loader checks expression and numeric-literal hard ceilings before an application chooses a runtime policy. Building Strings then compiles the loaded data and enforces the configured limits. Locale fallback starts a fresh generated-expansion budget for each candidate.

Swift

The parser checks expression and numeric-literal hard ceilings. DefaultStrings(configuration:) compiles localized strings files under the configured runtime limits. Locale fallback starts a fresh generated-expansion budget for each candidate.

Keying Strategy

Lokalized imposes no naming convention. Natural-language and contextual keys have different tradeoffs, and a project can mix them.

Swift uses ExactString for translation keys and placeholder names because ordinary String equality merges canonically equivalent Unicode spellings. String literals work directly; wrap a dynamic key with ExactString(key). Keep files as JSON or use the typed exact-key collections when constructing data in code.

Natural Language Keys

Example: "I read {{bookCount}} books."

Pros

  • Easy to create without a naming registry.
  • Placeholders document themselves in the key.
  • The key is a useful fallback if localized data is missing.

Cons

  • Product context can be lost.
  • Long legal or editorial copy makes poor keys.
  • Wording changes require changing every localized strings file.

Contextual Keys

Examples: Checkout.Title, Checkout.Submit, and Checkout.Cancel.

en.json Localized Strings File (English)
{
  "Checkout.Title": {
    "commentary": "Heading shown at the top of the checkout page.",
    "translation": "Checkout"
  },
  "Checkout.Submit": {
    "commentary": "Primary button that submits the order.",
    "translation": "Place order"
  },
  "Checkout.Cancel": {
    "commentary": "Secondary button that returns the user to the cart.",
    "translation": "Return to cart"
  }
}

Pros

  • Targets a specific surface or component.
  • Works for large or frequently revised copy.
  • Translation wording can change without code changes.

Cons

  • Every key needs a deliberate name and record.
  • Placeholders require explicit translator documentation.
  • A missing entry can expose the contextual key to users.

Or - Mix Both

Natural-language keys can cover ordinary product copy while contextual keys handle legal text and surfaces where wording or context changes independently.

Comparing Localization Formats

Lokalized, ICU MessageFormat, MessageFormat 2 (MF2), Fluent, and gettext all handle variable substitution and ordinary plural selection. The meaningful differences appear when several runtime facts jointly control wording, a language needs agreement beyond plurals, or the application needs a strict runtime contract.

A Common Baseline: Plural Selection

Classic ICU MessageFormat expresses a plural message compactly:

{bookCount, plural, one {I read # book.} other {I read # books.}}

Equivalent Lokalized structure

en.json Localized Strings File (English)
{
  "I read {{bookCount}} books.": {
    "translation": "I read {{bookCount}} {{books}}.",
    "placeholders": {
      "books": {
        "value": "bookCount",
        "translations": {
          "CARDINALITY_ONE": "book",
          "CARDINALITY_OTHER": "books"
        }
      }
    }
  }
}

This baseline is not a differentiator: every format above handles it well. Lokalized's extra structure starts paying for itself when the message must coordinate several forms or isolate one agreement-sensitive fragment without duplicating every complete sentence.

Where Lokalized Goes Further Out of the Box

ConcernLokalizedOther formats
Sparse compound rules Ordered predicates can combine typed forms, exact numbers, thresholds, &&, ||, and parentheses. A rule can replace one fragment or the whole message, with the ordinary translation serving as the default. ICU MessageFormat nests select and plural; MF2 enumerates multi-selector variants; Fluent uses select expressions. Equivalent output is often possible, but inequalities and sparse cross-field exceptions generally need more variants, a custom selector, or a value precomputed by application code. gettext leaves non-plural selection to keys, contexts, or application logic.
Grammatical vocabulary Cardinality, ordinality, gender, grammatical case, definiteness, classifiers, formality, clusivity, animacy, and phonetics are named, typed concepts shared by translation files and application code. ICU and Fluent can use application-defined selector keys; Fluent terms can model case and other facets. MF2 custom selectors can add domain-specific behavior. The format itself does not supply Lokalized's complete vocabulary as one built-in contract.
Cardinality ranges A generated placeholder accepts typed start and end values and selects the result using pinned CLDR plural-range data. Range agreement is not a built-in selector in the compared core message syntaxes. MF2 documents it as a custom-selector use case; other approaches normally preprocess the range, add custom logic, or select a separate key.
Phonetic agreement An application-supplied phonetic resolver maps runtime text to typed onset categories for rules such as a/an, silent or aspirated h, Italian initial clusters, Spanish stressed a, and Arabic sun or moon letters. The application generally supplies a precomputed selector value or extends the runtime with a custom function or selector.
Runtime guarantees Lokalized adds fail-fast compilation, bounded evaluation, structured diagnostics, deterministic locale matching, immutable thread-safe objects, and no runtime dependencies. ICU, MF2, Fluent, and gettext primarily define translation behavior. Validation, resource limits, locale negotiation, concurrency, and dependency choices vary by implementation and integration.
ICU's nested selectors, MF2's multi-selector matching and custom functions, and Fluent's selectors and parameterized terms are powerful. Lokalized's distinction is that its runtime provides the agreement concepts and operational guarantees above as one coherent, zero-dependency system, without requiring a project to invent custom selector conventions first.

Where Lokalized's Approach Shines

  • A translator needs direct control over case, gender, register, phonetics, or another typed form rather than an opaque application-generated flag.
  • Most messages follow a default translation but a few compound predicates require natural whole-phrase rewrites.
  • CLDR cardinality ranges or phonetic onset rules are real product requirements rather than theoretical edge cases.
  • An application benefits from startup validation, bounded evaluation, deterministic locale fallback, and a dependency-free runtime.

The examples below pass runtime facts straight to Lokalized. The application supplies limits, quantities, and formatted display values; the localized strings file decides how to describe them. Each example uses its own en.json for clarity. The calls assume strings is configured with that file and your app's locale supplier, as in Getting Started.

2. Stock Warnings: Sparse Exceptions, Whole-Message Rewrites

An order needs different wording when the requested quantity exceeds stock. The warning also depends on formality, a supplied low-stock threshold, and whether the order would take the last items. A few ordered rules cover those exceptions; the default remains a simple availability statement.

JavaScript

import { FORMALITY_FORMAL, FORMALITY_INFORMAL } from "lokalized";

const key = "Order availability.";

// The requested quantity exceeds stock; formal wording
strings.get(key, {
  stockCount: 2, requestedCount: 3,
  lowStockThreshold: 5, formality: FORMALITY_FORMAL
}); // => "We can supply 2 items of the 3 requested. Please reduce the quantity."

// The same quantities; informal wording
strings.get(key, {
  stockCount: 2, requestedCount: 3,
  lowStockThreshold: 5, formality: FORMALITY_INFORMAL
}); // => "Only 2 items left. Try a smaller order."

// A different compound rule selects the whole message
strings.get(key, {
  stockCount: 1, requestedCount: 1,
  lowStockThreshold: 5, formality: FORMALITY_FORMAL
}); // => "Last chance: 1 item left."

Java

String key = "Order availability.";

// The requested quantity exceeds stock; formal wording
String message1 = strings.get(key, Map.of(
  "stockCount", 2, "requestedCount", 3,
  "lowStockThreshold", 5, "formality", Formality.FORMAL
));
assertEquals("We can supply 2 items of the 3 requested. Please reduce the quantity.", message1);

// The same quantities; informal wording
String message2 = strings.get(key, Map.of(
  "stockCount", 2, "requestedCount", 3,
  "lowStockThreshold", 5, "formality", Formality.INFORMAL
));
assertEquals("Only 2 items left. Try a smaller order.", message2);

// A different compound rule selects the whole message
String message3 = strings.get(key, Map.of(
  "stockCount", 1, "requestedCount", 1,
  "lowStockThreshold", 5, "formality", Formality.FORMAL
));
assertEquals("Last chance: 1 item left.", message3);

Swift

let key: ExactString = "Order availability."

// The requested quantity exceeds stock; formal wording
let message1 = try strings.get(key, placeholders: [
  "stockCount": .integer(2), "requestedCount": .integer(3),
  "lowStockThreshold": .integer(5), "formality": .languageForm(.formality(.formal))
])
assert(message1 == "We can supply 2 items of the 3 requested. Please reduce the quantity.")

// The same quantities; informal wording
let message2 = try strings.get(key, placeholders: [
  "stockCount": .integer(2), "requestedCount": .integer(3),
  "lowStockThreshold": .integer(5), "formality": .languageForm(.formality(.informal))
])
assert(message2 == "Only 2 items left. Try a smaller order.")

// A different compound rule selects the whole message
let message3 = try strings.get(key, placeholders: [
  "stockCount": .integer(1), "requestedCount": .integer(1),
  "lowStockThreshold": .integer(5), "formality": .languageForm(.formality(.formal))
])
assert(message3 == "Last chance: 1 item left.")
en.json Localized Strings File (English)
{
  "Order availability.": {
    "translation": "{{stockCount}} {{items}} available.",
    "placeholders": {
      "items": {
        "value": "stockCount",
        "translations": {
          "CARDINALITY_ONE": "item",
          "CARDINALITY_OTHER": "items"
        }
      }
    },
    "alternatives": [
      {
        "stockCount == 0": "Sold out."
      },
      {
        "requestedCount > stockCount && formality == FORMALITY_FORMAL": "We can supply {{stockCount}} {{items}} of the {{requestedCount}} requested. Please reduce the quantity."
      },
      {
        "requestedCount > stockCount": "Only {{stockCount}} {{items}} left. Try a smaller order."
      },
      {
        "stockCount <= lowStockThreshold && (requestedCount == stockCount || formality == FORMALITY_INFORMAL)": "Last chance: {{stockCount}} {{items}} left."
      }
    ]
  }
}

The sold-out rule takes priority. An oversized request then gets a complete formal or informal rewrite. Finally, a compound condition using &&, ||, and parentheses selects the last-chance message. The generated items fragment keeps plural agreement correct in every sentence.

Where the extra work moves: ICU MessageFormat, MF2, and Fluent can already select formal versus informal wording. Here, application code or a custom selector must additionally classify the relationships between requestedCount, stockCount, and lowStockThreshold, including the compound last-chance condition. gettext needs application-selected keys or contexts as well. With Lokalized, translators can change the order and wording of these exceptions in the same file instead of coordinating new application flags such as oversizedOrder and lastChance.

3. Delivery Estimates: Range Agreement Plus Conditional Rewrites

A delivery estimate can be a range, an exact duration, or same-day delivery. A longer-than-usual range gets a formal rewrite. Even English exposes why a range cannot blindly use its final endpoint for plural agreement: 0-1 days and 1 day need different forms.

JavaScript

import { FORMALITY_FORMAL, FORMALITY_INFORMAL } from "lokalized";

import { createStrings } from "lokalized";
import { cardinalRangeData } from "lokalized/data/ranges";

// englishStrings is the parsed en.json below; currentLocale comes from the app.
const strings = createStrings({
  localizedStringSupplier: () => ({ en: englishStrings }),
  fallbackLocale: "en", localeSupplier: () => currentLocale,
  pluralData: { ranges: cardinalRangeData }
});

const key = "Delivery estimate.";

// A range ending in 1 still takes the English plural
strings.get(key, {
  minDays: 0, maxDays: 1,
  usualMaxDays: 3, formality: FORMALITY_INFORMAL
}); // => "Delivery in 0-1 days."

// Equal endpoints use an exact duration
strings.get(key, {
  minDays: 1, maxDays: 1,
  usualMaxDays: 3, formality: FORMALITY_INFORMAL
}); // => "Delivery in 1 day."

// A longer estimate uses a formal whole-message rewrite
strings.get(key, {
  minDays: 3, maxDays: 5,
  usualMaxDays: 3, formality: FORMALITY_FORMAL
}); // => "Please allow 3-5 days for delivery."

Java

String key = "Delivery estimate.";

// A range ending in 1 still takes the English plural
String message1 = strings.get(key, Map.of(
  "minDays", 0, "maxDays", 1,
  "usualMaxDays", 3, "formality", Formality.INFORMAL
));
assertEquals("Delivery in 0-1 days.", message1);

// Equal endpoints use an exact duration
String message2 = strings.get(key, Map.of(
  "minDays", 1, "maxDays", 1,
  "usualMaxDays", 3, "formality", Formality.INFORMAL
));
assertEquals("Delivery in 1 day.", message2);

// A longer estimate uses a formal whole-message rewrite
String message3 = strings.get(key, Map.of(
  "minDays", 3, "maxDays", 5,
  "usualMaxDays", 3, "formality", Formality.FORMAL
));
assertEquals("Please allow 3-5 days for delivery.", message3);

Swift

let key: ExactString = "Delivery estimate."

// A range ending in 1 still takes the English plural
let message1 = try strings.get(key, placeholders: [
  "minDays": .integer(0), "maxDays": .integer(1),
  "usualMaxDays": .integer(3), "formality": .languageForm(.formality(.informal))
])
assert(message1 == "Delivery in 0-1 days.")

// Equal endpoints use an exact duration
let message2 = try strings.get(key, placeholders: [
  "minDays": .integer(1), "maxDays": .integer(1),
  "usualMaxDays": .integer(3), "formality": .languageForm(.formality(.informal))
])
assert(message2 == "Delivery in 1 day.")

// A longer estimate uses a formal whole-message rewrite
let message3 = try strings.get(key, placeholders: [
  "minDays": .integer(3), "maxDays": .integer(5),
  "usualMaxDays": .integer(3), "formality": .languageForm(.formality(.formal))
])
assert(message3 == "Please allow 3-5 days for delivery.")
en.json Localized Strings File (English)
{
  "Delivery estimate.": {
    "translation": "Delivery in {{minDays}}-{{maxDays}} {{rangeDays}}.",
    "placeholders": {
      "rangeDays": {
        "range": {
          "start": "minDays",
          "end": "maxDays"
        },
        "translations": {
          "CARDINALITY_ONE": "day",
          "CARDINALITY_OTHER": "days"
        }
      },
      "singleDays": {
        "value": "maxDays",
        "translations": {
          "CARDINALITY_ONE": "day",
          "CARDINALITY_OTHER": "days"
        }
      }
    },
    "alternatives": [
      {
        "maxDays == 0": "Delivery today."
      },
      {
        "minDays == maxDays": "Delivery in {{maxDays}} {{singleDays}}."
      },
      {
        "maxDays > usualMaxDays && formality == FORMALITY_FORMAL": "Please allow {{minDays}}-{{maxDays}} {{rangeDays}} for delivery."
      }
    ]
  }
}

rangeDays uses CLDR plural-range rules for both endpoints; singleDays uses ordinary cardinal rules. Equal endpoints and same-day delivery get whole-message alternatives before the longer-than-usual rule is considered. Java and Swift include range data; JavaScript opts into it with lokalized/data/ranges.

Where the extra work moves: Selecting a plural from maxDays alone would produce the wrong range form in the first call. The compared core selectors do not supply this two-endpoint agreement. An ICU MessageFormat, Fluent, or gettext integration must compute a range category or choose a range-specific message; an MF2 integration can add a custom range selector. The exact-duration and longer-than-usual comparisons also need derived selectors or application branching. Lokalized combines range agreement and those rewrites directly in the translation file.

These comparisons concern the formats' core selectors and stock functions. Extensions and application logic can achieve the same output. The advantage shown here is where the rules live: Lokalized lets translators express cross-field wording conditions and range agreement using a shared built-in vocabulary, while the app keeps ownership of the underlying facts and business decisions.