Skip to content

Commit 67c2948

Browse files
committed
chore: initial commit
0 parents  commit 67c2948

30 files changed

Lines changed: 7192 additions & 0 deletions

.github/renovate.json5

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
3+
extends: ['config:recommended', 'schedule:weekly', 'group:allNonMajor'],
4+
labels: ['dependencies'],
5+
rangeStrategy: 'bump',
6+
packageRules: [
7+
{
8+
matchDepTypes: ['peerDependencies'],
9+
enabled: false,
10+
},
11+
],
12+
ignoreDeps: [
13+
// manually bumping
14+
'node',
15+
'@types/node',
16+
],
17+
}

.github/workflows/checks.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: checks
2+
on:
3+
- push
4+
- pull_request
5+
6+
jobs:
7+
test:
8+
uses: boringnode/.github/.github/workflows/test.yml@main
9+
10+
lint:
11+
uses: boringnode/.github/.github/workflows/lint.yml@main
12+
13+
typecheck:
14+
uses: boringnode/.github/.github/workflows/typecheck.yml@main

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Build
2+
build
3+
4+
# Node & Dependencies
5+
node_modules
6+
coverage
7+
8+
# Build tools specific
9+
.yarn/*
10+
!.yarn/patches
11+
!.yarn/plugins
12+
!.yarn/releases
13+
!.yarn/sdks
14+
!.yarn/versions
15+
npm-debug.log
16+
yarn-error.log
17+
18+
# Editors specific
19+
.fleet
20+
.idea
21+
.vscode

.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

LICENSE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The MIT License
2+
3+
Copyright 2025 Romain Lanz <romain.lanz@pm.me>
4+
5+
This project is a TypeScript port of [Doctrine Inflector](https://github.com/doctrine/inflector).
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+
9+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+
11+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12+
13+
---
14+
15+
## Doctrine Inflector License
16+
17+
Copyright (c) 2006-2015 Doctrine Project
18+
19+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20+
21+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
22+
23+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<div align="center">
2+
3+
[![typescript-image]][typescript-url]
4+
[![npm-image]][npm-url]
5+
[![npm-download-image]][npm-download-url]
6+
[![license-image]][license-url]
7+
8+
</div>
9+
10+
`@boringnode/pluralize` is a TypeScript port of the pluralization features from [Doctrine Inflector](https://github.com/doctrine/inflector).
11+
12+
Built-in support for:
13+
- **English** (`en`, `english`)
14+
- **French** (`fr`, `french`)
15+
16+
> [!NOTE]
17+
> Changes to inflection rules (adding, removing, or modifying rules) will not be considered as breaking changes.
18+
19+
## Installation
20+
21+
```sh
22+
npm install @boringnode/pluralize
23+
```
24+
25+
## Usage
26+
27+
### Simple Functions
28+
29+
The easiest way to use the library is with the simple functions.
30+
31+
```ts
32+
import { pluralize, singularize } from '@boringnode/pluralize'
33+
34+
pluralize('word') // 'words'
35+
pluralize('person') // 'people'
36+
pluralize('child') // 'children'
37+
38+
singularize('words') // 'word'
39+
singularize('people') // 'person'
40+
singularize('children') // 'child'
41+
```
42+
43+
### Using a Different Locale
44+
45+
You can create an `Inflector` instance with a different locale.
46+
47+
```ts
48+
import { Inflector } from '@boringnode/pluralize'
49+
50+
const inflector = new Inflector('fr')
51+
52+
inflector.pluralize('cheval') // 'chevaux'
53+
inflector.pluralize('bijou') // 'bijoux'
54+
inflector.singularize('chevaux') // 'cheval'
55+
```
56+
57+
### Custom Rules with Inflector Class
58+
59+
You can create an `Inflector` instance to add custom rules.
60+
61+
```ts
62+
import { Inflector } from '@boringnode/pluralize'
63+
64+
const inflector = new Inflector()
65+
.addIrregular('gex', 'gexes')
66+
.addUninflected('pokemon')
67+
.addPluralRule(/(.*)gon$/i, '$1gons')
68+
.addSingularRule(/(.*)gons$/i, '$1gon')
69+
70+
inflector.pluralize('gex') // 'gexes'
71+
inflector.pluralize('pokemon') // 'pokemon'
72+
inflector.singularize('dragons') // 'dragon'
73+
```
74+
75+
### Creating a Custom Language Ruleset
76+
77+
You can create and register custom language rulesets for other languages.
78+
79+
```ts
80+
import { Inflector, type LanguageRuleset } from '@boringnode/pluralize'
81+
import {
82+
Ruleset,
83+
Transformations,
84+
Transformation,
85+
Patterns,
86+
Pattern,
87+
Substitutions,
88+
Substitution,
89+
} from '@boringnode/pluralize/builder'
90+
91+
const FrenchRuleset: LanguageRuleset = {
92+
getSingularRuleset: () =>
93+
new Ruleset(
94+
new Transformations(
95+
new Transformation(new Pattern('aux$'), 'al'),
96+
new Transformation(new Pattern('s$'), '')
97+
),
98+
new Patterns(new Pattern('cas'), new Pattern('os')),
99+
new Substitutions(new Substitution('yeux', 'oeil'))
100+
),
101+
getPluralRuleset: () =>
102+
new Ruleset(
103+
new Transformations(
104+
new Transformation(new Pattern('al$'), 'aux'),
105+
new Transformation(new Pattern('$'), 's')
106+
),
107+
new Patterns(new Pattern('cas'), new Pattern('os')),
108+
new Substitutions(new Substitution('oeil', 'yeux'))
109+
),
110+
}
111+
112+
// Register the ruleset
113+
Inflector.register('fr', FrenchRuleset)
114+
115+
// Use it
116+
const inflector = new Inflector('fr')
117+
inflector.pluralize('cheval') // 'chevaux'
118+
inflector.singularize('chevaux') // 'cheval'
119+
```
120+
121+
## API
122+
123+
### Functions
124+
125+
- `pluralize(word: string): string` - Returns the plural form of a word
126+
- `singularize(word: string): string` - Returns the singular form of a word
127+
128+
### Inflector Class
129+
130+
| Method | Description |
131+
| --------------------------------------- | ----------------------------------------------------------- |
132+
| `new Inflector(locale?: string)` | Creates a new inflector instance (defaults to `'en'`) |
133+
| `Inflector.register(locale, ruleset)` | Registers a custom language ruleset |
134+
| `pluralize(word)` | Returns the plural form of a word |
135+
| `singularize(word)` | Returns the singular form of a word |
136+
| `addIrregular(singular, plural)` | Adds an irregular word mapping |
137+
| `addUninflected(word)` | Adds a word that doesn't change between singular and plural |
138+
| `addPluralRule(pattern, replacement)` | Adds a custom pluralization rule |
139+
| `addSingularRule(pattern, replacement)` | Adds a custom singularization rule |
140+
141+
### Builder Exports (`@boringnode/pluralize/builder`)
142+
143+
For creating custom language rulesets:
144+
145+
| Export | Description |
146+
| ----------------- | ----------------------------------------------------- |
147+
| `Pattern` | A regex pattern for matching words |
148+
| `Patterns` | A collection of patterns for uninflected words |
149+
| `Transformation` | A pattern + replacement for regular inflection rules |
150+
| `Transformations` | A collection of transformations |
151+
| `Substitution` | A direct word-to-word mapping for irregular words |
152+
| `Substitutions` | A collection of substitutions |
153+
| `Ruleset` | Combines transformations, patterns, and substitutions |
154+
155+
[npm-image]: https://img.shields.io/npm/v/@boringnode/pluralize.svg?style=for-the-badge&logo=npm
156+
[npm-url]: https://www.npmjs.com/package/@boringnode/pluralize
157+
[npm-download-image]: https://img.shields.io/npm/dm/@boringnode/pluralize?style=for-the-badge
158+
[npm-download-url]: https://www.npmjs.com/package/@boringnode/pluralize
159+
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
160+
[typescript-url]: https://www.typescriptlang.org
161+
[license-image]: https://img.shields.io/npm/l/@boringnode/pluralize?color=blueviolet&style=for-the-badge
162+
[license-url]: LICENSE.md

bin/test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @boringnode/pluralize
3+
*
4+
* @license MIT
5+
* @copyright BoringNode
6+
*/
7+
8+
import { configure, processCLIArgs, run } from '@japa/runner'
9+
import { assert } from '@japa/assert'
10+
11+
processCLIArgs(process.argv.splice(2))
12+
configure({
13+
files: ['tests/**/*.spec.ts'],
14+
plugins: [assert()],
15+
})
16+
17+
void run()

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { configPkg } from '@adonisjs/eslint-config'
2+
3+
export default configPkg({})

