npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@randajan/say

v2.1.0

Published

Tiny, chainable phrase lookup helper for simple localization with fallbacks.

Readme

@randajan/say

Tiny i18n dictionary with callable sugar and number inflection.

say("hello")
say.num("hours", 3)
say.dateTime(Date.now())
Done.

Install

npm install @randajan/say

Quick Start

import Lexicon from "@randajan/say";
import en from "@randajan/say/defaults/locales/en";
import cs from "@randajan/say/defaults/locales/cs";

const lexicon = new Lexicon({
    locales: [en, cs],
    translations: {
        hello: ["Hello", "Ahoj"],
        hours: ["{#} hour[s]", "{#} hodin[|a|y|]"]
    }
});

const sayEn = lexicon.select("en");
const sayCs = lexicon.select("cs");

sayEn("hello");        // "Hello"
sayEn.num("hours", 1); // "1 hour"
sayEn.num("hours", 3); // "3 hours"

sayCs("hello");        // "Ahoj"
sayCs.num("hours", 1); // "1 hodina"
sayCs.num("hours", 3); // "3 hodiny"
sayCs.num("hours", 5); // "5 hodin"

sayEn.date("2024-01-02T03:04:05.678Z", { timeZone: "UTC" }); // localized date
sayCs.time("bad-input"); // "Neplatné datum" (from locale.invalidDate)

Exports

Main package

import Lexicon, { Lexicon as LexiconClass, Locale, Say } from "@randajan/say";

default export is Lexicon.

Locale defaults

import en from "@randajan/say/defaults/locales/en";
import de from "@randajan/say/defaults/locales/de";
import cs from "@randajan/say/defaults/locales/cs";
import sk from "@randajan/say/defaults/locales/sk";
import pl from "@randajan/say/defaults/locales/pl";

Mental Model

  • Lexicon: translations + fallback graph.
  • Locale: number + date/time formatting + inflection selector.
  • Say: callable view bound to one locale.

select(localeId) gives you a say function:

const say = lexicon.select("en");
say("hello");
say.or("missing", "<fallback>");
say.num("hours", 2);
say.date(new Date());
say.time(Date.now());
say.dateTime("2024-01-02T03:04:05.678Z");

API

new Lexicon({ locales, translations, parent } = {})

  • locales: array of Locale | string | object.
  • translations: Record<string, string[]> aligned by locales index.
  • parent: optional parent Lexicon.

Locale IDs are matched exactly ("en" is not "en-GB").

lexicon.lookup(localeId, phraseId, throwError = true)

Lookup order:

  1. self
  2. siblings
  3. parent

If nothing is found and throwError === true, throws.

lexicon.resolveLocale(localeId, throwError = true)

Resolve order:

  1. self
  2. parent
  3. siblings

If not found and throwError === true, throws.

lexicon.select(localeId)

Returns a callable Say instance bound to resolved Locale.

lexicon.extend({ locales, translations } = {})

Creates a child Lexicon with parent = this.

lexicon.addSibling(lexicon)

Adds sibling lexicon for lookup fallback.

say(phraseId)

Returns phrase or fallback "{phraseId}".

say.has(phraseId)

Returns boolean.

say.or(phraseId, fallback)

Returns phrase or custom fallback.

say.num(phraseId, num, opt = {})

Uses locale inflection on phrase template.

If phrase is missing, fallback template is used: {{#} phraseId}.

say.date(value = Date.now(), opt = {})

Returns localized date string (toLocaleDateString).

say.time(value = Date.now(), opt = {})

Returns localized time string (toLocaleTimeString).

say.dateTime(value = Date.now(), opt = {})

Returns localized date-time string (toLocaleString).

value can be Date, timestamp, or date-like string.

If value is invalid date, output is:

  • opt.invalidDate (if provided)
  • otherwise locale.invalidDate

say.all(text)

Replaces all letter words in text using say.

new Locale({ ... })

Main options:

  • id: locale ID for toLocaleString.
  • inflectSelector(number, patternArray).
  • numberPlaceholder (default "{#}").
  • nanSymbol (default "?"), nanSelect.
  • infinitySymbol (default "\\u221E"), infinitySelect.
  • invalidDate (default "?"): fallback for invalid date input in say.date*.

Phrase Patterns

Use "{#}" for number and one bracket pattern for inflection.

Examples:

"{#} hour[s]"         // en default: 1 hour, 2 hours
"{#} hodin[|a|y|]"    // cs default: 1 hodina, 2 hodiny, 5 hodin
"{#} [is|are] open"   // custom selector can pick a full word

Pattern content goes to inflectSelector(number, patternArray).

say.num options

  • decimal: number or [min, max] for fraction digits.
  • noZero: hide output when rounded number is zero.
  • noNaN: hide output for NaN.
  • noInfinity: hide output for Infinity.
  • noBS: hide all non-finite and zero shortcuts.
  • nanSymbol: override locale NaN symbol.
  • infinitySymbol: override locale infinity symbol.

say.date, say.time, say.dateTime options

All Intl.DateTimeFormatOptions are supported and forwarded to native formatter.

Extra option:

  • invalidDate: override fallback for invalid date values.

Example:

say.dateTime("2024-01-02T03:04:05.678Z", {
    timeZone: "UTC",
    dateStyle: "short",
    timeStyle: "medium"
});

say.date("bad-input"); // from locale.invalidDate
say.date("bad-input", { invalidDate: "<custom>" }); // "<custom>"

Defaults Strategy

Built-in locale defaults are intentionally small:

  • en, de share English inflection selector.
  • cs, sk share Czech-style selector.
  • pl has dedicated selector.

If you need strict grammar rules, pass your own Locale instances.

License

MIT