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

@jimray/human-eyes

v0.2.0

Published

Format data for people. Human-friendly formatting functions and web components based on Django's humanize filters.

Readme

Human Eyes

Legible data for real people, JavaScript filtering based on Django's humanize filters.

Web Components make it possible, easy even, to build the kinds of complex modules we used to rely on backend framewoks for. It's nice to keep them easy to read.

Two ways to use it:

  • Functions — pure JS you call from your own code (pluralize(2)"s")
  • Web Components — reactive custom elements that re-render when attributes change (<human-eyes-pluralize count="2" word="item"> → "2 items")

Use the functions when you're building strings in JS that you want to make more human readable.

Use the components when you want live-updating text on the page without writing any rendering logic.

Zero dependencies. Pure ES modules. Works in any browser or runtime — just drop the files in and import.

Disclaimer: This package, including this README but not this specific paragraph, is almost 100% prompt-engineered, machine generated, agent-assisted — however we're referring to code written by an LLM these days (in this case Claude Opus 4.6). I find the term "vibe coding" obscures more than it enlightens but if that's what you're feeling, go for it. Like almost everyone I know in tech right now, I've got some Big Feelings about agentic coding, this project is part of working that out.

I've reviewed the code to the best of my (not at all exceptional or even very above average) abilities.

Quick start

Copy the files you need into your project. Set up an import map so your JS can use clean bare specifiers — no bundler required:

<script type="importmap">
{
  "imports": {
    "human-eyes/pluralize": "./human-eyes/pluralize.js",
    "human-eyes/timesince": "./human-eyes/timesince.js",
    "human-eyes/ordinal":   "./human-eyes/ordinal.js"
  }
}
</script>
<script type="module">
  import { pluralize } from "human-eyes/pluralize";
  import { timesince } from "human-eyes/timesince";
  import { ordinal }   from "human-eyes/ordinal";

  `${count} file${pluralize(count)}`;       // "3 files"
  `joined ${timesince(user.createdAt)} ago`; // "joined 2 months, 3 days ago"
  `finished ${ordinal(position)}`;           // "finished 1st"
</script>

Each file is self-contained — grab one, grab all of them, no build step required. Map only the functions you use.

Functions

pluralize

Returns a plural suffix based on a numeric value. Mirrors Django's pluralize filter.

import { pluralize } from "human-eyes/pluralize";

pluralize(1);              // ""
pluralize(2);              // "s"
pluralize(1, "es");        // ""
pluralize(2, "es");        // "es"
pluralize(1, "y,ies");     // "y"
pluralize(2, "y,ies");     // "ies"
pluralize(0, "person,people"); // "people" (full-word replacement)

timesince / timeuntil

Formats the time difference between two dates as a human-readable string with up to two levels of granularity. Mirrors Django's timesince and timeuntil filters.

import { timesince, timeuntil } from "human-eyes/timesince";

timesince("2024-01-01", "2024-01-05T06:30:00");
// "4 days, 6 hours"

timesince("2024-06-01", "2024-06-01T00:00:30");
// "0 minutes" (less than 60 seconds)

timeuntil("2024-12-25", "2024-12-20");
// "5 days"

The second argument defaults to new Date() if omitted.


filesizeformat

Formats a number of bytes as a human-readable file size using 1024-based units. Mirrors Django's filesizeformat filter.

import { filesizeformat } from "human-eyes/filesizeformat";

filesizeformat(0);           // "0 bytes"
filesizeformat(1);           // "1 byte"
filesizeformat(1024);        // "1 KB"
filesizeformat(123456789);   // "117.7 MB"
filesizeformat(1073741824);  // "1 GB"

truncatechars / truncatewords

Truncates strings by character count or word count, appending "…" when truncated. Mirrors Django's truncatechars and truncatewords filters.

import { truncatechars, truncatewords } from "human-eyes/truncate";

truncatechars("Joel is a slug", 7);   // "Joel i…"
truncatechars("short", 10);           // "short" (no truncation)

truncatewords("Joel is a slug", 2);   // "Joel is …"
truncatewords("hello\nworld foo", 2); // "hello world …" (newlines removed)

