Plurality handles standard English well out of the box, but every domain has
words it can't predict: internal product names, technical jargon, borrowed
terms from other languages. Plurality.Custom lets you teach it your domain
without losing any built-in coverage.
Creating a custom module
defmodule MyApp.Inflection do
use Plurality.Custom,
irregulars: [
{"regex", "regexen"},
{"pokemon", "pokemon"},
{"elasticsearch", "elasticsearch"}
],
uncountables: [
"kubernetes",
"graphql",
"redis"
]
endThis generates a module with the same API as Plurality:
MyApp.Inflection.pluralize("regex") #=> "regexen"
MyApp.Inflection.pluralize("kubernetes") #=> "kubernetes"
MyApp.Inflection.pluralize("leaf") #=> "leaves" (falls through to defaults)
MyApp.Inflection.singularize("regexen") #=> "regex"
MyApp.Inflection.plural?("kubernetes") #=> true
MyApp.Inflection.singular?("kubernetes") #=> trueHow resolution works
Your custom entries are checked first. If the word isn't found in your custom data, it falls through to the built-in Plurality engine:
1. Custom uncountables (MapSet) → return unchanged
2. Custom irregulars (Map) → return mapped form
3. Built-in engine → full three-tier resolutionThis means custom entries always take priority over built-in data.
Options
:irregulars
A list of {singular, plural} tuples. Each pair is stored in both a forward
map (singular -> plural) and a reverse map (plural -> singular).
irregulars: [
{"regex", "regexen"}, # regex → regexen, regexen → regex
{"pokemon", "pokemon"}, # identity pair — unchanged in both directions
{"octopus", "octopodes"} # override built-in octopus → octopuses
]Identity pairs (where singular equals plural) work correctly — the word is
returned unchanged in both directions, similar to uncountables but with
explicit plural?/1 and singular?/1 handling.
:uncountables
A list of strings. These words are returned unchanged by both pluralize/2
and singularize/1, and return true for both plural?/1 and singular?/1.
uncountables: [
"kubernetes",
"graphql",
"redis"
]Application integration
Call custom modules directly from the code that needs domain-specific rules:
MyApp.Inflection.pluralize("regex") #=> "regexen"
MyApp.Inflection.pluralize("leaf") #=> "leaves"If your application wants a single entry point, wrap the custom module in your own application code:
defmodule MyApp.Nouns do
def pluralize(word, opts \\ []), do: MyApp.Inflection.pluralize(word, opts)
def singularize(word), do: MyApp.Inflection.singularize(word)
endPlurality itself avoids application configuration so different callers can make different choices without shared global state.
Classical mode with custom modules
Custom modules support the classical: true option. It passes through to
the engine for words that fall through your custom data:
MyApp.Inflection.pluralize("aquarium", classical: true) #=> "aquaria"The inflect/3 function also accepts options:
MyApp.Inflection.inflect("aquarium", 2, classical: true) #=> "aquaria"Generated functions
use Plurality.Custom generates these functions in your module:
| Function | Description |
|---|---|
pluralize/2 | Pluralize with options (check:, classical:) |
singularize/1 | Singularize |
plural?/1 | Check if plural |
singular?/1 | Check if singular |
inflect/3 | Count-based inflection with options |
Design rationale
Custom modules are called directly rather than being registered in a global registry. This is intentional:
- Explicit — no hidden delegation or library-owned application config
- Composable — different parts of your app can use different custom modules
- Zero overhead — compiled into module bytecode, no ETS or runtime config reads
- Compile-time verified — typos in irregular pairs surface at build time