mrkeyoor.com_
Sat 08 Aug 22:02 UTC
npmUtilsupdated 08 Aug 2026

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.

Verdict

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.

API stability5/5The entire public API is one CommonJS function taking a string, string array, or undefined plus an availability object, and returning a string or undefined. Version 6.2.0 has been on npm since 2018-09-12, while the published source and bundled types agree on that signature. Its long freeze makes behavior highly predictable, though it also freezes case sensitivity, truthy-value checks, and inherited-property behavior.
Docs2/5The package metadata links to its directory in the format-message monorepo, but the published 6.2.0 tarball contains only index.js, package.json, and types.d.ts, with no package README or usage examples. The short source comment identifies RFC 4647 lookup, normalized-tag assumptions, and case sensitivity, which is valuable, but users must read the implementation to discover falsy-value and prototype-chain behavior.
Maintenance1/5npm dates the current 6.2.0 release to 2018-09-12. The parent repository was pushed on 2026-02-15 and is not archived, but its top-level README prominently says the current maintainers are looking for someone to take over maintenance. That recent repository activity does not establish active stewardship of this three-file subpackage, so consumers should treat it as stable but effectively unattended.
Ecosystem3/5The package recorded 3,689,073 downloads in the measured week and belongs to the format-message monorepo, so it remains common in internationalization dependency trees. It has zero runtime dependencies and bundled TypeScript declarations, which ease retention. Its role is deliberately narrow, however, and it offers no Accept-Language parser, framework middleware, translation loader, Intl integration, or extension mechanism.

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
Skip it if

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

PackageRegistryPick it when
@formatjs/intl-localematchernpmYou need an Intl.LocaleMatcher ponyfill with lookup and best-fit matching over canonical locale lists
locale-matchernpmYou want a small maintained matcher that also normalizes locale input
negotiatornpmYou need HTTP Accept-Language parsing and preference ordering alongside other content-negotiation headers
accept-language-parsernpmYou mainly need to parse weighted Accept-Language headers before selecting a supported language