@itayost/il
v1.1.0
Published
Israeli domain primitives. Zero dependencies.
Readme
@itayost/il
Israeli domain primitives. Zero runtime dependencies, ESM only.
Phone numbers
An Israeli phone number is written five different ways in the wild, and a
normalized one is indistinguishable from a raw one if both are just string.
This package gives the canonical form its own type, so passing an unparsed
value where a canonical one is required is a compile error rather than a
lookup that silently misses.
import { parseIlPhone, toChatId, toLocal, toStorage } from "@itayost/il";
const phone = parseIlPhone("050-123-4567"); // IlPhone | null
if (phone) {
toChatId(phone); // "[email protected]"
toLocal(phone); // "050-123-4567"
toStorage(phone, "bare"); // "972501234567"
}parseIlPhone accepts local, international with or without a plus, and any of
those with spaces, dashes or parentheses. It returns null rather than
throwing, and never returns a half-normalized value. Landlines,
non-geographic 07x numbers and mobiles written without a trunk zero all
parse.
It deliberately rejects WhatsApp Chat IDs, LIDs, star short codes and non-Israeli numbers instead of coercing them into a number that looks plausible but reaches someone else.
The canonical form is an in-memory contract, not a storage format — toStorage
renders back into whatever format your database already holds, so adopting this
package needs no migration.
Chat IDs
import { fromChatId } from "@itayost/il";
fromChatId("[email protected]"); // IlPhone
fromChatId("[email protected]"); // null, a group
fromChatId("123456789012345@lid"); // null, an opaque identifierMoney
Nine formatters for the same shekel amount existed across the portfolio and
disagreed on the glyph (₪ or ש״ח), whether it leads or trails, which locale
groups the thousands, how many agorot to show, and what an unusable input
renders as. Two grouped digits with no locale at all, so their output followed
the server rather than the reader.
import { formatIls, formatAgorot } from "@itayost/il";
formatIls(150); // "150 ₪"
formatIls(1234.5); // "1,234.5 ₪"
formatIls(1234.5, { decimals: 2 }); // "1,234.50 ₪"
formatIls(585000, { compact: true }); // "₪585K"
formatIls(150, { locale: "en-US" }); // "₪150"
formatAgorot(15000); // "150 ₪"The package states no opinion about the glyph or where it sits. Intl decides
both from the locale, which is why Hebrew trails the symbol and English leads
it. Any locale works, so a multilingual Consumer passes its own and gets the
right form rather than a Hebrew string it has to undo.
Unusable input returns null rather than a dash, because the dash is copy —
Consumers variously want -, — and חינם — and that choice belongs to the
screen. Nothing throws. A number that failed to parse upstream, a blank string
from an empty column, a division by zero: all of them are an absence, never a
price of NaN ₪ or a free one.
Numbers may arrive as strings, since Postgres numeric and Prisma Decimal
columns serialize that way.
The output separates the amount from the symbol with a non-breaking space, so a
narrow column cannot wrap the two apart. The invisible bidi marks Intl adds
around Hebrew currency are stripped, since they corrupt character-count maths
and leak into WhatsApp messages and CSV exports; pass bidiMarks: true to keep
them when embedding into mixed-direction text.
Israel time
Twelve repos reason about Asia/Jerusalem and five wrote their own helper. They
diverged in ways that surface twice a year or once a day: one derives the
calendar date from en-US parts and another from an en-CA formatter, and one
carries a % 24 because hour12: false reports midnight as hour 24 on some ICU
builds.
import { israelParts, israelDate, startOfIsraelDay, formatIlDate, formatIlTime } from "@itayost/il";
israelParts("2026-01-14T22:30:00Z");
// { year: 2026, month: 1, day: 15, hour: 0, minute: 30, weekday: "Thu", minutesOfDay: 30 }
israelDate("2026-01-14T22:30:00Z"); // "2026-01-15" — already tomorrow in Israel
startOfIsraelDay(); // UTC midnight of today's Israel date, for a date-only column
formatIlDate(new Date()); // "יום רביעי, 4 במרץ 2026"
formatIlDate(new Date(), { style: "short" }); // "04.03.2026"
formatIlTime(new Date()); // "16:30"Everything is Intl, so there is no tz database to ship and no date-fns-tz to
install. Midnight is hour 0, never 24. An unusable date returns null
rather than throwing or resolving to the first of January.
The formatters take a locale rather than naming a language, so a multilingual
Consumer asks for what it wants instead of undoing a Hebrew string. The Hebrew
output is byte-identical to what date-fns with its Hebrew locale produced.
israelParts accepts a second argument if you need a different zone; Quiet
Hours uses it, which is how the two stay consistent.
Quiet Hours
When a bot must not message a person: a nightly window plus the Shabbat blackout, always evaluated in Israel local time.
import { isQuiet } from "@itayost/il";
isQuiet(new Date()); // { quiet: true, reason: "shabbat" } | { quiet: false }Defaults to 09:00–21:00 and Friday 16:00 → Saturday 20:30 in Asia/Jerusalem.
Pass a config to change the window or the boundaries.
Licence
MIT