The character limit for truncatechars includes the ellipsis character. truncatewords removes newlines before splitting, matching Django's behavior.


yesno

Maps truthy, falsy, and null/undefined values to custom strings. Mirrors Django's yesno filter.

import { yesno } from "human-eyes/yesno";

yesno(true, "yeah,no,maybe");   // "yeah"
yesno(false, "yeah,no,maybe");  // "no"
yesno(null, "yeah,no,maybe");   // "maybe"
yesno(null, "yeah,no");         // "no" (null falls back to false value)
yesno(true);                    // "yes" (defaults)

The argument is a comma-separated string: "trueValue,falseValue[,nullValue]". If only two values are given, null/undefined maps to the false value.


floatformat

Formats floating-point numbers with control over decimal places. Mirrors Django's floatformat filter.

import { floatformat } from "human-eyes/floatformat";

// Default: 1 decimal, strip trailing zeros
floatformat(34.23234);          // "34.2"
floatformat(34.0);              // "34"

// Positive arg: always show N decimals
floatformat(34.23234, 3);       // "34.232"
floatformat(34.0, 3);           // "34.000"

// Zero: round to integer
floatformat(39.56, 0);          // "40"

// Negative arg: up to N decimals, strip trailing zeros
floatformat(34.232, -3);        // "34.232"
floatformat(34.0, -3);          // "34"

// "g" suffix: enable thousand-separator grouping
floatformat(34232.34, "2g");    // "34,232.34"

slugify

Converts a string to a URL-friendly slug. Mirrors Django's slugify filter.

import { slugify } from "human-eyes/slugify";

slugify("Joel is a slug");    // "joel-is-a-slug"
slugify("Héllo Wörld");       // "hello-world"
slugify("  Hello, World!  "); // "hello-world"
slugify("hello_world");       // "hello_world" (underscores preserved)

Uses Unicode NFKD normalization to decompose accented characters, then strips combining marks.


oxfordComma

Joins a list of items into a human-readable string with an Oxford comma. Inspired by humanize-plus's oxford function. Not a Django built-in, but a natural complement to the other filters.

import { oxfordComma } from "human-eyes/oxford";

oxfordComma(["apple"]);                          // "apple"
oxfordComma(["apple", "banana"]);                // "apple and banana"
oxfordComma(["apple", "banana", "cherry"]);      // "apple, banana, and cherry"

// Limit the number of items shown
oxfordComma(["a", "b", "c", "d", "e"], 3);       // "a, b, c, and 2 others"
oxfordComma(["a", "b", "c", "d"], 3);            // "a, b, c, and 1 other"

// Custom suffix
oxfordComma(["a", "b", "c"], 2, "and more");     // "a, b, and more"

ordinal

Converts an integer to its ordinal string representation. Mirrors Django's ordinal filter.

import { ordinal } from "human-eyes/ordinal";

ordinal(1);    // "1st"
ordinal(2);    // "2nd"
ordinal(3);    // "3rd"
ordinal(4);    // "4th"
ordinal(11);   // "11th"
ordinal(12);   // "12th"
ordinal(13);   // "13th"
ordinal(22);   // "22nd"
ordinal(103);  // "103rd"
ordinal(111);  // "111th"

Correctly handles the 11th/12th/13th special cases and their hundreds equivalents (111th, 112th, 113th).


intword

Converts large integers to friendly text representation. Mirrors Django's intword filter. Works best for numbers over 1 million.

import { intword } from "human-eyes/intword";

intword(1000000);        // "1 million"
intword(1200000);        // "1.2 million"
intword(1200000000);     // "1.2 billion"
intword(1500000000000);  // "1.5 trillion"
intword(-3500000);       // "-3.5 million"
intword(500);            // "500"
intword(999999);         // "999,999"

Supports million, billion, trillion, quadrillion, and quintillion. Numbers below 1 million are returned with comma formatting.


naturalday

Returns "today", "yesterday", or "tomorrow" when appropriate, otherwise returns a formatted date string. Mirrors Django's naturalday filter.

import { naturalday } from "human-eyes/naturalday";

