diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 53fcad0..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,36 +0,0 @@
-module.exports = {
- parser: '@typescript-eslint/parser',
- plugins: ['@typescript-eslint'],
- extends: [
- 'eslint:recommended',
- 'airbnb-base',
- "plugin:@typescript-eslint/eslint-recommended",
- 'plugin:@typescript-eslint/recommended',
- ],
- rules: {
- 'import/extensions': ['error', {
- 'ts': 'never',
- 'json': 'always'
- }],
- 'class-methods-use-this': 'off',
- 'no-restricted-syntax': 'off',
- 'no-param-reassign': 'off',
- 'no-cond-assign': 'off',
- 'no-useless-escape': 'off',
- 'quotes': 'error',
- 'semi': 'error',
- 'comma-dangle': ['error', {
- 'arrays': 'always',
- }],
- },
- env: {
- browser: true,
- },
- settings: {
- 'import/resolver': 'webpack',
- },
- parserOptions: {
- ecmaVersion: 2018,
- sourceType: 'module',
- },
-};
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4fc309a..d4eef7d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -5,17 +5,25 @@ on:
tags:
- 'v*.*.*'
+permissions:
+ id-token: write # Required for OIDC
+ contents: read
+
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
with:
- node-version: 12
+ node-version: 24.x
registry-url: https://registry.npmjs.org/
- run: npm install
- - run: npm version "${GITHUB_REF:11}" --git-tag-version false --commit-hooks false
- - run: npm publish --access public
- env:
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
+ - run: |
+ VERSION="${GITHUB_REF:11}"
+ npm version "$VERSION" --git-tag-version false --commit-hooks false
+ if [[ "$VERSION" == *"-"* ]]; then
+ npm publish --access public --tag next
+ else
+ npm publish --access public
+ fi
diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml
index c547248..568444f 100644
--- a/.github/workflows/quality.yml
+++ b/.github/workflows/quality.yml
@@ -3,15 +3,17 @@ on:
branches:
- '*' # matches every branch
- '*/*' # matches every branch containing a single '/'
+ pull_request:
jobs:
- eslint:
- name: eslint
+ quality:
+ name: quality
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v6
- name: Use Node.js
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v6
with:
- node-version: 12.x
+ node-version: 24.x
- run: npm install
- run: npm run eslint
+ - run: npm test
diff --git a/README.md b/README.md
index c1d44b1..87b92e7 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,105 @@
-# Date Formatter
+# @date-js/date-formatter
-- A light javascript tool for formatting date (<3kB).
-- Zero dependency.
-- TypeScript compatible.
+[](https://www.npmjs.com/package/@date-js/date-formatter)
+[](LICENSE)
-## Example
+**Format a date. Nothing else. Under 1KB gzipped.**
-``` javascript
-DateFormatter.format('%l %j %F %Y', new Date());
-// Sunday 12 October 2014
+No config. No plugins. No dependencies. Locale support out of the box via the native `Intl` API.
+
+```javascript
+DateFormatter.format('%l, %F %j %Y', new Date());
+// Sunday, March 29 2026
+
+DateFormatter.format('%l %j %F %Y', new Date(), 'fr-FR');
+// dimanche 29 mars 2026
```
-``` javascript
-DateFormatter.format('%l %j %F %Y', new Date(), 'fr_FR');
-// Samedi 12 octobre 2014
+## Why this lib?
+
+If all you need is to display a date, there's no point in shipping a full date toolkit. Perfect for landing pages, lightweight widgets, or any project where pulling in a full date library would be overkill.
+
+- **Under 1KB gzipped** — no dead code, no bloat
+- **Zero dependencies** — nothing to audit, nothing to break
+- **Locale support out of the box** — automatically uses the browser language, no config needed
+- **Familiar syntax** — `%Y-%m-%d` style, like PHP's `date()` or Python's `strftime`
+
+## Install & Usage
+
+### With a bundler (webpack, Vite, Rollup…)
+
+```bash
+npm install @date-js/date-formatter
```
-By default, Date Formatter uses the current browser language. You can force a locale like this:
-``` javascript
+Use the default export to get a ready-to-use singleton:
+
+```javascript
+import DateFormatter from '@date-js/date-formatter';
+
+DateFormatter.format('%d/%m/%Y', new Date()); // 29/03/2026
DateFormatter.setLocale('fr-FR');
+DateFormatter.format('%l %j %F %Y', new Date()); // dimanche 29 mars 2026
```
-You can see examples in example directory.
+Or import the class directly to create isolated instances (useful when managing multiple locales):
-## Install it
+```javascript
+import { DateFormatter } from '@date-js/date-formatter';
-### With NPM
+const fr = new DateFormatter('fr-FR');
+const en = new DateFormatter('en-US');
-``` bash
-npm install @date-js/date-formatter --save
+fr.format('%l %j %F %Y', new Date()); // dimanche 29 mars 2026
+en.format('%l %j %F %Y', new Date()); // Sunday March 29 2026
```
-``` javascript
-import DateFormatter from '@date-js/date-formatter';
+### In a browser — classic script tag
+
+The UMD bundle exposes `DateFormatter` as a global:
+
+```html
+
+
```
-### Otherwise
+### In a browser — ES module
+
+Modern browsers support `
+ DateFormatter.format('%d/%m/%Y', new Date()); // 29/03/2026
+
```
-## Format
-
-| Character | Description | Example |
-| ------------- |:-------------:| -----:|
-| %Y | A full numeric representation of a year, 4 digits | 2020 |
-| %y | A two digit representation of a year | 20 |
-| %d | Day of the month, 2 digits with leading zeros | 01 to 31 |
-| %l | A full textual representation of the day of the week | Sunday through Saturday |
-| %D | A textual representation of a day, three letters | Mon through Sun |
-| %F | A full textual representation of a month, such as January or March | January through December |
-| %M | A short textual representation of a month, three letters | Jan through Dec |
-| %m | Numeric representation of a month, with leading zeros | 01 through 12 |
-| %n | Numeric representation of a month, without leading zeros | 1 through 12 |
-| %G | 24-hour format of an hour without leading zeros | 0 through 23 |
-| %H | 24-hour format of an hour with leading zeros | 00 through 23 |
-| %i | Minutes with leading zeros | 00 to 59 |
-| %s | Seconds with leading zeros | 00 to 59 |
+## Format specifiers
+
+| Specifier | Description | Example |
+|---|---|---|
+| `%Y` | Year, 4 digits | `2026` |
+| `%y` | Year, 2 digits | `26` |
+| `%d` | Day of the month, with leading zero | `01` – `31` |
+| `%j` | Day of the month, without leading zero | `1` – `31` |
+| `%l` | Full weekday name *(locale-aware)* | `Sunday`, `dimanche` |
+| `%D` | Short weekday name *(locale-aware)* | `Sun`, `dim.` |
+| `%F` | Full month name *(locale-aware)* | `March`, `mars` |
+| `%M` | Short month name *(locale-aware)* | `Mar`, `mars` |
+| `%m` | Month, with leading zero | `01` – `12` |
+| `%n` | Month, without leading zero | `1` – `12` |
+| `%H` | Hour (24h), with leading zero | `00` – `23` |
+| `%G` | Hour (24h), without leading zero | `0` – `23` |
+| `%i` | Minutes, with leading zero | `00` – `59` |
+| `%s` | Seconds, with leading zero | `00` – `59` |
+
+Escape a `%` with a backslash: `\%`.
+
+## License
+
+MIT
diff --git a/bin/docker.sh b/bin/docker.sh
new file mode 100755
index 0000000..8b42af8
--- /dev/null
+++ b/bin/docker.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+DIRECTORY=$(dirname $(realpath $0 ))
+
+TTY_FLAG=$([ -t 0 ] && echo "-it" || echo "-i")
+docker run $TTY_FLAG --rm \
+ -v "$DIRECTORY/..":/home/node/app \
+ -w /home/node/app \
+ -u $(id -u ${USER}):$(id -g ${USER}) \
+ node:24-slim \
+ "${@:-bash}"
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..44f67b5
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,12 @@
+const tseslint = require('typescript-eslint');
+
+module.exports = tseslint.config(
+ ...tseslint.configs.recommended,
+ {
+ files: ['**/*.ts'],
+ rules: {
+ 'quotes': ['error', 'single'],
+ 'semi': 'error',
+ },
+ }
+);
diff --git a/package.json b/package.json
index 8ac961d..b99111f 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,22 @@
{
"name": "@date-js/date-formatter",
"version": "0.0.0-version",
- "description": "A light javascript tool for formatting date.",
+ "description": "Tiny date formatter with locale support. Zero dependencies, under 1KB gzipped.",
"main": "dist/date-formatter.js",
+ "exports": {
+ ".": {
+ "import": "./dist/date-formatter.js",
+ "require": "./dist/date-formatter.js",
+ "types": "./dist/types/index.d.ts"
+ }
+ },
"scripts": {
"prepublishOnly": "webpack",
- "eslint": "eslint ./src --ext .ts,.js,.d.ts",
- "eslint-fix": "eslint ./src --ext .ts,.js,.d.ts --fix",
+ "eslint": "eslint src",
+ "eslint-fix": "eslint src --fix",
"watch": "webpack --watch",
- "build": "webpack"
+ "build": "webpack",
+ "test": "vitest run"
},
"repository": {
"type": "git",
@@ -16,11 +24,18 @@
},
"keywords": [
"date",
- "light",
- "small",
- "tiny",
"format",
- "formatter"
+ "formatter",
+ "date-format",
+ "strftime",
+ "intl",
+ "locale",
+ "i18n",
+ "lightweight",
+ "tiny",
+ "zero-dependency",
+ "browser",
+ "typescript"
],
"files": [
"dist"
@@ -33,30 +48,12 @@
},
"homepage": "https://github.com/date-js/date-formatter#readme",
"devDependencies": {
- "@babel/core": "^7.14.6",
- "@babel/preset-env": "^7.14.7",
- "@babel/preset-typescript": "^7.14.5",
- "@types/node": "^12.12.62",
- "@typescript-eslint/eslint-plugin": "^2.34.0",
- "@typescript-eslint/parser": "^2.34.0",
- "babel-loader": "^8.2.2",
- "eslint": "^6.8.0",
- "eslint-config-airbnb-base": "^14.2.0",
- "eslint-config-standard": "^14.1.1",
- "eslint-import-resolver-webpack": "^0.12.2",
- "eslint-plugin-import": "^2.22.1",
- "eslint-plugin-promise": "^4.3.1",
- "path": "^0.12.7",
- "terser-webpack-plugin": "^5.1.4",
- "ts-loader": "^9.2.3",
- "typescript": "^4.3.4",
- "webpack": "^5.41.0",
- "webpack-cli": "^4.7.2"
- },
- "babel": {
- "presets": [
- "@babel/preset-env",
- "@babel/preset-typescript"
- ]
+ "eslint": "^10.1.0",
+ "ts-loader": "^9.5.4",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.57.2",
+ "vitest": "^4.1.2",
+ "webpack": "^5.105.4",
+ "webpack-cli": "^7.0.2"
}
}
diff --git a/src/App.ts b/src/DateFormatter.ts
similarity index 52%
rename from src/App.ts
rename to src/DateFormatter.ts
index 206da56..f5d551d 100644
--- a/src/App.ts
+++ b/src/DateFormatter.ts
@@ -1,5 +1,9 @@
-export default class App {
- protected locale: string = navigator.language;
+export default class DateFormatter {
+ private locale: string;
+
+ constructor(locale?: string) {
+ this.locale = locale ?? (typeof navigator !== 'undefined' ? navigator.language : 'en');
+ }
public setLocale(locale: string): void {
this.locale = locale;
@@ -8,27 +12,30 @@ export default class App {
public format(format: string, date: Date, locale?: string): string {
let formatted = format;
let varData;
- const varRegexp = /%([a-z]+)/gi;
+ let delta = 0;
+ const varRegexp = /(? {
+ it('%Y — full year', () => {
+ expect(DateFormatter.format('%Y', date)).toBe('2025');
+ });
+
+ it('%y — 2-digit year', () => {
+ expect(DateFormatter.format('%y', date)).toBe('25');
+ });
+
+ it('%d — day with zero padding', () => {
+ expect(DateFormatter.format('%d', date)).toBe('05');
+ });
+
+ it('%j — day without zero padding', () => {
+ expect(DateFormatter.format('%j', date)).toBe('5');
+ });
+
+ it('%m — month with zero padding', () => {
+ expect(DateFormatter.format('%m', date)).toBe('01');
+ });
+
+ it('%n — month without zero padding', () => {
+ expect(DateFormatter.format('%n', date)).toBe('1');
+ });
+
+ it('%H — hours with zero padding', () => {
+ expect(DateFormatter.format('%H', date)).toBe('09');
+ });
+
+ it('%G — hours without zero padding', () => {
+ expect(DateFormatter.format('%G', date)).toBe('9');
+ });
+
+ it('%i — minutes with zero padding', () => {
+ expect(DateFormatter.format('%i', date)).toBe('03');
+ });
+
+ it('%s — seconds with zero padding', () => {
+ expect(DateFormatter.format('%s', date)).toBe('07');
+ });
+
+ it('%l — full weekday name (en)', () => {
+ expect(DateFormatter.format('%l', date, 'en')).toBe('Sunday');
+ });
+
+ it('%D — short weekday name (en)', () => {
+ expect(DateFormatter.format('%D', date, 'en')).toBe('Sun');
+ });
+
+ it('%F — full month name (en)', () => {
+ expect(DateFormatter.format('%F', date, 'en')).toBe('January');
+ });
+
+ it('%M — short month name (en)', () => {
+ expect(DateFormatter.format('%M', date, 'en')).toBe('Jan');
+ });
+});
+
+describe('combined formats', () => {
+ it('ISO-like date %Y-%m-%d', () => {
+ expect(DateFormatter.format('%Y-%m-%d', date)).toBe('2025-01-05');
+ });
+
+ it('full datetime %Y-%m-%d %H:%i:%s', () => {
+ expect(DateFormatter.format('%Y-%m-%d %H:%i:%s', date)).toBe('2025-01-05 09:03:07');
+ });
+});
+
+describe('percent escaping', () => {
+ it('\\% outputs a literal %', () => {
+ expect(DateFormatter.format('\\%Y', date)).toBe('%Y');
+ });
+
+ it('mixed escaped and unescaped', () => {
+ expect(DateFormatter.format('\\%Y %Y', date)).toBe('%Y 2025');
+ });
+});
+
+describe('locale', () => {
+ it('locale passed to format() is used', () => {
+ expect(DateFormatter.format('%l', date, 'fr-FR')).toBe('dimanche');
+ });
+
+ it('setLocale() changes the default locale', () => {
+ const instance = new DateFormatterClass('en');
+ instance.setLocale('fr-FR');
+ expect(instance.format('%l', date)).toBe('dimanche');
+ });
+
+ it('locale passed to format() takes precedence over setLocale()', () => {
+ const instance = new DateFormatterClass('fr-FR');
+ expect(instance.format('%l', date, 'en')).toBe('Sunday');
+ });
+});
+
+describe('unknown specifier', () => {
+ it('unknown specifier is left as-is', () => {
+ expect(DateFormatter.format('%z', date)).toBe('%z');
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
index ee5ce83..c977829 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,8 +4,6 @@
"outDir": "./dist/",
"declarationDir": "./dist/types",
"declaration": true,
- "noImplicitAny": true,
- "resolveJsonModule": true,
"esModuleInterop": true,
"lib": [
"es2015",
diff --git a/webpack.config.js b/webpack.config.js
index 611ff04..02a176c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,40 +1,25 @@
const path = require('path');
-const TerserPlugin = require('terser-webpack-plugin');
-const config = {
+module.exports = {
mode: 'production',
- entry: {
- 'index': './src/index.ts',
- },
+ entry: './src/index.ts',
output: {
filename: 'date-formatter.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'umd',
- library: 'DateFormatter'
+ library: 'DateFormatter',
+ libraryExport: 'default',
},
resolve: {
- extensions: ['.ts', '.js', '.json'],
+ extensions: ['.ts', '.js'],
},
module: {
rules: [
- {
- test: /\.ts$/,
- exclude: /(node_modules)/,
- use: {
- loader: 'babel-loader',
- }
- },
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
- }
- ]
- },
- optimization: {
- minimize: true,
- minimizer: [new TerserPlugin()],
+ },
+ ],
},
};
-
-module.exports = [config];