anylocale API reference
Overview
anylocale reads what native Intl knows about how a locale behaves — text direction, the first day of its week, which days are the weekend, which calendars and time zones it uses, whether it counts hours to 12 or 24, and which digits it writes with.
Everyone hardcodes this and everyone gets it wrong. Your runtime already ships the correct table for 200+ locales; anylocale is the thin reader. One export, no data files, no config.
import { anylocale } from 'anylocale'
anylocale('ar-EG').direction // "rtl"
anylocale('en-GB').weekStart // 1 — Monday
anylocale('en-US').weekStart // 7 — Sunday, same language
anylocale('fa-IR').weekend // [5] — Friday only
anylocale('ar-EG').timeZones // ["Africa/Cairo"]This is the behaviour side of a locale. For the naming side — what a code is called in a given language — reach for anyaround instead.
Install
npm install anylocale
# or
pnpm add anylocale
# or
yarn add anylocaleZero dependencies, ~1kb gzipped, ESM and CJS builds with types. Or take the whole family at once with npm install anyfamily.
anylocale()
The single entry point. Pass a BCP 47 tag, or an array used as a fallback chain.
anylocale(tag)
anylocale([tag, fallback])
anylocale('pt-BR') // the record
anylocale(['xx-Nope', 'de-DE']).tag // "de-DE"Fields are computed on access, so reading direction never asks the runtime for calendars or time zones. They are still plain own enumerable properties, so spreading and JSON.stringify behave exactly as you would expect.
const { direction, weekStart } = anylocale(navigator.language)
JSON.stringify(anylocale('en-US'))
// {"tag":"en-US","direction":"ltr","weekStart":7,…}Throws TypeError on an empty fallback chain, RangeError when no tag is well-formed BCP 47, and a plain Error when the runtime has no Intl Locale Info at all.
Fields
tagstringthe canonical tag that was resolved — "en-us" → "en-US"
direction"ltr" | "rtl"text direction of the locale's script
weekStart1–7first day of the week, ISO numbering
weekendnumber[]days counted as the weekend, ISO numbering
minimalDaysnumberdays of a week that must fall in a year for it to be that year's first week
calendarsstring[]usable calendars, preferred first
timeZonesstring[]IANA zones for the region; empty for language-only tags
hourCyclesstring[]"h12", "h23", … preferred first
numberingSystemsstring[]"latn", "arab", … preferred first
There are no options — the tag is the whole input. Everything a locale can tell you is on the record.
Week & weekend
weekStart and weekend use ISO numbering: 1 is Monday, 7 is Sunday. That is CLDR's convention, and it is not JavaScript's — Date.prototype.getDay() returns 0 for Sunday.
const iso = anylocale('en-US').weekStart // 7
const js = iso % 7 // 0 — what getDay() would sayTo lay out a calendar, rotate the week so it opens on the locale's first day:
const { weekStart, weekend } = anylocale(locale)
const days = Array.from({ length: 7 }, (_, i) => ((weekStart - 1 + i) % 7) + 1)
// en-US -> [7, 1, 2, 3, 4, 5, 6] Sunday first
// en-GB -> [1, 2, 3, 4, 5, 6, 7] Monday first
// ar-EG -> [6, 7, 1, 2, 3, 4, 5] Saturday first
const isWeekend = new Set(weekend)
days.map((d) => ({ day: d, weekend: isWeekend.has(d) }))minimalDaysis the ISO-8601 week-numbering rule: how many days of a week must fall inside a year for that week to count as the year's first. Most locales say 1; a few say 4.
What breaks without this
Every one of these is a real assumption people encode by hand, and each is wrong somewhere.
Same language, different week
en-US starts the week on Sunday, en-GB on Monday. A table keyed on language is wrong for half the English-speaking world — the region subtag is what decides.
The weekend is not always a pair
fa-IR has a one-day weekend: Friday. Code that destructures two days, or assumes weekend.length === 2, breaks on it.
RTL does not imply Arabic digits
he-IL is right-to-left and uses Latin numerals; ar-EG is right-to-left and uses Arabic-Indic ones. Direction and numbering system are independent.
Language does not decide the clock either
en-US is a 12-hour locale, en-GB a 24-hour one. Same language again.
ISO numbering is not getDay()
weekStart and weekend are ISO: 1 is Monday, 7 is Sunday. Date.prototype.getDay() returns 0 for Sunday. Convert with iso % 7 before comparing.
Recipes
Copy, paste, move on.
// Set document direction without a hand-kept RTL language list
document.documentElement.dir = anylocale(userLocale).direction
// …or in a React tree
<html lang={locale} dir={anylocale(locale).direction}>
// Order the columns of a date picker
const start = anylocale(locale).weekStart
const days = Array.from({ length: 7 }, (_, i) => ((start - 1 + i) % 7) + 1)
// Highlight weekend cells — not always Saturday and Sunday
const weekend = new Set(anylocale(locale).weekend)
const isWeekend = (isoDay) => weekend.has(isoDay)
// 12- or 24-hour clock, per the locale rather than per the language
const use12h = anylocale(locale).hourCycles[0] === 'h12'
// Offer the calendar the region actually uses
anylocale('fa-IR').calendars[0] // "persian"
anylocale('th-TH').calendars[0] // "buddhist"
// Suggest a default time zone from the user's locale
anylocale('ar-EG').timeZones[0] // "Africa/Cairo"
// Degrade gracefully where the API is missing
const dir = anylocale.supported ? anylocale(locale).direction : 'ltr'React / Next.js
The most common use is the document direction. Read it once, high in the tree, and let the rest of the app inherit it.
import { anylocale } from 'anylocale'
export default function RootLayout({ children, params: { locale } }) {
const dir = anylocale.supported ? anylocale(locale).direction : 'ltr'
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
)
}anyfamily-react exposes the same reader as useAnylocale, taking the locale from the shared AnyfamilyProvider when you do not pass one.
import { AnyfamilyProvider, useAnylocale } from 'anyfamily-react'
function WeekHeader() {
const { weekStart, weekend } = useAnylocale()
// …
}
<AnyfamilyProvider locale="ar-EG">
<WeekHeader />
</AnyfamilyProvider>SSR
anylocale is pure and synchronous — no clock, no state, no DOM — so server and client render identically for the same tag.
One caveat that is about the runtime rather than the package: server and browser can ship different ICU builds, and support itself differs between them. Pass an explicit tag rather than reading the ambient locale, and if you branch on anylocale.supported, do it in a place where a mismatch cannot cause a hydration difference — or gate the branch behind an effect.
// Deterministic: the tag comes from the route, not from the environment
export default function Layout({ params: { locale } }) {
return <html lang={locale} dir={anylocale(locale).direction}>…</html>
}Locales
Any valid BCP 47 tag. A fallback chain resolves to the first tag the runtime has data for, not merely the first that parses — "xx-Nope" is well-formed BCP 47 and would otherwise win.
anylocale('pt-BR').tag // "pt-BR"
anylocale('en-us').tag // "en-US" — canonicalised
anylocale(['xx-Nope', 'de-DE']).tag // "de-DE"
anylocale(['zz-Fake']).tag // "zz-Fake" — nothing has dataWhen no tag in the chain has data, the first well-formed one is used and the runtime answers with its own defaults. That beats throwing: you still get a usable direction and week.
Support flag
anylocale.supported is true when the runtime exposes Intl Locale Info in either shape. Where it is false, every call throws — so branch on it rather than on a version table.
import { anylocale } from 'anylocale'
const dir = anylocale.supported ? anylocale(tag).direction : 'ltr'Through the anyfamily meta-package the flag is reached the same way — anylocale.supported — since every package now carries its own.
Compatibility
Intl Locale Info reached Stage 4 (ES2026), but it was standardised twice: first as properties (locale.weekInfo), then as methods (locale.getWeekInfo()). Engines are split — Node 22 ships only the properties.
anylocale reads whichever shape it finds, so you never write loc.getWeekInfo?.() ?? loc.weekInfo yourself. Because support is uneven and still moving, the package deliberately ships no version table: feature-detect with anylocale.supported.
The package itself runs anywhere Node 18+ runs; the data is what may be absent. CI runs the suite on Node 20, 22 and 24, skipping the data-dependent tests wherever the API is missing.
Limitations
A few things worth knowing before you ship:
It reads, it does not format
anylocale hands you facts. Turning a code into a readable name — "US" into "United States" — is anyaround's job, and formatting a date with them is anywhen's.
Values track the runtime's CLDR
Everything here comes from the ICU data your engine ships. Exact calendar and time-zone lists can shift between versions, so test behaviour rather than exact arrays.
Time zones need a region
A language-only tag has no region to look up, so timeZones is empty for "en" and populated for "en-GB". That is the data, not a bug.
Support is uneven and moving
The proposal was standardised twice — properties first, then methods — and engines are split. anylocale reads either shape, but where neither exists it throws. Branch on anylocale.supported.