Skip to content

feat: add pluralizer package#1128

Merged
krishankumar01 merged 18 commits intomasterfrom
kkumar-gcc/str-plural-method
Jul 23, 2025
Merged

feat: add pluralizer package#1128
krishankumar01 merged 18 commits intomasterfrom
kkumar-gcc/str-plural-method

Conversation

@krishankumar01
Copy link
Member

@krishankumar01 krishankumar01 commented Jul 16, 2025

📑 Description

Closes goravel/goravel#731

What’s in this PR

  • Brand‑new pluralizer package for Go
  • Out‑of‑the‑box English inflector (singular ↔ plural)
  • Easy to swap languages or add your own
  • Register custom rules without touching core code

Quick Start

import (
     "github.com/goravel/framework/support/pluralizer"
     "github.com/goravel/framework/support/str"
)

// default (English)
fmt.Println(pluralizer.Plural("goose"))    // "geese"
fmt.Println(pluralizer.Singular("heroes")) // "hero"

// fluent strings also provide chaining for these methods 
fmt.Println(str.Of("goose").Plural())    // "geese"
fmt.Println(str.Of("heroes").Singular()) // "hero"

Swap the Global Inflector

Do this at app startup (not inside goroutines)

if err := pluralizer.UseLanguage("english"); err != nil {
    log.Fatal(err)
}

// see what’s active
fmt.Println(pluralizer.GetLanguage().Name()) // "english"

Custom Rules

// irregular: mouse → mice
_ = pluralizer.RegisterIrregular("english",
    pluralizer.Substitution{Singular: "mouse", Plural: "mice"},
)

// mark words uninflected
_ = pluralizer.RegisterUninflected("english", "sheep", "species")

// only plural uninflected
_ = pluralizer.RegisterPluralUninflected("english", "media")

// only singular uninflected
_ = pluralizer.RegisterSingularUninflected("english", "data")

Add Your Own Inflector

Implement this interface:

type Language interface {
    Name() string
    SingularRuleset() Ruleset
    PluralRuleset() Ruleset
}

Register & use it in init():

pluralizer.RegisterLanguage("fantasy", &FantasyLang{})
_ = pluralizer.UseLanguage("fantasy")

Note: Changing or registering inflectors isn’t goroutine‑safe; do it at startup.
Found a missing rule? Please open an issue!

✅ Checks

  • Added test cases for my code

@krishankumar01 krishankumar01 requested a review from a team as a code owner July 16, 2025 16:14
@krishankumar01 krishankumar01 changed the title Add pluralizer package feat: add pluralizer package Jul 16, 2025
@codecov
Copy link

codecov bot commented Jul 16, 2025

Codecov Report

Attention: Patch coverage is 87.40602% with 67 lines in your changes missing coverage. Please review.

Project coverage is 68.44%. Comparing base (bff7225) to head (8caa345).
Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
support/pluralizer/rules/ruleset.go 0.00% 25 Missing ⚠️
support/pluralizer/rules/substitution.go 0.00% 15 Missing ⚠️
support/pluralizer/rules/transformation.go 0.00% 10 Missing ⚠️
support/pluralizer/inflector/inflector.go 74.28% 8 Missing and 1 partial ⚠️
support/pluralizer/rules/pattern.go 0.00% 6 Missing ⚠️
support/pluralizer/inflector/match_case.go 97.67% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1128      +/-   ##
==========================================
+ Coverage   66.81%   68.44%   +1.62%     
==========================================
  Files         214      221       +7     
  Lines       14050    14319     +269     
==========================================
+ Hits         9387     9800     +413     
+ Misses       4287     4144     -143     
+ Partials      376      375       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 72966f3 Previous: bff7225 Ratio
BenchmarkFile_ReadWrite 329027 ns/op 6202 B/op 97 allocs/op 205065 ns/op 6201 B/op 97 allocs/op 1.60
BenchmarkFile_ReadWrite - ns/op 329027 ns/op 205065 ns/op 1.60

This comment was automatically generated by workflow using github-action-benchmark.

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Jul 17, 2025

Thanks, will check this after v1.16 is released.

@hwbrzzl hwbrzzl requested a review from Copilot July 17, 2025 01:48
@hwbrzzl
Copy link
Contributor

hwbrzzl commented Jul 17, 2025

And please add the Review Ready tag when it is ready.

This comment was marked as outdated.

@krishankumar01
Copy link
Member Author

krishankumar01 commented Jul 17, 2025

@hwbrzzl , this package is needed to resolve the table name from the struct name. Currently, it only supports English, but it can be easily extended to support other languages.

Ref: https://laravel.com/docs/12.x/localization#pluralization-language

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the rule come from, please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, great PR 👍

}

func Plural(word string) string {
return instance.Plural(word)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way to add and switch language, add a UseLanguage as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I have introduced a new method for this

"unicode"
)

type inflector struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be used by user when implementing a new language?