index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @boringnode/pluralize
3+
*
4+
* @license MIT
5+
* @copyright BoringNode
6+
*/
7+
8+
export { Inflector, pluralize, singularize } from './src/inflector.js'
9+
export type { LanguageRuleset } from './src/inflector.js'

package.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"name": "@boringnode/pluralize",
3+
"description": "A TypeScript port of Doctrine Inflector to pluralize and singularize English words",
4+
"version": "0.1.0",
5+
"engines": {
6+
"node": ">=20.6"
7+
},
8+
"main": "build/index.js",
9+
"type": "module",
10+
"files": [
11+
"build"
12+
],
13+
"exports": {
14+
".": "./build/index.js",
15+
"./builder": "./build/src/builder.js"
16+
},
17+
"scripts": {
18+
"build": "yarn clean && tsup-node",
19+
"clean": "del-cli build",
20+
"format": "prettier --write .",
21+
"lint": "eslint .",
22+
"prepublishOnly": "yarn build",
23+
"release": "yarn dlx release-it",
24+
"test": "c8 yarn quick:test",
25+
"quick:test": "yarn node --import ts-node-maintained/register/esm --enable-source-maps bin/test.ts",
26+
"typecheck": "tsc --noEmit"
27+
},
28+
"devDependencies": {
29+
"@adonisjs/eslint-config": "^2.1.2",
30+
"@adonisjs/prettier-config": "^1.4.5",
31+
"@adonisjs/tsconfig": "^1.4.1",
32+
"@japa/assert": "^4.2.0",
33+
"@japa/runner": "^4.4.0",
34+
"@swc/core": "^1.15.7",
35+
"@types/node": "^20.17.19",
36+
"c8": "^10.1.3",
37+
"del-cli": "^7.0.0",
38+
"eslint": "^9.39.2",
39+
"prettier": "^3.7.4",
40+
"release-it": "^19.1.0",
41+
"ts-node-maintained": "^10.9.6",
42+
"tsup": "^8.5.1",
43+
"typescript": "^5.9.3"
44+
},
45+
"author": "Romain Lanz <romain.lanz@pm.me>",
46+
"license": "MIT",
47+
"keywords": [
48+
"pluralize",
49+
"singularize",
50+
"inflector",
51+
"plural",
52+
"singular"
53+
],
54+
"prettier": "@adonisjs/prettier-config",
55+
"publishConfig": {
56+
"access": "public",
57+
"tag": "latest"
58+
},
59+
"release-it": {
60+
"git": {
61+
"commitMessage": "chore(release): ${version}",
62+
"tagAnnotation": "v${version}",
63+
"tagName": "v${version}"
64+
},
65+
"github": {
66+
"release": true,
67+
"releaseName": "v${version}",
68+
"web": true
69+
}
70+
},
71+
"packageManager": "yarn@4.12.0"
72+
}

0 commit comments

Comments
 (0)