lookup-closest-locale
lookup-closest-locale chooses the first usable locale key from an object. Give it `pt-BR` and an object containing `pt`, and it progressively removes subtags until `pt` matches; give it an ordered array and it tries each requested locale in turn. That is essentially the RFC 4647 lookup shape in 20 lines of CommonJS. It returns the matching key or undefined and does not parse Accept-Language weights, canonicalize case, compare likely subtags, load translations, or provide a default locale.
Useful as a frozen, tiny parent-locale lookup when your inputs are already clean and its exact behavior is part of an existing application. For new request negotiation, use a maintained matcher or an Accept-Language parser and avoid the truthy-object trap entirely.
Use it if
- You already have normalized BCP 47 locale tags and need exact-then-parent fallback such as zh-Hant-TW to zh-Hant to zh
- Your translation catalogs live in a plain object whose available entries all have truthy values
- You need to try a short, preordered list of user locale preferences without adding an i18n framework
- You maintain format-message code that already uses this helper and want to preserve its matching behavior
- You receive raw Accept-Language headers: this package does not parse q weights, wildcards, malformed ranges, or header ordering rules
- Your locale tags are not already canonicalized: the source says matching is case-sensitive, so EN-us does not match en-US
- Your availability object stores falsy values such as an empty string, 0, false, or null: the implementation tests available[candidate] rather than key ownership and treats those keys as absent
- You need standards-complete locale negotiation: it only removes dash-separated suffixes and does not implement best-fit matching, likely-subtag comparison, or Intl.Locale behavior
- You want an actively owned dependency: 6.2.0 was published in 2018 and the monorepo README explicitly says the project is looking for maintainers
Setup reality
npm install lookup-closest-locale installs a 1,337-byte, three-file package with no runtime dependencies, peer dependencies, native code, credentials, or configuration. It ships a CommonJS function and an `export =` TypeScript declaration, so `const lookupClosestLocale = require('lookup-closest-locale')` is the least surprising import. TypeScript projects using `esModuleInterop` can default-import it, while strict ESM tooling may need `createRequire` or bundler interop because the package declares neither `module` nor `exports`. The difficult part is preparing valid input. Locale tags must already be normalized and case must match your object keys exactly; use `Intl.getCanonicalLocales` before lookup if input comes from browsers, profiles, or URLs. The second argument is not a set despite behaving like one. The implementation accepts a plain object and checks the truthiness of `available[candidate]`, so an intentionally empty catalog is invisible. It also reads inherited properties, meaning an ordinary `{}` can unexpectedly match names such as `toString`; build a null-prototype lookup object or ensure every candidate is a real locale tag. The requested array is only an ordered list. Passing an Accept-Language header string makes the whole header one candidate and does not respect q values. No match returns undefined, so every call site needs an explicit default or a deliberate error. The package contains no README of its own in the published tarball and its parent project is seeking maintainers, so pin behavior with local tests before depending on edge cases.
Patterns
Fall back from a region to its languagematch-parent-locale
const lookupClosestLocale = require('lookup-closest-locale');
const catalogs = {
en: { greeting: 'Hello' },
fr: { greeting: 'Bonjour' },
};
const locale = lookupClosestLocale('fr-CA', catalogs);
console.log(locale); // 'fr'The function removes trailing dash-separated subtags until it finds a truthy object value.
Try locales in preference orderprefer-locale-list
const lookupClosestLocale = require('lookup-closest-locale');
const available = { de: true, en: true };
const match = lookupClosestLocale(['fr-CA', 'de-AT', 'en-US'], available);
console.log(match); // 'de'The array must already be sorted by preference. The package does not interpret q weights.
Use the returned key to select a catalogchoose-catalog
const lookupClosestLocale = require('lookup-closest-locale');
const catalogs = {
en: require('./locales/en.json'),
'pt-BR': require('./locales/pt-BR.json'),
pt: require('./locales/pt.json'),
};
const key = lookupClosestLocale('pt-BR', catalogs);
const messages = key ? catalogs[key] : catalogs.en;Lookup returns the matching key, not the catalog value. Handle undefined before indexing.
Provide an explicit defaultprovide-default-locale
const lookupClosestLocale = require('lookup-closest-locale');
const available = { en: true, es: true };
const selected = lookupClosestLocale(userLocales, available) ?? 'en';There is no built-in default. `??` is clearer than `||` because successful keys are non-empty strings.
Canonicalize requested locales before matchingcanonicalize-tags
const lookupClosestLocale = require('lookup-closest-locale');
function canonicalize(values) {
return Intl.getCanonicalLocales(values);
}
const available = { 'en-US': true, 'fr-CA': true };
const match = lookupClosestLocale(canonicalize(['EN-us']), available);Matching is case-sensitive. Intl.getCanonicalLocales also throws RangeError for malformed tags, so catch it at an untrusted-input boundary.
Build a truthy availability object from keysbuild-availability-index
const lookupClosestLocale = require('lookup-closest-locale');
const localeKeys = ['en', 'fr', 'zh-Hant'];
const available = Object.fromEntries(localeKeys.map((key) => [key, true]));
const match = lookupClosestLocale('zh-Hant-TW', available);Use true markers when only availability matters. Falsy values are treated as missing even when the property exists.
Use a null-prototype availability objectavoid-prototype-matches
const lookupClosestLocale = require('lookup-closest-locale');
const available = Object.assign(Object.create(null), {
en: true,
fr: true,
});
const match = lookupClosestLocale(requested, available);The implementation reads `available[candidate]`; a normal object inherits truthy names such as toString.
Represent an intentionally empty catalogpreserve-empty-catalog
const lookupClosestLocale = require('lookup-closest-locale');
const catalogs = { en: '', fr: 'messages-fr' };
const availability = { en: true, fr: true };
const key = lookupClosestLocale('en-US', availability);
const catalog = key === undefined ? undefined : catalogs[key];Passing catalogs directly would skip `en` because its value is an empty string. Keep availability separate when values can be falsy.
Match keys held in a Mapconvert-map-input
const lookupClosestLocale = require('lookup-closest-locale');
const catalogs = new Map([
['en', { hello: 'Hello' }],
['ja', { hello: 'こんにちは' }],
]);
const available = Object.fromEntries([...catalogs.keys()].map((key) => [key, true]));
const key = lookupClosestLocale('ja-JP', available);
const catalog = key ? catalogs.get(key) : undefined;The second argument must support property access; passing a Map directly never matches its entries.
Parse an Accept-Language header before lookupparse-accept-language
const parser = require('accept-language-parser');
const lookupClosestLocale = require('lookup-closest-locale');
const requested = parser.parse(req.headers['accept-language'] || '')
.map(({ code, script, region }) => [code, script, region].filter(Boolean).join('-'));
const locale = lookupClosestLocale(requested, { en: true, fr: true }) ?? 'en';Do not pass the raw header to lookup-closest-locale. A parser is needed for q weights and header syntax.
Import with TypeScript's CommonJS syntaxtypescript-import
import lookupClosestLocale = require('lookup-closest-locale');
const available: Record<string, true> = { en: true, 'en-GB': true };
const match: string | undefined = lookupClosestLocale('en-GB-x-demo', available);The bundled declaration uses `export =`. A default import depends on esModuleInterop or allowSyntheticDefaultImports.
Turn no match into a clear errorfail-on-no-match
const lookupClosestLocale = require('lookup-closest-locale');
function requireSupportedLocale(requested, available) {
const match = lookupClosestLocale(requested, available);
if (match === undefined) {
throw new RangeError('No supported locale matched');
}
return match;
}The package returns undefined silently, which can otherwise become a confusing property lookup later.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @formatjs/intl-localematcher | npm | You need an Intl.LocaleMatcher ponyfill with lookup and best-fit matching over canonical locale lists |
| locale-matcher | npm | You want a small maintained matcher that also normalizes locale input |
| negotiator | npm | You need HTTP Accept-Language parsing and preference ordering alongside other content-negotiation headers |
| accept-language-parser | npm | You mainly need to parse weighted Accept-Language headers before selecting a supported language |