fluent-core-langneg
Quick Reference
Package Overview
| Property | Value |
|---|---|
| Package | @fluent/langneg |
| Version | 0.7.0 |
| License | Apache-2.0 |
| Dependencies | None (zero runtime dependencies) |
| Install | npm install @fluent/langneg |
Exports
| Export | Type | Purpose |
|---|---|---|
negotiateLanguages |
Function | Match user-requested locales against available locales |
acceptedLanguages |
Function | Parse HTTP Accept-Language header into sorted locale array |
filterMatches |
Function | Low-level matching engine (exported but rarely used directly) |
Strategy Comparison
Given requested ["de-DE", "fr-FR"] and available ["it", "de", "en-US", "fr-CA", "de-DE", "fr", "de-AU"]:
| Strategy | Result | Behavior |
|---|---|---|
"filtering" (default) |
["de-DE", "de", "fr", "fr-CA"] |
ALL matches for ALL requested locales |
"matching" |
["de-DE", "fr"] |
BEST single match per requested locale |
"lookup" |
["de-DE"] |
SINGLE best match across all requested locales |
Critical Warnings
NEVER use the
"lookup"strategy without providingdefaultLocale— the function throws anErrorifdefaultLocaleis undefined with lookup strategy.
NEVER pass non-BCP 47 locale strings (e.g.,
"english","ENUS") — the negotiation algorithm expects standard BCP 47 tags like"en-US","fr-CA","sr-Latn".
ALWAYS provide
defaultLocalein production code — without it, negotiation can return an empty array if no matches are found, leaving the user with no translations.
negotiateLanguages()
Full Signature
import { negotiateLanguages } from "@fluent/langneg";
function negotiateLanguages(
requestedLocales: Readonly<Array<string>>,
availableLocales: Readonly<Array<string>>,
options?: {
strategy?: "filtering" | "matching" | "lookup";
defaultLocale?: string;
}
): Array<string>
Parameters
| Parameter | Type | Description |
|---|---|---|
requestedLocales |
Readonly<Array<string>> |
User's preferred locales (e.g., navigator.languages) |
availableLocales |
Readonly<Array<string>> |
Locales the application supports |
options.strategy |
"filtering" | "matching" | "lookup" |
Negotiation algorithm (default: "filtering") |
options.defaultLocale |
string |
Fallback locale; appended to results if not already present |
Return Value
Array<string> — locale identifiers ordered by preference.
defaultLocale Behavior
- For
"filtering"and"matching":defaultLocaleis appended to results if provided and not already present in the match results - For
"lookup":defaultLocaleis REQUIRED — the function throwsErrorif omitted; pushed to results if no matches found
Client-Side Usage
import { negotiateLanguages } from "@fluent/langneg";
const AVAILABLE_LOCALES = ["en-US", "fr", "de", "ja"];
const userLocales = negotiateLanguages(
navigator.languages, // e.g., ["de-DE", "en-US"]
AVAILABLE_LOCALES,
{ defaultLocale: "en-US" }
);
// With filtering: ["de", "en-US"] — "de-DE" matched "de" via subtag matching
Server-Side Usage
import { acceptedLanguages, negotiateLanguages } from "@fluent/langneg";
const requested = acceptedLanguages(req.headers["accept-language"] || "");
const supported = negotiateLanguages(requested, AVAILABLE_LOCALES, {
defaultLocale: "en-US",
});
Strategies In Depth
Filtering (Default)
Returns ALL available locales that match ANY requested locale. Most permissive — produces the longest fallback chain.
const result = negotiateLanguages(
["de-DE", "fr-FR"],
["it", "de", "en-US", "fr-CA", "de-DE", "fr", "de-AU"],
{ strategy: "filtering", defaultLocale: "en-US" }
);
// Result: ["de-DE", "de", "fr", "fr-CA", "en-US"]
// "de-AU" excluded — no "de-AU" in requested; "en-US" appended as defaultLocale
ALWAYS use filtering when building a fallback chain for FluentBundle sequences — it maximizes translation coverage.
Matching
Returns the BEST single match for EACH requested locale. One result per request entry.
const result = negotiateLanguages(
["de-DE", "fr-FR"],
["it", "de", "en-US", "fr-CA", "de-DE", "fr", "de-AU"],
{ strategy: "matching", defaultLocale: "en-US" }
);
// Result: ["de-DE", "fr", "en-US"]
// One match per requested locale; defaultLocale appended
ALWAYS use matching when you need a concise preference list without redundant variants.
Lookup
Returns the SINGLE best match across ALL requested locales. Most restrictive — exactly one result.
const result = negotiateLanguages(
["de-DE", "fr-FR"],
["it", "de", "en-US", "fr-CA", "de-DE", "fr", "de-AU"],
{ strategy: "lookup", defaultLocale: "en-US" }
);
// Result: ["de-DE"]
// Single best match; defaultLocale used only if zero matches found
ALWAYS use lookup when you need exactly one locale (e.g., selecting a date format library, choosing a single UI direction).
Strategy Selection Decision Tree
Need to select locales?
├── Need a full fallback chain for translations?
│ └── YES → Use "filtering" (default)
│ Returns all matching locales for maximum coverage
│
├── Need one best locale per user preference?
│ └── YES → Use "matching"
│ Returns best match per requested locale
│
└── Need exactly one locale for the entire app?
└── YES → Use "lookup"
Returns single best match
⚠ REQUIRES defaultLocale or throws Error
acceptedLanguages()
Parses HTTP Accept-Language headers into a sorted array of locale strings.
import { acceptedLanguages } from "@fluent/langneg";
const locales = acceptedLanguages("fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5");
// Result: ["fr-CH", "fr", "en", "de", "*"] — sorted by q-value descending
- Parses quality values (
q=) from the header string - Sorts by descending quality, preserving order for equal weights
- ALWAYS use on the server instead of
navigator.languages(which is unavailable in Node.js) - Throws
TypeErrorif the argument is not a string
BCP 47 Locale Identifiers
All locale strings in @fluent/langneg follow BCP 47 (IETF language tag standard):
| Format | Example | Components |
|---|---|---|
| Language | en, fr, de |
ISO 639 language code |
| Language + Region | en-US, fr-CA, de-DE |
Language + ISO 3166 country code |
| Language + Script | sr-Latn, zh-Hans |
Language + ISO 15924 script code |
Subtag Matching
The negotiation algorithms perform subtag matching — a requested "de-DE" will match an available "de" as a fallback. The library includes minimal likely-subtags data to resolve generic locales (e.g., requesting "en" can match both "en-GB" and "en-US").
Input locales are coerced to strings internally via Array.from(...).map(String).
Integration with @fluent/react
ALWAYS use negotiateLanguages to determine the locale order before creating FluentBundle instances:
import { negotiateLanguages } from "@fluent/langneg";
import { FluentBundle, FluentResource } from "@fluent/bundle";
import { ReactLocalization } from "@fluent/react";
const negotiated = negotiateLanguages(
navigator.languages,
["en-US", "fr", "de"],
{ defaultLocale: "en-US" }
);
const bundles = negotiated.map((locale) => {
const bundle = new FluentBundle(locale);
bundle.addResource(new FluentResource(MESSAGES[locale]));
return bundle;
});
const l10n = new ReactLocalization(bundles);
The order returned by negotiateLanguages determines the fallback chain — the first bundle is the preferred locale, subsequent bundles serve as fallbacks for missing translations.
Reference Links
- references/methods.md — Full API signatures for negotiateLanguages, acceptedLanguages, filterMatches
- references/examples.md — Client-side, server-side, and per-strategy usage examples
- references/anti-patterns.md — Common mistakes and what to do instead
Official Sources
- https://github.com/projectfluent/fluent.js/blob/main/fluent-langneg/README.md
- https://github.com/projectfluent/fluent.js/blob/main/fluent-langneg/src/negotiate_languages.ts
- https://github.com/projectfluent/fluent.js/blob/main/fluent-langneg/src/accepted_languages.ts
- https://github.com/projectfluent/fluent.js/blob/main/fluent-langneg/src/index.ts