// Assuming today is June 15, 2024
naturalday("2024-06-15");  // "today"
naturalday("2024-06-14");  // "yesterday"
naturalday("2024-06-16");  // "tomorrow"
naturalday("2024-06-10");  // "Jun 10, 2024"

// Custom format
naturalday("2024-06-10", { month: "long", day: "numeric" });
// "June 10"

Compares calendar dates (ignoring time), so a Date at 11:59 PM and one at 12:01 AM on the same day both return "today". Accepts Intl.DateTimeFormatOptions for non-relative dates.


Web Components

For values that need reactive rendering in the DOM, three web components are provided. Import the component module to register the custom element.

<human-eyes-pluralize>

<script type="module">
  import "human-eyes/components/pluralize";
</script>

<!-- Simple: "3 items" / "1 item" -->
<human-eyes-pluralize count="3" word="item"></human-eyes-pluralize>

<!-- Custom suffix: "2 walruses" / "1 walrus" -->
<human-eyes-pluralize count="2" word="walrus" suffix="es"></human-eyes-pluralize>

<!-- Irregular: "2 counties" / "1 county" -->
<human-eyes-pluralize count="2" word="count" suffix="y,ies"></human-eyes-pluralize>

<!-- Full-word replacement: "0 people" / "1 person" -->
<human-eyes-pluralize count="0" suffix="person,people"></human-eyes-pluralize>

<!-- Suffix-only (for surrounding text): -->
<p>You have 5 unread message<human-eyes-pluralize count="5"></human-eyes-pluralize>.</p>

| Attribute | Default | Description | |-----------|---------|-------------| | count | — | Numeric value to pluralize against (required) | | word | — | Base word to prepend with count (renders {count} {word}{suffix}) | | suffix | "s" | Suffix string using Django conventions — plain ("es"), comma-pair ("y,ies"), or full-word ("person,people") |

When word is omitted, only the resolved suffix is rendered — useful for inline use within surrounding text.

Because it's a web component, count is a reactive attribute — update it from JS and the text re-renders automatically:

<label>Count: <span id="n">2</span></label>
<input type="range" id="slider" min="0" max="20" value="2">

<p><human-eyes-pluralize id="demo" count="2" word="message"></human-eyes-pluralize></p>
<p><human-eyes-pluralize id="demo2" count="2" word="cherr" suffix="y,ies"></human-eyes-pluralize></p>

<script type="module">
  import "human-eyes/components/pluralize";

  slider.oninput = () => {
    document.getElementById("n").textContent = slider.value;
    demo.setAttribute("count", slider.value);
    demo2.setAttribute("count", slider.value);
  };
</script>

A full working version of this is in demo.html.

<human-eyes-timesince>

Live-updating relative time display. Refreshes every 60 seconds by default.

<script type="module">
  import "human-eyes/components/timesince";
</script>

<human-eyes-timesince datetime="2024-01-01T00:00:00Z"></human-eyes-timesince>
<!-- "5 months, 2 weeks" (updates live) -->

<human-eyes-timesince datetime="2025-12-25" mode="until"></human-eyes-timesince>
<!-- "1 year, 6 months" -->

<!-- Disable live updates -->
<human-eyes-timesince datetime="2024-01-01" live="false"></human-eyes-timesince>

| Attribute | Default | Description | |-----------|---------|-------------| | datetime | — | ISO 8601 date string or Unix timestamp (required) | | mode | "since" | "since" or "until" | | live | "true" | Auto-update every 60s |

<human-eyes-filesizeformat>

<script type="module">
  import "human-eyes/components/filesizeformat";
</script>

<human-eyes-filesizeformat bytes="123456789"></human-eyes-filesizeformat>
<!-- "117.7 MB" -->

| Attribute | Description | |-----------|-------------| | bytes | File size in bytes (required) |


Why web components for only some filters?

Components are provided for filters whose output benefits from reactive attribute watching — values that change dynamically (live time, file upload sizes, item counts). The other filters (truncate, yesno, floatformat, slugify) are pure functions best called once during rendering.

Development

If you want to run the test suite, npm is used for dev tooling only:

npm install
npm test

Tests use Vitest. No runtime dependencies are installed — node_modules is only needed for running tests.

License

MIT