cronli5
v0.10.0
Published
Cron Like I'm Five: A Cron to English Utility
Readme
Cron Like I'm Five: Cron Patterns in Plain Language
Overview
Cron Like I'm Five (cronli5) generates plain-language, idiomatically
rendered descriptions of schedules from cron patterns in
several languages.
- Zero runtime dependencies. Tiny and safe to drop into any project.
- Runs anywhere. Ships ESM, CommonJS, and a browser global.
- Typed. Bundled TypeScript definitions, no
@typesneeded. - Flexible input. Accepts strings, arrays, or objects.
- Idiomatic output. Composes lists, ranges, and steps into natural sentences, not comma-joined fragments.
- Multilingual. Each language is a full renderer shipped as its own
module (
cronli5/lang/es). You bundle only the languages you import. - Input Formats. Accepts classic (five-part) cron patterns, extended (six-part) cron patterns, where the first field is assumed to refer to seconds, and full seven-part (Quartz-style) patterns with a trailing year. See Input Formats for details.
cronli5 is a good library to use if you need to display a natural-language
interpretation of a cron pattern in a Node or in a browser environment. If you
need to do other things with cron patterns, such as scheduling or computing
future run times, consider a library like @breejs/later.
Alternatives: The main alternative for descriptions is cRonstrue.
See the head-to-head comparison below for how the two
differ and which to pick.
Contents
- Overview
- Installation
- Usage
- Options
- Languages
- Output Examples
- cronli5 vs. cRonstrue
- Description Accuracy
- Note on Timezones
- Module Formats and Types
- Development
- About
- License
Installation
Requires Node.js 20 or newer.
Install using npm:
# For a Node project:
npm install --save cronli5
# If you plan to use the cli:
npm install -g cronli5Browser (script tag) via a CDN:
<script src="https://unpkg.com/cronli5"></script>
<!-- or: https://cdn.jsdelivr.net/npm/cronli5 -->When included in a script tag, the cronli5 function will be available as a
global in the scripts that follow.
Usage
Import as an ES module:
import cronli5 from 'cronli5';Or with CommonJS require:
const cronli5 = require('cronli5');A cron pattern can be a string, an array of fields, or an object. All three forms below describe the same schedule:
cronli5('*/5 * * * *'); // 'every five minutes'
cronli5(['*/5', '*', '*', '*', '*']); // 'every five minutes'
cronli5({ minute: '*/5' }); // 'every five minutes'By default cronli5(...) returns the lowercase, embeddable fragment. For
the capitalized, standalone sentence, set sentence: true — or
call the matching convenience method. cronli5.sentence(...) and
cronli5.fragment(...) are thin sugar over that option (each forwards all
other options) and read a little better at the call site:
cronli5('0 0 * * *'); // 'every day at midnight' (fragment, default)
cronli5.fragment('0 0 * * *'); // 'every day at midnight' (same as above)
cronli5.sentence('0 0 * * *'); // 'Runs every day at midnight.'
cronli5.sentence('0 0 * * *', {lang: de}); // 'Läuft täglich um Mitternacht.'(There is intentionally no cronli5.toString(...): it would shadow
Function.prototype.toString, which the runtime calls with no arguments for
String(cronli5), template literals, and console output — the named methods
sidestep that collision.)
TypeScript types are bundled, so usage is fully typed out of the box:
import cronli5, { type Cronli5Options } from 'cronli5';
const options: Cronli5Options = { ampm: false };
const description: string = cronli5('30 13 * * MON-FRI', options);
// 'every Monday through Friday at 13:30'As a command line tool:
$ cronli5 "*/5 * * * *"
Runs every five minutes.
# Other languages with --lang (de, es, fi, fr, pt, zh):
$ cronli5 --lang de "0 0 * * *"
Läuft täglich um Mitternacht.
$ cronli5 --lang fr "0 0 * * *"
S'exécute tous les jours à minuit.
# --fragment prints the bare, embeddable fragment instead of a sentence:
$ cronli5 --fragment "*/5 * * * *"
every five minutesDialects and script variants — including Traditional Chinese (zh-Hant) — are
selected through the library's dialect option rather than the CLI;
the CLI exposes only --lang and --fragment.
import cronli5 from 'cronli5';
import zh from 'cronli5/lang/zh';
cronli5('0 0 * * *', {lang: zh}); // '每天凌晨0点' (Simplified, default)
cronli5('0 0 * * *', {lang: zh, dialect: 'zh-Hant'}); // '每天凌晨0點' (Traditional)Input Formats
cronli5 accepts classic (five-part) cron patterns, extended (six-part) cron
patterns, where the first field is assumed to refer to seconds, and full seven-part
(Quartz-style) patterns with a trailing year.
It accepts the standard allowed values and the following operators:
- asterisks (
*) - commas (
,) - hyphens (
-) - slashes (
/).
Cron Aliases
@daily and other cron aliases are supported.
Extended Format Support
Ranges in cyclic fields may wrap around (22-2 is an overnight window, and
FRI-MON is a long weekend). Quartz-style tokens are also supported in the date and weekday fields: L (last day, or 5L for the last Friday), W (nearest weekday, e.g. 15W), and # (nth weekday, e.g. 1#2 for the second Monday). Quartz's ? ("no specific value") and its day-of-week numbering (1 = Sunday) require the quartz option; see its note for why.
Options
The cronli5 function takes an options object as its 2nd parameter:
| Option | Default | Description |
| --- | --- | --- |
| ampm | true (English) | Use a 12-hour clock. Set false for 24-hour time. The default is language-specific: English is 12-hour; Spanish and Finnish default to 24-hour (Finnish is 24-hour only). |
| dialect | 'us' | The English style. 'us' follows the Chicago Manual of Style: serial commas, through ranges, 9 a.m./5:30 p.m. times, noon/midnight, and January 1 dates. 'gb' follows the Guardian style guide: no serial comma, to ranges, 9am/5.30pm times, midday/midnight, and 1 January dates. 'house' is cronli5's legacy voice (9:30 AM, Monday - Friday). A custom object defines your own style. ('uk' is a deprecated alias for 'gb'.) See docs/dialects.md. |
| lang | English | A language module, e.g. import es from 'cronli5/lang/es'. Each language owns its words, conventions, and dialects — see Languages. |
| lenient | false | Never throw: invalid input returns the language's fallback description ('an unrecognizable cron pattern') instead. Useful when rendering arbitrary user crontabs. |
| quartz | false | Read the pattern with Quartz semantics. Quartz numbers the day-of-week 1 = Sunday … 7 = Saturday (standard cron uses 0/7 = Sunday, 1 = Monday) and requires exactly one of day-of-month or day-of-week to be ? ("no specific value"). With this off, ? is rejected outright rather than silently mis-read — a ? is the unambiguous mark of a Quartz pattern, so '0 0 ? * 2' would otherwise read as Tuesday when its author meant Monday. The Quartz numbering also applies inside the day-of-week operators (6L, 2#2) and the weekday L alias. Day names (MON) and the other fields are unaffected. Composable with seconds/years. |
| sentence | false | Return a complete standalone sentence ('Runs every day at midnight.', 'Läuft täglich um Mitternacht.') instead of the embeddable fragment. Each language supplies its own wrapping. Wraps a schedule and @reboot, but not the lenient fallback. The methods cronli5.sentence(...) / cronli5.fragment(...) (see Usage) are sugar for this option set to true / false. |
| short | false | Compact output: abbreviated month and weekday names, and hyphenated ranges everywhere through/to would appear (Mon-Fri, Jan-Mar, 1st-5th, 9 a.m.-5:45 p.m.). |
| seconds | false | Always treat the first field of strings and arrays as the second field. |
| years | false | Treat the last field of a six-field string/array as the year field. Otherwise the first field of a six-field pattern is treated as the second field. Seven-field patterns are unambiguous (seconds first, year last) and need no option. |
When a specific year is given — via a seven-field pattern, an object's
year property, or a six-field pattern with years: true — it is
folded into a specific calendar date ('on January 1, 2030 at noon')
or otherwise trails the description ('every Friday at 1 p.m. in 2030').
cronli5('0 0 12 1 1 * 2030'); // 'on January 1, 2030 at noon'
cronli5({ hour: 9, year: 2030 }); // 'every day at 9 a.m. in 2030'import cronli5 from 'cronli5';
const weekdaysAt1330 = '30 13 * * MON-FRI';
cronli5(weekdaysAt1330, { ampm: true, short: false });
// 'every Monday through Friday at 1:30 p.m.'
cronli5(weekdaysAt1330, { ampm: false, short: true });
// 'every Mon-Fri at 13:30'
cronli5(weekdaysAt1330, { dialect: 'gb' });
// 'every Monday to Friday at 1.30pm'Languages
English is the default. Other languages are full renderers over the same
language-independent core. A language ships as its own module and is selected
per call with the lang option. If you never import it, it never reaches your
bundle (each language adds about 3.3 KB gzipped).
import cronli5 from 'cronli5';
import es from 'cronli5/lang/es';
import fi from 'cronli5/lang/fi';
cronli5('30 9 * * MON-FRI'); // 'every Monday through Friday at 9:30 a.m.'
cronli5('30 9 * * MON-FRI', {lang: es}); // 'de lunes a viernes a las 09:30'
cronli5('30 9 * * MON-FRI', {lang: fi}); // 'maanantaista perjantaihin klo 9.30'Each language carries its own conventions and defaults — Spanish and
Finnish default to the 24-hour clock, for instance (Spanish takes
{ampm: true} for 12-hour times with day periods). See the per-language
docs below.
| Language | Module | Anchors | Doc |
| --- | --- | --- | --- |
| English | built in (also cronli5/lang/en) | Chicago Manual of Style; Guardian ('gb' dialect) | docs/lang/en.md |
| German | cronli5/lang/de | Duden (de-AT/de-CH dialects) | docs/lang/de.md |
| Spanish | cronli5/lang/es | RAE / FundéuRAE | docs/lang/es.md |
| Finnish | cronli5/lang/fi | Kielitoimiston ohjepankki; SFS 4175 | docs/lang/fi.md |
| French | cronli5/lang/fr | Imprimerie nationale / Académie française (fr-FR; fr-CA is a future axis) | docs/lang/fr.md |
| Portuguese | cronli5/lang/pt | VOLP / Academia Brasileira de Letras (pt-BR; pt-PT is a future axis) | docs/lang/pt.md |
| Ukrainian | cronli5/lang/uk | Ukrainian National Commission on Spelling (2019) | docs/lang/uk.md |
| Chinese (Mandarin) | cronli5/lang/zh | Simplified (zh-Hans) default; Traditional (zh-Hant) | docs/lang/zh.md |
Language maturity
Languages ship as experimental → beta → stable (model-drafted → model-validated → verified by a fluent human). A new language is usually built by sibling-derivation — deriving it from its nearest validated relative (porting that renderer and translating its reviewed corpus, then TDD to green) — and reaches beta once it passes the objective gates (round-trip, fuzz, OR-scope, the cRonstrue comparison) and review by a blind panel of Claude Sonnet instances. It graduates to stable only on review by a fluent human:
| Language | Status |
| --- | --- |
| German | beta |
| English | stable |
| Spanish | beta |
| Finnish | beta |
| French | beta |
| Portuguese | beta |
| Ukrainian | beta |
| Chinese (Mandarin, Simplified) | beta |
| Chinese (Mandarin, Simplified) (zh-Hant) | experimental |
For the review evidence behind each status, see docs/language-status.md.
Each language doc includes a generated side-by-side table against the matching cRonstrue locale. The architecture is described in docs/i18n-design.md.
Output Examples
Output follows the Chicago Manual of Style by default (serial commas,
9 a.m. times, noon/midnight, cardinal month-day dates). See the
dialect option for Guardian-style British English. Bare days
of the month use suffixed ordinals (on the 1st and 15th).
// Single values and steps
cronli5('*/5 * * * *'); // 'every five minutes'
cronli5('0 9 * * MON'); // 'every Monday at 9 a.m.'
// Lists
cronli5('5,10,15 * * * * *'); // 'at 5, 10, and 15 seconds past the minute'
cronli5('0 9,17 * * *'); // 'every day at 9 a.m. and 5 p.m.'
cronli5('0 0 1,15 * *'); // 'on the 1st and 15th at midnight'
cronli5('0 12 * 6,12 *'); // 'every day in June and December at noon'
// Ranges (wrap-around ranges describe overnight and weekend windows)
cronli5('0-29 * * * *'); // 'every minute from 0 through 29 past the hour'
cronli5('0 9-17 * * *'); // 'every hour from 9 a.m. through 5 p.m.'
cronli5('0 0 1-15 * *'); // 'on the 1st through 15th at midnight'
cronli5('0 22-2 * * *'); // 'every hour from 10 p.m. through 2 a.m.'
cronli5('0 0 * * FRI-MON'); // 'every Friday through Monday at midnight'
// Compound patterns
cronli5('0,30 9 * * *'); // 'every day at 9 a.m. and 9:30 a.m.'
cronli5('*/15 9-17 * * *'); // 'every 15 minutes from 9 a.m. through 5 p.m.'
cronli5('30 9-17 * * *');
// 'at 30 minutes past the hour from 9 a.m. through 5 p.m.'
cronli5('0 12 1 1 *'); // 'on January 1 at noon'
cronli5('0 * 13 * *'); // 'every hour on the 13th'
// Quartz tokens
cronli5('0 0 L * *'); // 'on the last day of the month at midnight'
cronli5('0 0 * * 5L'); // 'on the last Friday of the month at midnight'
cronli5('0 0 * * 1#2'); // 'on the second Monday of the month at midnight'
cronli5('0 0 15W * *'); // 'on the weekday nearest the 15th at midnight'cronli5 vs. cRonstrue
cRonstrue is the most widely used cron-description library, but
it differs from cronli5 in philosophy. cronli5 writes one flowing sentence
and does additional validation; its languages are full renderers
(seven so far). cRonstrue assembles per-field fragments from
translated templates, which is how it covers 39 locales. The same compound
pattern — 5,10 30 9 * * MON — in every language:
| Language | cronli5 | cRonstrue 3.14.0 | | --- | --- | --- | | English | at 5 and 10 seconds past the minute, every Monday at 9:30 a.m. | At 5 and 10 seconds past the minute, at 30 minutes past the hour, at 09:00 AM, only on Monday | | German | in den Sekunden 5 und 10, montags um 9:30 Uhr | Bei Sekunde 5 und 10, bei Minute 30, um 09:00, nur jeden Montag | | Spanish | los lunes, en los segundos 5 y 10 de las 09:30 | A los 5 y 10 segundos del minuto, a los 30 minutos de la hora, a las 09:00, sólo el lunes | | Finnish | 5 ja 10 sekunnin kohdalla, maanantaisin klo 9.30 | 5 ja 10 sekunnnin jälkeen, 30 minuuttia yli, klo 09:00, vain maanantai | | French | le lundi, aux secondes 5 et 10 de 9 h 30 | 5 et 10 secondes après la minute, 30 minutes après l'heure, 09:00, uniquement le lundi | | Portuguese | às segundas-feiras, nos segundos 5 e 10 das 09:30 | Aos 5 e 10 segundos do minuto, aos 30 minutos da hora, Às 09:00, somente de segunda-feira | | Ukrainian | о 5-й і 10-й секунді, по понеділках о 9:30 | О 5 та 10 секунді, о 30 хвилині, о 09:00, тільки в понеділок | | Chinese (Simplified) | 每周一,9点30分的第5、10秒 | 在一分钟后的第 5 和 10 秒, 在整点后的第 30 分钟, 在上午 09:00, 仅星期一 | | Chinese (Traditional) | 每週一,9點30分的第5、10秒 | 在一分鐘後的 5 和 10 秒, 在整點後的 30 分, 在 09:00, 僅在 星期一 |
See docs/cronli5-vs-cronstrue.md for more generated side-by-side output tables, and docs/migrating-from-cronstrue.md if you are switching from cRonstrue.
Description Accuracy
Sometimes minimizing verbosity results in ambiguities. For example, "every two
minutes" could reasonably refer to two behaviorally distinct cron patterns
with minute accuracy: */2 * * * * and 1/2 * * * * (and 120 behaviorally
distinct cron patterns with second accuracy). As a tradeoff, this library does
not qualify cases that begin on the first second, minute, or hour of the
corresponding minute, hour, or day. So */3 * * * * will be "every three
minutes", while 2/3 * * * * will be "every three minutes from two minutes
past the hour".
Note on Timezones
cronli5 always describes cron patterns with respect to whatever system
timezone the cron pattern is being run in. This utility does not, nor does it
ever intend to, deal with timezone conversions. That functionality would
require some non-trivial dependencies like moment-timezone and moment
to even approximate correctness and the output could still be wrong anyways
because timezones are problematic. Associate the displayed
description with a timezone (e.g. America/Phoenix) when there is the
possibility for confusion.
Module Formats and Types
cronli5 is authored as an ES module in src/. It is published with dual builds
so it works everywhere:
- ESM (
import cronli5 from 'cronli5') resolves todist/cronli5.js. - CommonJS (
const cronli5 = require('cronli5')) resolves todist/cronli5.cjs. - Browser (
<script src="cronli5.min.js">) exposes a globalcronli5(English only).
Language subpaths (cronli5/lang/en, cronli5/lang/es, cronli5/lang/fi)
ship the same dual ESM + CJS builds under dist/lang/.
TypeScript type definitions ship in cronli5.d.ts (and lang.d.ts for the
language subpaths) and are picked up automatically: no @types package
required.
Development
The library has no runtime dependencies. Development requires Node.js 20 or
newer (the Vitest 4 test runner needs it); the toolchain (ESLint, Vitest,
Chai, esbuild) lives in devDependencies.
npm install # install dev dependencies (also wires the git hooks)
npm test # run the Vitest suite (runs against src/, no build needed)
npm run coverage # run tests with Vitest V8 coverage and enforce thresholds
npm run lint # lint source and tests with ESLint
npm run build # emit dist/ (ESM + CJS) and the minified browser global
npm run verify # the full CI gate: lint, types, tests, coverage, docs, buildA pre-push git hook runs npm run verify so a push only lands when the full
gate is green. It is wired automatically on npm install (via core.hooksPath
→ .githooks/); bypass it in an emergency with git push --no-verify.
About
The project name is a reference to the phrase Explain Like I'm Five (ELI5), which is used to ask for a friendly, simplified, and layman-accessible summary of material that may be hard to understand without some background.
cronli5 was partially inspired by prettycron, which itself
is based on code from a gist by dunse. Although prettycron was
close to meeting my needs, I wasn't fully satisfied with the output. cronli5
tries to render as many cron patterns in as direct and as idiomatic language
as possible in every target language. Test cases that describe where it
fails to do so and which prescribe an obviously better description would be
greatly appreciated. Native speakers of target languages are the best.
License
MIT License
Copyright © 2026 Andrew Brož