Copy link
Member Author

@krishankumar01 krishankumar01 Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I’ve exposed two new methods for this:

  • UseLanguage: Switches the current global language if it already exists
  • RegisterLanguage: Allows you to register a new language

** recommended to use during application boot time

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Em, the logic is a bit complicated now. If so, I don't suggest releasing it to v1.16. A deep review is required. Hence, we can delay this PR as well, given that the another related PR (table -> model) spent many days to be merged.

We don't need to merge them hurriedly, it's fine to release them in v1.17. As we know, errors always appear in this situation.

@krishankumar01
Copy link
Member Author

krishankumar01 commented Jul 17, 2025

Yeah, pluralization is a bit tricky, so the logic ended up being a bit more complex. The earlier version wasn’t very flexible, so I changed it to make it more future-proof. No worries, take your time, we can include it in V1.17. The second PR depends on this, so we can delay that too.

@krishankumar01
Copy link
Member Author

@hwbrzzl Converting this PR to a draft for now, need to do more research on English word dictionaries. I'm exploring different sources to improve pattern accuracy.

@krishankumar01 krishankumar01 marked this pull request as draft July 18, 2025 16:12
@krishankumar01
Copy link
Member Author

This package may not always get pluralization and singularization 100% right, since English (like many languages) has lots of exceptions. But the goal is to keep improving it over time as we come across edge cases. Think of it as a work in progress that gets better the more we use it.

@hwbrzzl
Copy link
Contributor

hwbrzzl commented Jul 20, 2025

This package may not always get pluralization and singularization 100% right, since English (like many languages) has lots of exceptions. But the goal is to keep improving it over time as we come across edge cases. Think of it as a work in progress that gets better the more we use it.

Yes, we can provide a way for users to extend the special cases.

@krishankumar01
Copy link
Member Author

This package may not always get pluralization and singularization 100% right, since English (like many languages) has lots of exceptions. But the goal is to keep improving it over time as we come across edge cases. Think of it as a work in progress that gets better the more we use it.

Yes, we can provide a way for users to extend the special cases.

I've added a few methods to allow users to extend irregular and uninflected cases.

@krishankumar01 krishankumar01 marked this pull request as draft July 20, 2025 08:21
@krishankumar01 krishankumar01 marked this pull request as ready for review July 20, 2025 08:21
@hwbrzzl hwbrzzl requested a review from Copilot July 21, 2025 08:02

This comment was marked as outdated.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing PR, could you add the usage in the description? And I wonder if there is an existing package that can implement such features as well?

Comment on lines +55 to +119
func RegisterIrregular(lang string, substitutions ...pluralizer.Substitution) bool {
if len(substitutions) == 0 {
return false
}

language, factory, exists := getLanguageInstance(lang)
if !exists {
return false
}

language.PluralRuleset().AddIrregular(substitutions...)

flipped := rules.GetFlippedSubstitutions(substitutions...)
language.SingularRuleset().AddIrregular(flipped...)

factory.SetLanguage(language)
return true
}

func RegisterUninflected(lang string, words ...string) bool {
if len(words) == 0 {
return false
}

language, factory, exists := getLanguageInstance(lang)
if !exists {
return false
}

language.PluralRuleset().AddUninflected(words...)
language.SingularRuleset().AddUninflected(words...)

factory.SetLanguage(language)
return true
}

func RegisterPluralUninflected(lang string, words ...string) bool {
if len(words) == 0 {
return false
}

language, factory, exists := getLanguageInstance(lang)
if !exists {
return false
}

language.PluralRuleset().AddUninflected(words...)
factory.SetLanguage(language)
return true
}

