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.
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.
{ "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
Accept-Language preferences, and explicit tiebreakers are handled deterministically, with the same IANA language-range equivalents in every runtime; see the matching orderJavaScript
Date/time, number, percentage, and currency formatting or parsing - useIntl for formatting and handle parsing separately
Java
Date/time, number, percentage, and currency formatting or parsing - use the JDK's formatters and parsersSwift
Date/time, number, percentage, and currency formatting or parsing - use Foundation format styles and formattersJavaScript
Collation - use JavaScript'sIntl.Collator
Java
Collation - use the JDK'sCollator
Swift
Collation - use Foundation string comparison with an explicit localeJavaScript
Support Node.js before 20. The JavaScript package targets Node.js 20+ and modern browsersJava
Support Java 8 and below. Lokalized targets Java 9+Swift
Support older Apple systems. Lokalized targets Swift 6.2+, iOS 15+, and macOS 12+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.
JavaScript
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.
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
<dependency> <groupId>com.lokalized</groupId> <artifactId>lokalized</artifactId> <version>3.1.2</version> </dependency>
dependencies { implementation("com.lokalized:lokalized:3.1.2") }
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.
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
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.
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] )
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:
window.lokalized, with no modules or build step.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.
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.jsimport { 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.
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.
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.
Similarly-flavored commercially-friendly OSS libraries are available.
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.
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.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.
{ "welcome": "Hello, {{name}}!" }
{ "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.
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.
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.
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.
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.
Filenames must be IETF BCP 47 language tags, optionally suffixed by .json.
{ "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." } ] } }
Strings InstanceJavaScript
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.
Strings Instance for TranslationsRaw 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.")
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:
{ "You have {{formattedCount}} items.": { "translation": "You have {{formattedCount}} {{items}}.", "placeholders": { "items": { "value": "count", "translations": { "CARDINALITY_ONE": "item", "CARDINALITY_OTHER": "items" } } } } }
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.")
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.
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.
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.
DefaultStrings(configuration:) throws ConfigurationError when an ambiguous language lacks a complete tiebreaker list.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 is deterministic. It applies these rules in order:
en-AU can prefer en-001 before en.zh-TW can match zh-Hant while sr-Latn remains distinct from sr-Cyrl.no and Bokmål nb, after exact matches.q=0 exclusions.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.
| Request | CLDR interpretation | Preferred compatible file |
|---|---|---|
zh-TW | Traditional Chinese | zh-Hant |
zh-HK | Traditional Chinese | zh-Hant |
zh-CN | Simplified Chinese | zh-Hans |
zh | Simplified Chinese by default | zh-Hans |
sr | Cyrillic Serbian by default | sr-Cyrl |
sr-Latn | Latin Serbian | sr-Latn |
sh | Legacy tag associated with Latin Serbian | sr-Latn |
| Request | Loaded files | Result |
|---|---|---|
en-AU | en-001, en | en-001 |
en-AU | en | en |
en-CA | en-US, en-GB | First configured English tiebreaker |
fr-BE | fr-FR, fr-CA | First configured French tiebreaker |
| Request | Candidate order |
|---|---|
no-NO | no-NO → no → nb-NO → nb |
nb-NO | nb-NO → nb → no → no-NO |
Norwegian Nynorsk (nn) is independent and does not participate in this bridge.
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.
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.
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.
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
import { readStringsFromDirectory } from "lokalized/node"; // In Node: read every localized strings file in one directory const { catalogs, warnings } = readStringsFromDirectory("strings");
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
createStrings takes as loaded.chooseBrowserLocale(...).und.json file is the localized strings filename for the root locale.{} for an intentionally empty file.Java
com/example/myapp/strings.ClassLoader overload in containers, plugin systems, and test harnesses.exhaustiveClasspathSearch disabled unless a JAR omits package directory entries; enabling it scans every visible filesystem and JAR classpath root.META-INF/versions directory is reserved from package discovery; use loadFromClasspathResources(...) when an application intentionally needs an exact resource beneath it.und.json file is the localized strings filename for Locale.ROOT.{} for an intentionally empty file..json resources with invalid locale filenames and warns; filesystem loading remains strict.Swift
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.Bundle.main for an app, Bundle.module from the SwiftPM target that owns resources, or an explicit framework bundle.resourcePathsByLocale with loadFromBundle, or loadFromResources with local file URLs, for explicit resource mapping.String, Data, or InputStream values when your application supplies localized strings contents. Lokalized does not download them.| Resource | Default |
|---|---|
| One file or stream | 8 MiB |
| One text input | 8,388,608 UTF-16 code units |
| JSON nesting | 64 levels; hard maximum 128 |
| Aggregate input | 32 MiB |
| Localized strings files | 256 |
| Translation nodes | 100,000 |
| Warnings | 1,000 |
| Discovery entries, when scanning a directory or package | 100,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)
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 );
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.
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));
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.
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.
Lokalized is most useful when a sentence must be rewritten across more than one grammatical dimension.
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.
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.
{ "{{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." } ] } }
{ "{{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." } ] } }
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)
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.
{ "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" } } } } }
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 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.
{ "{{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" } } } } }
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 are typed caller values used by localized strings to choose words or phrases. The supported families follow the README's order below.
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)
Chooses agreement by grammatical gender. Common gender covers languages that merge masculine and feminine; neuter remains distinct.
GENDER_MASCULINE
GENDER_FEMININE
GENDER_COMMON
GENDER_NEUTER
Example: English selects He, She, or This person; Spanish may rewrite several agreeing words.
Changes nouns or pronouns according to syntactic role. The enum is intentionally broad, though not exhaustive for every language.
CASE_NOMINATIVE
CASE_ACCUSATIVE
CASE_GENITIVE
CASE_DATIVE
CASE_INSTRUMENTAL
CASE_LOCATIVE
CASE_PREPOSITIONAL
CASE_VOCATIVE
CASE_ABLATIVE
Russian: CASE_DATIVE can select Ивану in Отправить сообщение Ивану.
Distinguishes definite, indefinite, and construct or bound noun phrases.
DEFINITENESS_DEFINITE
DEFINITENESS_INDEFINITE
DEFINITENESS_CONSTRUCT
Arabic: a definite document can select الكتاب, while the indefinite form selects كتابًا.
Selects measure words and counters. The categories are generic semantic buckets; language-specific inventories may need dedicated keys.
CLASSIFIER_GENERAL
CLASSIFIER_PERSON
CLASSIFIER_ANIMAL
CLASSIFIER_LONG_THIN
CLASSIFIER_FLAT
CLASSIFIER_BOUND
CLASSIFIER_MACHINE
CLASSIFIER_VEHICLE
Japanese: CLASSIFIER_BOUND selects the book counter 冊.
Selects casual, informal, formal, humble, or honorific register.
FORMALITY_CASUAL
FORMALITY_INFORMAL
FORMALITY_FORMAL
FORMALITY_HUMBLE
FORMALITY_HONORIFIC
Example: one greeting key can produce Hey, Sam., Hello, Sam., or Greetings, Dr. Smith.
Distinguishes whether first-person plural includes or excludes the addressee.
CLUSIVITY_INCLUSIVE
CLUSIVITY_EXCLUSIVE
Malay: kita includes the listener; kami excludes them.
Distinguishes animate and inanimate referents when a language's agreement or case system requires it.
ANIMACY_ANIMATE
ANIMACY_INANIMATE
Russian: masculine accusative forms can differ for a brother (брата) and a table (стол).
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
CARDINALITY_ONE
CARDINALITY_TWO
CARDINALITY_FEW
CARDINALITY_MANY
CARDINALITY_OTHER
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.
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.
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
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
English: a resolver can select an honor and a gift from the same localized string.
CLDR maps numbers to rank categories. Like cardinalities, category names can cover many values.
ORDINALITY_ZERO
ORDINALITY_ONE
ORDINALITY_TWO
ORDINALITY_FEW
ORDINALITY_MANY
ORDINALITY_OTHER
English: 1 and 21 are ONE, 2 is TWO, 3 is FEW, and 12 is OTHER.
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.
translationFailureHandler returnstranslationFailureHandler, 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."missing-or-no-match" policy is the safe default."any-failure" policy also falls back after resolution failures."never" policy stops after the first failed locale.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.
returnKey() handler silently returns the key with caller placeholders interpolated.returnKey(consumer) handler reports the structured failure to an application observer, then returns the key.throwException() handler throws for missing translations and rethrows resolution failures.fallbackOnMissingTranslationOrNoMatchingAlternative() policy is the safe default.fallbackOnAnyFailure() policy also falls back after resolution failures.neverFallback() policy stops after the first failed locale.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.
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.
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.
.json.{} for an intentionally empty file.translation, commentary, placeholders, and alternatives.translation or at least one alternative.{ "I am going on vacation.": { "commentary": "Shown as an option in the user's status menu.", "translation": "I am going on holiday." } }
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])
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.
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.
\\{{name}} in JSON to render the literal text {{name}} instead of resolving it.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 explicitPlaceholderValue cases: numbers, plural operands, typed .languageForm values, or .text classified by a configured PhoneticResolver. One translations map cannot mix form families.
JavaScript
Caller-supplied text is isolated with Unicode FSI/PDI by default when inserted into right-to-left output. SelectbidiIsolation: "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. SelectBidiIsolation.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. SelectBidiIsolation.always when right-to-left values can appear in left-to-right output, or .disabled for sinks that cannot accept bidi controls.
A single generated fragment can choose a phrase from its own expressions while the rest of the message resolves independently:
{ "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" } } } } }
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.
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.
{ "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." } ] } } ] } }
Fragment alternatives and recursive alternatives use the same bounded expression language:
&& and ||.<, >, <=, and >= for numeric operands; use == and != for numbers and language forms.!.null literals.The formal grammar spells out precedence, every built-in language-form constant, numeric syntax, and valid caller-variable names.
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.
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.
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:
| Work | Default | Hard ceiling |
|---|---|---|
| Numeric precision, scale, visible decimals | 1,024 | 4,096 |
| Compact exponent | 64 | 4,096 |
| Expression characters | 2,048 | 4,096 |
| Expression tokens | 256 | 512 |
| Nested expression groups | 32 | 64 |
| Generated-placeholder depth | 32 | 64 |
| One interpolated result | 262,144 UTF-16 code units | 1,048,576 |
| Cumulative generated expansion per locale attempt | 1,048,576 UTF-16 code units | 8,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.
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.
Example: "I read {{bookCount}} books."
Examples: Checkout.Title, Checkout.Submit, and Checkout.Cancel.
{ "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" } }
Natural-language keys can cover ordinary product copy while contextual keys handle legal text and surfaces where wording or context changes independently.
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.
Classic ICU MessageFormat expresses a plural message compactly:
{bookCount, plural, one {I read # book.} other {I read # books.}}
{ "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.
| Concern | Lokalized | Other 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. |
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.
A search returns a count, a caller-selected result limit, and an elapsed time. Wording changes when the count reaches that limit or the time falls below a supplied threshold. Neither threshold is a fixed translation key. The result summary and timing phrase vary independently, so translators can compose them without repeating every complete sentence.
JavaScript
const key = "Search completed."; // One result, below both limits strings.get(key, { resultCount: 1, resultLimit: 50, elapsedMilliseconds: 45, instantThreshold: 100, formattedResultCount: "1", formattedResultLimit: "50", formattedDuration: "45 ms" }); // => "Found 1 result instantly." // The caller's result limit is reached strings.get(key, { resultCount: 50, resultLimit: 50, elapsedMilliseconds: 1800, instantThreshold: 100, formattedResultCount: "50", formattedResultLimit: "50", formattedDuration: "1.8 s" }); // => "Found at least 50 results in 1.8 s." // No results; exactly at the timing threshold strings.get(key, { resultCount: 0, resultLimit: 50, elapsedMilliseconds: 100, instantThreshold: 100, formattedResultCount: "0", formattedResultLimit: "50", formattedDuration: "100 ms" }); // => "Found no results in 100 ms."
Java
String key = "Search completed."; // One result, below both limits String message1 = strings.get(key, Map.of( "resultCount", 1, "resultLimit", 50, "elapsedMilliseconds", 45, "instantThreshold", 100, "formattedResultCount", "1", "formattedResultLimit", "50", "formattedDuration", "45 ms" )); assertEquals("Found 1 result instantly.", message1); // The caller's result limit is reached String message2 = strings.get(key, Map.of( "resultCount", 50, "resultLimit", 50, "elapsedMilliseconds", 1800, "instantThreshold", 100, "formattedResultCount", "50", "formattedResultLimit", "50", "formattedDuration", "1.8 s" )); assertEquals("Found at least 50 results in 1.8 s.", message2); // No results; exactly at the timing threshold String message3 = strings.get(key, Map.of( "resultCount", 0, "resultLimit", 50, "elapsedMilliseconds", 100, "instantThreshold", 100, "formattedResultCount", "0", "formattedResultLimit", "50", "formattedDuration", "100 ms" )); assertEquals("Found no results in 100 ms.", message3);
Swift
let key: ExactString = "Search completed." // One result, below both limits let message1 = try strings.get(key, placeholders: [ "resultCount": .integer(1), "resultLimit": .integer(50), "elapsedMilliseconds": .integer(45), "instantThreshold": .integer(100), "formattedResultCount": .text("1"), "formattedResultLimit": .text("50"), "formattedDuration": .text("45 ms") ]) assert(message1 == "Found 1 result instantly.") // The caller's result limit is reached let message2 = try strings.get(key, placeholders: [ "resultCount": .integer(50), "resultLimit": .integer(50), "elapsedMilliseconds": .integer(1800), "instantThreshold": .integer(100), "formattedResultCount": .text("50"), "formattedResultLimit": .text("50"), "formattedDuration": .text("1.8 s") ]) assert(message2 == "Found at least 50 results in 1.8 s.") // No results; exactly at the timing threshold let message3 = try strings.get(key, placeholders: [ "resultCount": .integer(0), "resultLimit": .integer(50), "elapsedMilliseconds": .integer(100), "instantThreshold": .integer(100), "formattedResultCount": .text("0"), "formattedResultLimit": .text("50"), "formattedDuration": .text("100 ms") ]) assert(message3 == "Found no results in 100 ms.")
{ "Search completed.": { "translation": "Found {{results}} {{timing}}.", "placeholders": { "results": { "translation": "{{formattedResultCount}} {{resultNoun}}", "alternatives": [ { "resultCount == 0": "no results" }, { "resultCount >= resultLimit": "at least {{formattedResultLimit}} results" } ] }, "timing": { "translation": "in {{formattedDuration}}", "alternatives": [ { "elapsedMilliseconds < instantThreshold": "instantly" } ] }, "resultNoun": { "value": "resultCount", "translations": { "CARDINALITY_ONE": "result", "CARDINALITY_OTHER": "results" } } } } }
resultCount >= resultLimit compares two runtime values. elapsedMilliseconds < instantThreshold does the same for timing. The first matching alternative wins within each fragment; ordinary plural agreement supplies result or results when no exception applies.
| Format | Extra work for this example |
|---|---|
| ICU MessageFormat | Plural and select match plural categories, exact numbers, or keywords. The application would normally compute selectors such as resultBucket = "at-limit" and timingBucket = "instant". Static numeric boundaries, including legacy choice, do not express a comparison with another runtime argument. |
| MF2 | Multi-selector matching can combine those buckets with wildcard variants. Its stock number and string selectors still need the application to compute the comparisons, or a custom selector function to perform them. Combining selectors is supported; these dynamic comparisons are the extra work. |
| Fluent | Select expressions can compose both fragments once given suitable selector values. The application must supply the comparison results or register custom functions to derive them from the raw facts. |
| gettext | ngettext takes one plural count. The application must choose additional message keys or contexts for the result-limit and timing branches, or assemble separately translated fragments. |
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.")
{ "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.
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.")
{ "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.