Skip to content

Commit 109eaec

Browse files
committed
chore: initial commit
0 parents  commit 109eaec

30 files changed

+7124
-0
lines changed

.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 <[email protected]>
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: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
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+
pattern,
83+
Patterns,
84+
Substitutions,
85+
Transformations,
86+
} from '@boringnode/pluralize/builder'
87+
88+
const SpanishRuleset: LanguageRuleset = {
89+
getSingularRuleset: () => ({
90+
regular: new Transformations([
91+
{ pattern: pattern('es$'), replacement: '' },
92+
{ pattern: pattern('s$'), replacement: '' },
93+
]),
94+
uninflected: new Patterns([pattern('lunes'), pattern('martes')]),
95+
irregular: new Substitutions([{ from: 'hombres', to: 'hombre' }]),
96+
}),
97+
getPluralRuleset: () => ({
98+
regular: new Transformations([
99+
{ pattern: pattern('[aeiou]$'), replacement: '$&s' },
100+
{ pattern: pattern('$'), replacement: 'es' },
101+
]),
102+
uninflected: new Patterns([pattern('lunes'), pattern('martes')]),
103+
irregular: new Substitutions([{ from: 'hombre', to: 'hombres' }]),
104+
}),
105+
}
106+
107+
// Register the ruleset
108+
Inflector.register('es', SpanishRuleset)
109+
110+
// Use it
111+
const inflector = new Inflector('es')
112+
inflector.pluralize('gato') // 'gatos'
113+
```
114+
115+
## API
116+
117+
### Functions
118+
119+
- `pluralize(word: string): string` - Returns the plural form of a word
120+
- `singularize(word: string): string` - Returns the singular form of a word
121+
122+
### Inflector Class
123+
124+
| Method | Description |
125+
| --------------------------------------- | ----------------------------------------------------------- |
126+
| `new Inflector(locale?: string)` | Creates a new inflector instance (defaults to `'en'`) |
127+
| `Inflector.register(locale, ruleset)` | Registers a custom language ruleset |
128+
| `pluralize(word)` | Returns the plural form of a word |
129+
| `singularize(word)` | Returns the singular form of a word |
130+
| `addIrregular(singular, plural)` | Adds an irregular word mapping |
131+
| `addUninflected(word)` | Adds a word that doesn't change between singular and plural |
132+
| `addPluralRule(pattern, replacement)` | Adds a custom pluralization rule |
133+
| `addSingularRule(pattern, replacement)` | Adds a custom singularization rule |
134+
135+
### Builder Exports (`@boringnode/pluralize/builder`)
136+
137+
For creating custom language rulesets:
138+
139+
| Export | Description |
140+
| ----------------- | -------------------------------------------------------- |
141+
| `pattern` | Helper function to create a case-insensitive regex |
142+
| `Patterns` | A collection of patterns for uninflected words |
143+
| `Transformation` | Interface: `{ pattern: RegExp, replacement: string }` |
144+
| `Transformations` | A collection of transformations |
145+
| `Substitution` | Interface: `{ from: string, to: string }` |
146+
| `Substitutions` | A collection of substitutions |
147+
| `Ruleset` | Interface: `{ regular, uninflected, irregular }` |
148+
149+
[npm-image]: https://img.shields.io/npm/v/@boringnode/pluralize.svg?style=for-the-badge&logo=npm
150+
[npm-url]: https://www.npmjs.com/package/@boringnode/pluralize
151+
[npm-download-image]: https://img.shields.io/npm/dm/@boringnode/pluralize?style=for-the-badge
152+
[npm-download-url]: https://www.npmjs.com/package/@boringnode/pluralize
153+
[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript
154+
[typescript-url]: https://www.typescriptlang.org
155+
[license-image]: https://img.shields.io/npm/l/@boringnode/pluralize?color=blueviolet&style=for-the-badge
156+
[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 . --fix",
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 <[email protected]>",
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": "[email protected]"
72+
}

0 commit comments

Comments
 (0)