func RegisterSingularUninflected(lang string, words ...string) bool {
if len(words) == 0 {
return false
}

language, factory, exists := getLanguageInstance(lang)
if !exists {
return false
}

language.SingularRuleset().AddUninflected(words...)
factory.SetLanguage(language)
return true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about add these functions to type Language interface? When users want to extend:

language := pluralizer.GetLanguage()
language.SingularRuleset.Add...
language.PluralRuleset.Add...
language.Add...

Copy link
Member Author

@krishankumar01 krishankumar01 Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if we add these methods to Language interface, the user would have to implement them for each new custom language, which is unnecessary since the logic will be the same for all languages. Language interface should be minimal.

@krishankumar01
Copy link
Member Author

And I wonder if there is an existing package that can implement such features as well?

Yeah, there are some existing packages, but most of them are limited to English. If we want to support additional languages in the future, they won’t be sufficient. So it's better to implement it ourselves.

Examples:

Comment on lines +141 to +142
PluralizerNoSubstitutionsGiven = New("no substitutions provided").SetModule(ModulePluralizer)
PluralizerNoWordsGiven = New("no words provided").SetModule(ModulePluralizer)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should throw an error in these two cases.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hwbrzzl hwbrzzl requested a review from Copilot July 21, 2025 14:01

This comment was marked as outdated.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, please update the description.

Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add str.Plural and str.Singular in the description as well.

hwbrzzl
hwbrzzl previously approved these changes Jul 22, 2025
Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing PR, thanks!

@krishankumar01 krishankumar01 marked this pull request as draft July 22, 2025 14:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive pluralizer package for the Goravel framework, providing English word inflection capabilities (singular ↔ plural) with an extensible architecture for supporting additional languages. The implementation includes integration with the existing string utility package and follows established design patterns with comprehensive test coverage.

Key changes include:

  • Complete pluralizer package with inflector engine, English language rules, and pattern-based transformation system
  • Integration with the str package through new Plural() and Singular() methods
  • Comprehensive contract interfaces and error handling for language management

Reviewed Changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated no comments.

Show a summary per file
File Description
support/str/str.go Added Plural() and Singular() methods with optional count parameter
support/str/str_test.go Comprehensive test coverage for new string inflection methods
support/pluralizer/ Complete pluralizer package implementation with rules engine
contracts/support/pluralizer/ Interface contracts for language, inflector, and rule components
errors/ New pluralizer-specific error definitions and module registration
mocks/support/pluralizer/ Generated mock implementations for all pluralizer interfaces
Comments suppressed due to low confidence (13)

support/pluralizer/inflector/inflector.go:19

  • [nitpick] The receiver variable name 'r' is ambiguous for an Inflector type. Consider using 'i' or 'inf' to better represent the type.
func (r *Inflector) Language() pluralizer.Language {

support/pluralizer/inflector/inflector.go:23

  • [nitpick] The receiver variable name 'r' is ambiguous for an Inflector type. Consider using 'i' or 'inf' to better represent the type.
func (r *Inflector) Plural(word string) string {

support/pluralizer/inflector/inflector.go:27

  • [nitpick] The receiver variable name 'r' is ambiguous for an Inflector type. Consider using 'i' or 'inf' to better represent the type.
func (r *Inflector) SetLanguage(language pluralizer.Language) pluralizer.Inflector {

support/pluralizer/inflector/inflector.go:33

  • [nitpick] The receiver variable name 'r' is ambiguous for an Inflector type. Consider using 'i' or 'inf' to better represent the type.
func (r *Inflector) Singular(word string) string {

support/pluralizer/inflector/inflector.go:37

  • [nitpick] The receiver variable name 'r' is ambiguous for an Inflector type. Consider using 'i' or 'inf' to better represent the type.
func (r *Inflector) inflect(word string, ruleset pluralizer.Ruleset) string {

support/pluralizer/rules/transformation.go:23

  • [nitpick] The receiver variable name 'r' is ambiguous for a Transformation type. Consider using 't' or 'trans' to better represent the type.
func (r *Transformation) Apply(word string) string {

support/pluralizer/rules/substitution.go:21

  • [nitpick] The receiver variable name 'r' is ambiguous for a Substitution type. Consider using 's' or 'sub' to better represent the type.
func (r *Substitution) From() string {

support/pluralizer/rules/substitution.go:25

  • [nitpick] The receiver variable name 'r' is ambiguous for a Substitution type. Consider using 's' or 'sub' to better represent the type.
func (r *Substitution) To() string {

support/pluralizer/rules/ruleset.go:21

  • [nitpick] The receiver variable name 'r' is ambiguous for a Ruleset type. Consider using 'rs' or 'ruleset' to better represent the type.
func (r *Ruleset) AddIrregular(substitutions ...pluralizer.Substitution) pluralizer.Ruleset {

support/pluralizer/rules/pattern.go:20

  • [nitpick] The receiver variable name 'r' is ambiguous for a Pattern type. Consider using 'p' or 'pat' to better represent the type.
func (r *Pattern) Matches(word string) bool {

support/pluralizer/english/english.go:63

  • [nitpick] The receiver variable name 'r' is ambiguous for a Language type. Consider using 'l' or 'lang' to better represent the type.
func (r *Language) Name() string {

support/pluralizer/english/english.go:67

  • [nitpick] The receiver variable name 'r' is ambiguous for a Language type. Consider using 'l' or 'lang' to better represent the type.
func (r *Language) SingularRuleset() pluralizer.Ruleset {

support/pluralizer/english/english.go:71

  • [nitpick] The receiver variable name 'r' is ambiguous for a Language type. Consider using 'l' or 'lang' to better represent the type.
func (r *Language) PluralRuleset() pluralizer.Ruleset {

@krishankumar01 krishankumar01 marked this pull request as ready for review July 23, 2025 11:57
Copy link
Contributor

@hwbrzzl hwbrzzl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@krishankumar01 krishankumar01 merged commit e1b9bf0 into master Jul 23, 2025
14 checks passed
@krishankumar01 krishankumar01 deleted the kkumar-gcc/str-plural-method branch July 23, 2025 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for pluralizer

3 participants