Skip to content

Add grammatical-case-aware TimeSpan humanization - #1879

Merged
clairernovotny merged 9 commits into
mainfrom
codex/timespan-grammatical-cases-686
Jul 29, 2026
Merged

Add grammatical-case-aware TimeSpan humanization#1879
clairernovotny merged 9 commits into
mainfrom
codex/timespan-grammatical-cases-686

Conversation

@clairernovotny

@clairernovotny clairernovotny commented Jul 29, 2026

Copy link
Copy Markdown
Member

Summary

TimeSpan can now produce a bare duration phrase in an explicitly requested grammatical case through HumanizeWithCase, without changing any existing Humanize, formatter, or strategy member or output.

The case-aware path uses explicit generated locale data, applies one case across multipart durations, keeps multi-count output numeric, and fails clearly when a locale, unit, or configured custom component cannot support the request. All 102 locale files are accounted for through 57 distinct roots, 34 not-applicable roots, and 11 same-language inherited children, with zero unsupported applicable roots, cases, or units and no English or invented-form fallback.

Compatibility

Existing Humanize, IFormatter, and ITimeSpanHumanizeStrategy contracts remain unchanged. Case support is exposed only through optional capability interfaces, including explicit implementations on the default concrete formatter and strategy so legacy calls using default remain source-compatible.

Custom formatter and strategy subclasses must explicitly opt into the new capabilities. The new dispatch also composes with the existing fractional-duration capability and preserves its exact-type opt-in and fail-closed behavior.

Legacy duration, zero, age, and toWords locale surfaces are byte-identical to the semantic base. Native citation phrases that differ are isolated inside durationCases.

Locale data and credit

Every root declares an explicit inventory, citation case, evidence, and per-case realization. Authored children replace the inherited discriminated union; omitted data inherits only through same-language, same-script parents. sameRenderedAs aliases are cycle checked and flattened to an authored terminal.

Explicit case-number contracts preserve every reachable named form even when its rendered text equals the default form. This keeps South Slavic singular, paucal, and plural selection correct for compound counts while leaving ordinary formatter fallback and duplicate collapsing unchanged.

Duration patterns use Unicode CLDR 48 at commit acd6d88ae493633240e19a87a721076a8a75c310 where available, plus immutable per-locale grammar, corpus, dictionary, and native-review sources recorded in the YAML. Thanks to @JFBM for reporting the original German use case in #686, and to the source authors and language-family reviewers credited in the locale data.

Validation

  • Source-generator suite: 158/158 on net10.0 and net11.0
  • Focused runtime and compatibility: 353/353 on net8.0, net10.0, and net11.0
  • Generated locale phrase grid: 228/228 on net8.0, net10.0, and net11.0
  • Public API approval: 1/1 on net8.0, net10.0, and net11.0; all four approval surfaces reconciled
  • Complete documentation validation, current and frozen API contracts, examples, content, fingerprints, and upgrade paths
  • Exact format verification
  • Full Humanizer tests: 58,838/58,838 on each of net8.0, net10.0, and net11.0
  • Fresh Release pack for netstandard2.0, net48, net8.0, net10.0, and net11.0
  • Packed analyzer entries and net8.0 analyzer smoke test
  • DevSkim DS173237 provenance waiver audit: 91 exact immutable revisions, 91 suppressed, zero out-of-scope waivers

Submission checklist

  • Implementation is clean
  • Code adheres to the existing coding standards
  • No Code Analysis warnings
  • There is proper unit test coverage
  • No copied StackOverflow, blog, or OSS code requiring disclosure
  • Comments are limited to necessary documentation and generated-data provenance
  • XML documentation is added for the new public API
  • The PR is rebased on the required latest semantic main baseline
  • The issue is linked with closing syntax
  • README and current documentation are updated
  • Applicable code and documentation validation has passed

Fixes #686

Copilot AI review requested due to automatic review settings July 29, 2026 01:15
@clairernovotny clairernovotny added this to the v4.0 milestone Jul 29, 2026
@clairernovotny clairernovotny added enhancement Adds or improves user-facing functionality localisation Changes translations, locale data, or culture-specific behavior labels Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds grammatical-case-aware TimeSpan humanization, locale duration-case schemas and generated tables, localized case data, expanded public APIs, validation, tests, and documentation.

Changes

Grammatical-case duration formatting

Layer / File(s) Summary
Locale schema and generated tables
src/Humanizer.SourceGenerators/..., src/Humanizer/Locales/*
Adds duration-case parsing, validation, inheritance rules, plural-form handling, generated lookup tables, and locale-authored duration forms.
Runtime API and formatting pipeline
src/Humanizer/TimeSpanHumanizeExtensions.cs, src/Humanizer/Localisation/Formatters/*, src/Humanizer/TimeSpanHumanizeStrategy/*
Adds HumanizeWithCase, formatter and strategy interfaces, grammatical-case enum members, case-aware unit formatting, number detectors, and explicit unsupported-case failures.
Validation and compatibility tests
tests/Humanizer.SourceGenerators.Tests/..., tests/Humanizer.Tests/...
Covers schema validation, inheritance, generated output, locale-specific forms, argument validation, unsupported cases and units, legacy compatibility, and public API snapshots.
Documentation and examples
readme.md, website/docs/..., website/scenario-api-contract.json
Documents the bare-duration contract, locale authoring rules, extension requirements, API surface, examples, and related guides.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TimeSpanHumanizeExtensions
  participant HumanizeStrategy
  participant DefaultFormatter
  participant LocaleDurationCaseTableCatalog
  Caller->>TimeSpanHumanizeExtensions: HumanizeWithCase(timeSpan, grammaticalCase)
  TimeSpanHumanizeExtensions->>HumanizeStrategy: Humanize(..., grammaticalCase)
  HumanizeStrategy->>DefaultFormatter: TimeSpanHumanize(timeUnit, count, grammaticalCase)
  DefaultFormatter->>LocaleDurationCaseTableCatalog: Resolve(culture)
  LocaleDurationCaseTableCatalog-->>DefaultFormatter: LocaleDurationCaseTable
  DefaultFormatter-->>HumanizeStrategy: Locale-authored duration phrase
  HumanizeStrategy-->>Caller: Bare grammatical-case duration
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

A rabbit hops through cases bright,
German words line up just right.
Forms bloom softly, rules take flight,
No fallback fog obscures the sight.
Bare phrases shine in local light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: grammatical-case-aware TimeSpan humanization.
Description check ✅ Passed The description matches the implemented feature and supporting compatibility notes.
Linked Issues check ✅ Passed The PR implements the requested case-aware TimeSpan API and German inflection support described in #686.
Out of Scope Changes check ✅ Passed The additional locale data, docs, generators, and tests all support the same grammatical-case TimeSpan feature.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/timespan-grammatical-cases-686

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a987414834

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Humanizer/Localisation/Formatters/DefaultFormatter.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new, opt-in grammatical-case-aware TimeSpan humanization API (HumanizeWithCase) to Humanizer, along with generated locale coverage tables, new capability interfaces for custom strategies/formatters, tests, and documentation so callers can request cased duration phrases without changing existing Humanize contracts.

Changes:

  • Introduces TimeSpan.HumanizeWithCase(...) plus new optional capability interfaces (IGrammaticalCaseTimeSpanHumanizeStrategy, IGrammaticalCaseTimeSpanFormatter) implemented by the defaults.
  • Extends locale YAML + source generator pipeline to classify and (when available) provide per-unit case overlays with explicit unsupported/not-applicable behavior.
  • Adds runtime + generator tests, public API approvals, and documentation updates covering the new API and authoring surface.

Reviewed changes

Copilot reviewed 135 out of 135 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
website/scenario-api-contract.json Adds the new capability interface docs to scenario contract validation.
website/docs/scenarios/localization-and-extensibility.mdx Documents how case-aware duration behavior is wired via optional strategy/formatter capabilities.
website/docs/scenarios/durations-and-ages.mdx Adds scenario documentation and example usage for HumanizeWithCase.
website/docs/scenarios/dates-times-durations-and-age.mdx Links new case-aware duration APIs from the overview scenario page.
website/docs/languages/supported-cultures.mdx Clarifies that duration-case coverage is separate from general locale coverage.
website/docs/languages/grammar-and-platform-formatting.mdx Notes scope/constraints of case-aware duration formatting.
website/docs/languages/custom-localization.mdx Documents custom formatter/strategy requirements for HumanizeWithCase.
website/docs/contributing/locale-yaml-reference.mdx Documents the new surfaces.durationCases authoring surface and runtime contract.
website/docs/api/Humanizer.TimeSpanHumanizeExtensions.md Adds API reference entries for the two HumanizeWithCase overloads.
website/docs/api/Humanizer.md Adds the two new capability interfaces to the API index.
website/docs/api/Humanizer.ITimeSpanHumanizeStrategy.md Adds the new derived interface to the ITimeSpanHumanizeStrategy page.
website/docs/api/Humanizer.IGrammaticalCaseTimeSpanHumanizeStrategy.md New API doc page for case-aware time-span strategy capability.
website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md New API doc page for case-aware formatter capability.
website/docs/api/Humanizer.IFormatter.md Adds the new derived formatter interface to the IFormatter page.
website/docs/api/Humanizer.GrammaticalCase.md Updates doc output for the expanded GrammaticalCase enum.
website/docs/api/Humanizer.DefaultTimeSpanHumanizeStrategy.md Documents DefaultTimeSpanHumanizeStrategy implementing the new capability method.
website/docs/api/Humanizer.DefaultFormatter.md Documents DefaultFormatter implementing the new case-aware TimeSpan formatting method.
tests/Humanizer.Tests/TimeSpanHumanizeTests.cs Adds unit tests for HumanizeWithCase, invalid inputs, and legacy custom component behavior.
tests/Humanizer.Tests/Localisation/ml/MalayalamDurationCaseTests.cs Adds Malayalam case-aware duration assertions and an explicit unsupported-unit test.
tests/Humanizer.Tests/Localisation/hu/HungarianDurationCaseTests.cs Adds Hungarian case-aware duration assertions.
tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs Pins selected CLDR-derived case forms + verifies “unsupported” locale behavior throws clearly.
tests/Humanizer.Tests/Localisation/fi/FinnishDurationCaseTests.cs Adds Finnish case-aware duration assertions.
tests/Humanizer.Tests/Localisation/de/GermanDurationCaseTests.cs Adds German case-aware singular/multiple expectations (original motivating example).
tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt Approves new public API surface for net48.
tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt Approves new public API surface for net8.
tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt Approves new public API surface for net10.
tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet11_0.verified.txt Approves new public API surface for net11.
tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs Adds generator tests ensuring duration-case overlays are complete and validated.
tests/Humanizer.SourceGenerators.Tests/SourceGenerators/CanonicalLocaleSchemaTests.cs Adds generator tests verifying every checked-in locale is duration-case-classified with expected counts.
src/Humanizer/TimeSpanHumanizeStrategy/IGrammaticalCaseTimeSpanHumanizeStrategy.cs Introduces the new optional strategy capability interface.
src/Humanizer/TimeSpanHumanizeStrategy/DefaultTimeSpanHumanizeStrategy.cs Implements the capability method and routes it through DefaultHumanize.
src/Humanizer/TimeSpanHumanizeExtensions.cs Adds HumanizeWithCase overloads and threads an optional GrammaticalCase through formatting pipeline.
src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs Refactors unit-gender lookup to a virtual hook for reuse/consistency.
src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs Adds runtime structures + culture-resolution for generated duration-case tables.
src/Humanizer/Localisation/Formatters/IGrammaticalCaseTimeSpanFormatter.cs Introduces the new optional formatter capability interface.
src/Humanizer/Localisation/Formatters/DefaultFormatter.cs Implements case-aware duration formatting and consults generated duration-case tables.
src/Humanizer/Locales/af.yml Adds durationCases classification metadata.
src/Humanizer/Locales/am.yml Adds durationCases classification metadata and overlays (where applicable).
src/Humanizer/Locales/ar.yml Adds durationCases classification metadata.
src/Humanizer/Locales/as.yml Adds durationCases classification metadata.
src/Humanizer/Locales/az.yml Adds durationCases classification metadata.
src/Humanizer/Locales/be.yml Adds durationCases classification metadata.
src/Humanizer/Locales/bg.yml Adds durationCases classification metadata.
src/Humanizer/Locales/bn.yml Adds durationCases classification metadata.
src/Humanizer/Locales/bs.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ca.yml Adds durationCases classification metadata.
src/Humanizer/Locales/cy.yml Adds durationCases classification metadata.
src/Humanizer/Locales/da.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/de.yml Adds durationCases overlays for German case support.
src/Humanizer/Locales/el.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/en.yml Adds durationCases classification metadata (not-applicable).
src/Humanizer/Locales/es.yml Adds durationCases classification metadata.
src/Humanizer/Locales/et.yml Adds durationCases classification metadata.
src/Humanizer/Locales/eu.yml Adds durationCases classification metadata.
src/Humanizer/Locales/fa.yml Adds durationCases classification metadata.
src/Humanizer/Locales/fi.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/fil.yml Adds durationCases classification metadata.
src/Humanizer/Locales/fr.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ga.yml Adds durationCases classification metadata.
src/Humanizer/Locales/gl.yml Adds durationCases classification metadata.
src/Humanizer/Locales/gu.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ha.yml Adds durationCases classification metadata.
src/Humanizer/Locales/he.yml Adds durationCases classification metadata.
src/Humanizer/Locales/hi.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/hr.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/hu.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/id.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ig.yml Adds durationCases classification metadata.
src/Humanizer/Locales/is.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/it.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ja.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ka.yml Adds durationCases classification metadata.
src/Humanizer/Locales/kk.yml Adds durationCases classification metadata.
src/Humanizer/Locales/km.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ko.yml Adds durationCases classification metadata.
src/Humanizer/Locales/kok.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ku.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ky.yml Adds durationCases classification metadata.
src/Humanizer/Locales/lb.yml Adds durationCases classification metadata.
src/Humanizer/Locales/lo.yml Adds durationCases classification metadata.
src/Humanizer/Locales/lv.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/mk.yml Adds durationCases classification metadata.
src/Humanizer/Locales/mn.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ms.yml Adds durationCases classification metadata.
src/Humanizer/Locales/mt.yml Adds durationCases classification metadata.
src/Humanizer/Locales/my.yml Adds durationCases classification metadata.
src/Humanizer/Locales/nb.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/ne.yml Adds durationCases classification metadata.
src/Humanizer/Locales/nl.yml Adds durationCases classification metadata.
src/Humanizer/Locales/nn.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/or.yml Adds durationCases classification metadata.
src/Humanizer/Locales/pa.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/pa-Arab.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ps.yml Adds durationCases classification metadata.
src/Humanizer/Locales/pt.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ro.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/si.yml Adds durationCases classification metadata.
src/Humanizer/Locales/so.yml Adds durationCases classification metadata.
src/Humanizer/Locales/sq.yml Adds durationCases classification metadata.
src/Humanizer/Locales/sr.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/sr-Latn.yml Adds durationCases classification metadata.
src/Humanizer/Locales/sv.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/sw.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ta.yml Adds durationCases overlays for distinct case support.
src/Humanizer/Locales/te.yml Adds durationCases classification metadata.
src/Humanizer/Locales/th.yml Adds durationCases classification metadata.
src/Humanizer/Locales/tk.yml Adds durationCases classification metadata.
src/Humanizer/Locales/tr.yml Adds durationCases classification metadata.
src/Humanizer/Locales/ur.yml Adds durationCases classification metadata.
src/Humanizer/Locales/uz-Cyrl-UZ.yml Adds durationCases classification metadata.
src/Humanizer/Locales/uz-Latn-UZ.yml Adds durationCases classification metadata.
src/Humanizer/Locales/vi.yml Adds durationCases classification metadata.
src/Humanizer/Locales/yo.yml Adds durationCases classification metadata.
src/Humanizer/Locales/zh-Hans.yml Adds durationCases classification metadata.
src/Humanizer/Locales/zh-Hant.yml Adds durationCases classification metadata.
src/Humanizer/Locales/zu-ZA.yml Adds durationCases classification metadata.
src/Humanizer/GrammaticalCase.cs Expands GrammaticalCase enum to include additional CLDR-relevant cases.
src/Humanizer.SourceGenerators/HumanizerSourceGenerator.cs Wires new duration-case table generation into the generator pipeline.
src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs Generates the runtime duration-case lookup tables per locale.
src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs Adds durationCases to the list of required locale surfaces.
src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs Exposes phrase parsing helper for duration-case normalization.
src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs Adds parsing/validation models for the durationCases YAML surface + coverage generation.
src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs Includes durationCases in canonical surface extraction.
readme.md Adds a quick-start example and contract notes for HumanizeWithCase.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Humanizer/Localisation/Formatters/DefaultFormatter.cs Outdated
Comment thread src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
Copilot AI review requested due to automatic review settings July 29, 2026 03:28
@clairernovotny
clairernovotny force-pushed the codex/timespan-grammatical-cases-686 branch from a987414 to a8944c7 Compare July 29, 2026 03:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8944c787d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Humanizer/Localisation/Formatters/DefaultFormatter.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Comments suppressed due to low confidence (2)

src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs:34

  • The primary-constructor parameter 'cases' is immediately shadowed by a field with the same name (readonly ... cases = cases;). Even if it compiles, it’s easy to misread and makes later edits error-prone. Use a distinct field name (e.g. _cases).
    src/Humanizer/TimeSpanHumanizeExtensions.cs:482
  • This NotSupportedException message falls back to CultureInfo.CurrentUICulture when no culture is provided, but Configurator.GetFormatter(null) resolves using CultureInfo.CurrentCulture. The message can therefore report the wrong culture name.

Comment thread src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
Comment thread src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (5)
src/Humanizer/TimeSpanHumanizeExtensions.cs (1)

176-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ambiguous-by-reading overload: the 8th positional bool changes meaning.

In the existing DefaultHumanize the 8th parameter is toWords; in this new overload it is toSymbols. The compiler distinguishes them by the trailing GrammaticalCase, but a caller passing true positionally gets a silently different behavior. Consider renaming this entry point (e.g. DefaultHumanizeWithCase) to keep the internal contract unmistakable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/TimeSpanHumanizeExtensions.cs` around lines 176 - 196, Rename
the new DefaultHumanize overload that accepts GrammaticalCase to an unambiguous
name such as DefaultHumanizeWithCase, and update all callers accordingly. Keep
the existing DefaultHumanize overload and its toWords positional contract
unchanged.
src/Humanizer/Localisation/Formatters/DefaultFormatter.cs (1)

66-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Case-aware formatting is not overridable by derived formatters.

Every other formatting member on this public type is virtual, but this is an explicit interface implementation, so subclasses (including third-party formatters deriving from DefaultFormatter) can neither override nor call it. If the intent is that locale formatters can specialize case handling, expose a protected virtual core method that the explicit implementation delegates to; retrofitting that after release is harder than doing it now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/Localisation/Formatters/DefaultFormatter.cs` around lines 66 -
120, The explicit IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize
implementation prevents derived formatters from specializing case-aware
formatting. Extract its validation and formatting logic into a protected virtual
core method on DefaultFormatter, then have the explicit interface member
delegate to that method so subclasses can override the behavior.
src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs (1)

50-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Surface authoring errors as generator diagnostics instead of thrown exceptions.

Create throws for both non-empty diagnostics and for any locale missing durationCases. Since LocaleDurationCaseTableCatalogInput.Create (src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs, Line 49-58) only guards Diagnostics.IsEmpty, a locale YAML file that parses cleanly but omits the surface will crash the generator instead of producing the InvalidLocaleDefinition diagnostic used by every other locale path in LocaleCatalogInput.Create. Consider returning diagnostics from this layer (or catching in the caller) so authors get an actionable error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs` around lines 50
- 92, Update DurationCaseCoverageInput.Create to report missing durationCases
classifications through the generator’s existing InvalidLocaleDefinition
diagnostic flow instead of throwing InvalidOperationException. Reuse the
established diagnostic-returning pattern used by LocaleCatalogInput.Create and
LocaleDurationCaseTableCatalogInput.Create, while preserving successful catalog
and coverage-row construction for valid locales.
src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs (2)

25-45: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Emitted case array and runtime index share one unenforced contract with GrammaticalCase. The generator emits a fixed-order LocalizedDurationCase?[] from CaseOrder, and the runtime looks entries up by raw enum ordinal with no length or order validation anywhere; adding an enum member without touching the generator silently misresolves cases or throws IndexOutOfRangeException.

  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs#L25-L45: derive/validate CaseOrder against the GrammaticalCase member list (or emit a compile-time assertion on the final member) so a mismatch fails the build.
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs#L32-L43: bounds-check the index in TryGetCase and return false for out-of-range values instead of relying on every caller pre-validating.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs`
around lines 25 - 45, The CaseOrder contract with GrammaticalCase is unenforced:
update LocaleDurationCaseTableCatalogInput.cs lines 25-45 to derive or validate
the emitted order against the enum members, or add a compile-time assertion that
fails when the final member changes; update LocaleDurationCaseTable.cs lines
32-43 in TryGetCase to bounds-check the raw enum index and return false for
out-of-range values.

25-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Positional coupling between CaseOrder and the GrammaticalCase enum is unenforced.

The emitted LocalizedDurationCase?[] is indexed at runtime by (int)grammaticalCase (src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs, Line 34). Today both lists have 18 entries in the same order, but nothing fails the build if a future enum member is added without updating this array — the failure would be a silent wrong-case lookup or an IndexOutOfRangeException. Consider emitting a compile-time guard (e.g. generate the array sized/ordered from a shared name list and emit a static assertion on GrammaticalCase.Translative) or a generator-side length check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs`
around lines 25 - 45, Add generator-side validation in the catalog generation
flow that compares CaseOrder against the GrammaticalCase enum’s expected member
count and ordering, failing generation when they diverge. Anchor the check to
CaseOrder and the emitted LocalizedDurationCase?[] so runtime indexing by
(int)grammaticalCase remains safe; preserve the existing case order when
validation succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Humanizer/Locales/cs.yml`:
- Around line 108-123: Update the plural entries in the Czech duration-case
overlays in cs.yml, replacing incorrect forms with the proper plural ending for
each grammatical case: dative sekundám, instrumental sekundami, and locative
sekundách. Apply this consistently to every affected duration unit and overlay
range, including the visible second forms, while leaving singular, dual, paucal,
and numeric entries unchanged.

In `@src/Humanizer/Locales/fi.yml`:
- Around line 304-314: Update illative.units.minute.multiple.forms in the
Finnish locale so the default, dual, paucal, and plural forms use minuuttiin
instead of minuutista, matching the existing singular illative form and
neighboring units.
- Around line 183-189: Update the year forms under countPlacement in the Finnish
locale so default, dual, paucal, and plural use the partitive form “{count}
vuotta” instead of “{count} vuoteen”; preserve the existing singular form.

In `@src/Humanizer/Locales/hr.yml`:
- Around line 154-243: Update the instrumental duration templates under
instrumental.units so every count-bearing multiple form uses valid Croatian
instrumental plural forms, including sekundama, minutama, satima, danima,
tjednima, mjesecima, and godinama. Replace the existing count-bearing forms
across default, singular, dual, paucal, and plural entries while preserving the
single numeric forms and template structure.

In `@src/Humanizer/Locales/hu.yml`:
- Around line 10-22: Update the accusative millisecond default form under
units.millisecond.multiple.forms to use the accusative “-et” ending, matching
the singular form and other accusative categories; leave the surrounding locale
entries unchanged.

In `@src/Humanizer/Locales/is.yml`:
- Around line 238-248: Update the multiple week duration forms in the week
localization block so every count-based form renders “{count} viku” instead of
“{count} vikna”, while preserving the existing “vikur” forms elsewhere in the
block.

In `@src/Humanizer/Locales/kn.yml`:
- Around line 400-405: Update the vocative `year` locale forms so `default`,
`dual`, `paucal`, and `plural` use the correct counted plural morphology,
matching the established Kannada plural form for non-singular counts. Preserve
`singular` as `{count} ವರ್ಷ`; if Kannada intentionally treats `ವರ್ಷ` as
invariant, verify and retain that behavior explicitly.

In `@src/Humanizer/Locales/ml.yml`:
- Around line 253-333: The Malayalam instrumental duration forms in the
instrumental.units entries currently use the generic postposition “കൊണ്ട്”;
replace each unit’s single numeric and multiple plural-form values with the
corresponding CLDR 48 “-നാൽ” inflected forms, including second, minute, hour,
day, week, month, and year, while preserving the existing structure and count
placement.

In `@src/Humanizer/Locales/nb.yml`:
- Around line 7-8: Fix the shared durationCases YAML nesting so it is a child of
inflection, allowing the generator to read surfaces.inflection.durationCases.
Apply the indentation change in src/Humanizer/Locales/nb.yml lines 7-8,
src/Humanizer/Locales/nn.yml lines 8-9, src/Humanizer/Locales/my.yml lines 7-8,
src/Humanizer/Locales/ne.yml lines 7-8, and src/Humanizer/Locales/nl.yml lines
7-8; indent each block’s classification content beneath durationCases as needed.

In `@src/Humanizer/Locales/pl.yml`:
- Around line 7-99: Update the dual entries under durationCases.units for
second, minute, hour, week, month, and year across the genitive, dative,
instrumental, locative, and vocative case sections in pl.yml. Replace reused
singular or genitive text with the correct Polish dual form for each declared
case, including dative -om, instrumental -ami, locative -ach, and vocative
forms, while preserving the existing single, default, singular, paucal, and
plural values.

In `@src/Humanizer/Locales/ro.yml`:
- Around line 7-99: Update the Romanian genitive duration entries under
durationCases to preserve numeric counts in every multiple form: make the
default form retain the {count} placeholder for fallback counts such as 21, and
configure the dual form as the bare counted phrase without de for 2. Keep
single.numeric unchanged for exact one-unit phrases and apply the same pattern
consistently across all listed units.

In `@src/Humanizer/Locales/sk.yml`:
- Around line 18-22: Update the Slovak locale plural forms for each duration
unit in the genitive, dative, accusative, instrumental, and locative case
sections, replacing nominative-style values with grammatically correct
case-specific plural forms. Preserve the existing default, singular, dual, and
paucal values unless they also require correction for the same case-specific
rule.

In `@src/Humanizer/Locales/sr.yml`:
- Around line 100-103: Update the Serbian locale’s accusative millisecond entry
under the duration case table to use an explicit accusative translation of
“милисекунде” instead of sameAsNominative. Follow the existing explicit
case-mapping structure in sr.yml.

In `@src/Humanizer/Locales/ta.yml`:
- Around line 45-55: Update the multiple-count hour forms in all three hour
entries to use plural Tamil மணிநேரங்… templates for default, dual, paucal, and
plural, while preserving the singular form unchanged. Apply the appropriate
plural base for each context, such as மணிநேரங்களுக்கு, மணிநேரங்களை, or
மணிநேரங்களில், so counts of two or more no longer use singular-hour wording.
- Around line 146-153: Update the Tamil durationCases.accusative.day singular
configuration so one-day accusative durations use a count-bearing phrase with
the {count} placeholder, replacing the relative-date “நாளை” value while
preserving the existing multiple/default form structure.

In `@src/Humanizer/Localisation/Formatters/DefaultFormatter.cs`:
- Around line 339-358: Update FormatCaseAwareTimeSpanPhrase so count == 1 always
returns the numeric singular phrase via phrase.Single, rather than preferring
SingleWordsVariant. Preserve the existing fallback/error behavior and add
coverage for a locale defining both singular variants to verify HumanizeWithCase
uses the numeric form.

In `@src/Humanizer/TimeSpanHumanizeExtensions.cs`:
- Around line 467-482: The NotSupportedException in FormatTimePart reports
CurrentUICulture even though the formatter fallback uses CurrentCulture. Update
the fallback culture reference in the exception message to CurrentCulture while
preserving the existing explicit culture-name behavior.

In `@website/docs/api/Humanizer.DefaultFormatter.md`:
- Around line 305-314: Use sequential heading levels for the generated API
sections by changing the Parameters heading to h3 in
website/docs/api/Humanizer.DefaultFormatter.md (lines 305-314) and
website/docs/api/Humanizer.DefaultTimeSpanHumanizeStrategy.md (lines 84-93); no
other content changes are needed.

In `@website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md`:
- Around line 24-48: Normalize the heading hierarchy by changing the Parameters,
Returns, and Exceptions headings to ### under the method headings in
website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md:24-48,
website/docs/api/Humanizer.IGrammaticalCaseTimeSpanHumanizeStrategy.md:26-86,
and both overload sections in
website/docs/api/Humanizer.TimeSpanHumanizeExtensions.md:242-296 and :314-362.

---

Nitpick comments:
In `@src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs`:
- Around line 50-92: Update DurationCaseCoverageInput.Create to report missing
durationCases classifications through the generator’s existing
InvalidLocaleDefinition diagnostic flow instead of throwing
InvalidOperationException. Reuse the established diagnostic-returning pattern
used by LocaleCatalogInput.Create and
LocaleDurationCaseTableCatalogInput.Create, while preserving successful catalog
and coverage-row construction for valid locales.

In
`@src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs`:
- Around line 25-45: The CaseOrder contract with GrammaticalCase is unenforced:
update LocaleDurationCaseTableCatalogInput.cs lines 25-45 to derive or validate
the emitted order against the enum members, or add a compile-time assertion that
fails when the final member changes; update LocaleDurationCaseTable.cs lines
32-43 in TryGetCase to bounds-check the raw enum index and return false for
out-of-range values.
- Around line 25-45: Add generator-side validation in the catalog generation
flow that compares CaseOrder against the GrammaticalCase enum’s expected member
count and ordering, failing generation when they diverge. Anchor the check to
CaseOrder and the emitted LocalizedDurationCase?[] so runtime indexing by
(int)grammaticalCase remains safe; preserve the existing case order when
validation succeeds.

In `@src/Humanizer/Localisation/Formatters/DefaultFormatter.cs`:
- Around line 66-120: The explicit
IGrammaticalCaseTimeSpanFormatter.TimeSpanHumanize implementation prevents
derived formatters from specializing case-aware formatting. Extract its
validation and formatting logic into a protected virtual core method on
DefaultFormatter, then have the explicit interface member delegate to that
method so subclasses can override the behavior.

In `@src/Humanizer/TimeSpanHumanizeExtensions.cs`:
- Around line 176-196: Rename the new DefaultHumanize overload that accepts
GrammaticalCase to an unambiguous name such as DefaultHumanizeWithCase, and
update all callers accordingly. Keep the existing DefaultHumanize overload and
its toWords positional contract unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f5004c69-fc1d-456f-b1ac-30e14f90f668

📥 Commits

Reviewing files that changed from the base of the PR and between 567fa79 and a8944c7.

📒 Files selected for processing (137)
  • readme.md
  • src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs
  • src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
  • src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs
  • src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs
  • src/Humanizer.SourceGenerators/HumanizerSourceGenerator.cs
  • src/Humanizer/GrammaticalCase.cs
  • src/Humanizer/Locales/af.yml
  • src/Humanizer/Locales/am.yml
  • src/Humanizer/Locales/ar.yml
  • src/Humanizer/Locales/as.yml
  • src/Humanizer/Locales/az.yml
  • src/Humanizer/Locales/be.yml
  • src/Humanizer/Locales/bg.yml
  • src/Humanizer/Locales/bn.yml
  • src/Humanizer/Locales/bs.yml
  • src/Humanizer/Locales/ca.yml
  • src/Humanizer/Locales/cs.yml
  • src/Humanizer/Locales/cy.yml
  • src/Humanizer/Locales/da.yml
  • src/Humanizer/Locales/de.yml
  • src/Humanizer/Locales/el.yml
  • src/Humanizer/Locales/en.yml
  • src/Humanizer/Locales/es.yml
  • src/Humanizer/Locales/et.yml
  • src/Humanizer/Locales/eu.yml
  • src/Humanizer/Locales/fa.yml
  • src/Humanizer/Locales/fi.yml
  • src/Humanizer/Locales/fil.yml
  • src/Humanizer/Locales/fr.yml
  • src/Humanizer/Locales/ga.yml
  • src/Humanizer/Locales/gl.yml
  • src/Humanizer/Locales/gu.yml
  • src/Humanizer/Locales/ha.yml
  • src/Humanizer/Locales/he.yml
  • src/Humanizer/Locales/hi.yml
  • src/Humanizer/Locales/hr.yml
  • src/Humanizer/Locales/hu.yml
  • src/Humanizer/Locales/hy.yml
  • src/Humanizer/Locales/id.yml
  • src/Humanizer/Locales/ig.yml
  • src/Humanizer/Locales/is.yml
  • src/Humanizer/Locales/it.yml
  • src/Humanizer/Locales/ja.yml
  • src/Humanizer/Locales/ka.yml
  • src/Humanizer/Locales/kk.yml
  • src/Humanizer/Locales/km.yml
  • src/Humanizer/Locales/kn.yml
  • src/Humanizer/Locales/ko.yml
  • src/Humanizer/Locales/kok.yml
  • src/Humanizer/Locales/ku.yml
  • src/Humanizer/Locales/ky.yml
  • src/Humanizer/Locales/lb.yml
  • src/Humanizer/Locales/lo.yml
  • src/Humanizer/Locales/lt.yml
  • src/Humanizer/Locales/lv.yml
  • src/Humanizer/Locales/mk.yml
  • src/Humanizer/Locales/ml.yml
  • src/Humanizer/Locales/mn.yml
  • src/Humanizer/Locales/mr.yml
  • src/Humanizer/Locales/ms.yml
  • src/Humanizer/Locales/mt.yml
  • src/Humanizer/Locales/my.yml
  • src/Humanizer/Locales/nb.yml
  • src/Humanizer/Locales/ne.yml
  • src/Humanizer/Locales/nl.yml
  • src/Humanizer/Locales/nn.yml
  • src/Humanizer/Locales/or.yml
  • src/Humanizer/Locales/pa-Arab.yml
  • src/Humanizer/Locales/pa.yml
  • src/Humanizer/Locales/pl.yml
  • src/Humanizer/Locales/ps.yml
  • src/Humanizer/Locales/pt.yml
  • src/Humanizer/Locales/ro.yml
  • src/Humanizer/Locales/ru.yml
  • src/Humanizer/Locales/si.yml
  • src/Humanizer/Locales/sk.yml
  • src/Humanizer/Locales/sl.yml
  • src/Humanizer/Locales/so.yml
  • src/Humanizer/Locales/sq.yml
  • src/Humanizer/Locales/sr-Latn.yml
  • src/Humanizer/Locales/sr.yml
  • src/Humanizer/Locales/sv.yml
  • src/Humanizer/Locales/sw.yml
  • src/Humanizer/Locales/ta.yml
  • src/Humanizer/Locales/te.yml
  • src/Humanizer/Locales/th.yml
  • src/Humanizer/Locales/tk.yml
  • src/Humanizer/Locales/tr.yml
  • src/Humanizer/Locales/uk.yml
  • src/Humanizer/Locales/ur.yml
  • src/Humanizer/Locales/uz-Cyrl-UZ.yml
  • src/Humanizer/Locales/uz-Latn-UZ.yml
  • src/Humanizer/Locales/vi.yml
  • src/Humanizer/Locales/yo.yml
  • src/Humanizer/Locales/zh-Hans.yml
  • src/Humanizer/Locales/zh-Hant.yml
  • src/Humanizer/Locales/zu-ZA.yml
  • src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
  • src/Humanizer/Localisation/Formatters/IGrammaticalCaseTimeSpanFormatter.cs
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs
  • src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs
  • src/Humanizer/TimeSpanHumanizeExtensions.cs
  • src/Humanizer/TimeSpanHumanizeStrategy/DefaultTimeSpanHumanizeStrategy.cs
  • src/Humanizer/TimeSpanHumanizeStrategy/IGrammaticalCaseTimeSpanHumanizeStrategy.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/CanonicalLocaleSchemaTests.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.CanonicalSchema.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet11_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt
  • tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/de/GermanDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/fi/FinnishDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/hu/HungarianDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/ml/MalayalamDurationCaseTests.cs
  • tests/Humanizer.Tests/TimeSpanHumanizeTests.cs
  • website/docs/api/Humanizer.DefaultFormatter.md
  • website/docs/api/Humanizer.DefaultTimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.GrammaticalCase.md
  • website/docs/api/Humanizer.IFormatter.md
  • website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md
  • website/docs/api/Humanizer.IGrammaticalCaseTimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.ITimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.TimeSpanHumanizeExtensions.md
  • website/docs/api/Humanizer.md
  • website/docs/contributing/locale-yaml-reference.mdx
  • website/docs/languages/custom-localization.mdx
  • website/docs/languages/grammar-and-platform-formatting.mdx
  • website/docs/languages/supported-cultures.mdx
  • website/docs/scenarios/dates-times-durations-and-age.mdx
  • website/docs/scenarios/durations-and-ages.mdx
  • website/docs/scenarios/localization-and-extensibility.mdx
  • website/scenario-api-contract.json

Comment thread src/Humanizer/Locales/cs.yml Outdated
Comment thread src/Humanizer/Locales/fi.yml Outdated
Comment thread src/Humanizer/Locales/fi.yml Outdated
Comment thread src/Humanizer/Locales/hr.yml Outdated
Comment thread src/Humanizer/Locales/hu.yml Outdated
Comment thread src/Humanizer/Locales/ta.yml Outdated
Comment thread src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
Comment thread src/Humanizer/TimeSpanHumanizeExtensions.cs Outdated
Comment thread website/docs/api/Humanizer.DefaultFormatter.md
Comment thread website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md
Copilot AI review requested due to automatic review settings July 29, 2026 12:12
@clairernovotny
clairernovotny force-pushed the codex/timespan-grammatical-cases-686 branch from a8944c7 to cf10904 Compare July 29, 2026 12:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

devskim found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
Comment thread src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
Comment thread src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (7)
tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs (1)

1123-1150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse CompleteCitationDurationPhrases here.

This inline phrasesText is a verbatim copy of the constant declared at Line 1177, minus the two-space indent difference.

♻️ Proposed refactor
         var catalog = HumanizerSourceGenerator.DurationCaseNormalization.ParseForTests(
             "zz",
             InvariantCitationDurationCases,
-            phrasesText:
-            """
-              duration:
-                millisecond:
-            ...
-            """);
+            phrasesText: CompleteCitationDurationPhrases);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs`
around lines 1123 - 1150, Replace the duplicated inline phrasesText YAML in the
affected test with the existing CompleteCitationDurationPhrases constant,
preserving the test’s behavior and assertion setup.
tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.cs (1)

2141-2169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent no-op if the marker text ever drifts.

The match is byte-exact on indentation and quote style. If canonicalization emits "not-applicable" or different indentation, evidence injection quietly does nothing and the resulting failure points at schema validation rather than the fixture helper. Consider making the miss explicit for fixtures that do declare durationCases.

♻️ Optional guard
             var marker = $"  durationCases:{lineEnding}    classification: 'not-applicable'{lineEnding}";
             if (!candidateText.Contains(marker, StringComparison.Ordinal))
             {
+                // No not-applicable durationCases block to enrich; fixtures that intentionally
+                // omit or customise the surface are left untouched.
                 return candidateText;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.cs`
around lines 2141 - 2169, Update AddDefaultDurationEvidence to detect when
enabled fixtures declare durationCases but the expected marker is not found, and
fail explicitly instead of silently returning the unchanged candidateText.
Preserve the current evidence replacement behavior when the marker matches and
the no-op behavior when the feature is disabled.
tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs (2)

273-274: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

"one " is a fragile English-fallback probe.

Any locale whose authored form legitimately contains the substring (romanized data, Latin-script variants) will trip this. Asserting the result differs from the English table's rendering for that unit/case would be more precise, though the current heuristic is acceptable for now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs`
around lines 273 - 274, Update the assertion in the locale test around the
non-English branch to compare the generated result with the English table’s
rendering for the same duration unit and case, rather than checking for the
fragile substring "one ". Preserve the case-insensitive locale handling and
ensure the assertion verifies that non-English output differs from the English
fallback.

25-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duration construction only handles the units currently in the data rows.

unit == TimeUnit.Week ? days*7 : FromDays(count) (and the Hour/Day variant at Line 67) silently produces the wrong TimeSpan if a Month/Year/Minute row is ever added. A small switch over TimeUnit would keep the theory extensible.

Also applies to: 67-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs`
around lines 25 - 27, Update the duration construction in both test cases around
the existing conditional expressions to use a switch over TimeUnit, mapping each
supported unit—including minute, hour, day, week, month, and year—to its
appropriate TimeSpan conversion. Preserve the existing count-based duration
behavior while ensuring newly added data rows cannot silently use an incorrect
fallback.
tests/Humanizer.Tests/TimeSpanHumanizeTests.cs (2)

700-748: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

These two tests differ only by the requested case.

NominativeCaseRejectsLegacyCustomStrategy and NonNominativeCaseRejectsLegacyCustomStrategy are otherwise identical; a [Theory] over GrammaticalCase.Nominative / GrammaticalCase.Dative removes ~25 duplicated lines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Humanizer.Tests/TimeSpanHumanizeTests.cs` around lines 700 - 748,
Combine NominativeCaseRejectsLegacyCustomStrategy and
NonNominativeCaseRejectsLegacyCustomStrategy into one parameterized theory with
GrammaticalCase.Nominative and GrammaticalCase.Dative as inputs. Preserve the
shared strategy setup, exception assertions, CallCount check, and configuration
restoration for both cases.

789-814: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use DistinctCultureSwap for the divergent-culture setup.

This test depends on CurrentCulture and CurrentUICulture differing, and it mutates/restores only the UI culture by hand. Based on learnings, prefer the DistinctCultureSwap helper (already used at Line 576 in this file) so both properties are set and restored together.

♻️ Proposed refactor
-        var originalUiCulture = CultureInfo.CurrentUICulture;
-        try
-        {
-            CultureInfo.CurrentUICulture = new("fr-FR");
-
-            var exception = Assert.Throws<NotSupportedException>(
+        using var _ = new Humanizer.Tests.Localisation.DistinctCultureSwap(new("en-US"), new("fr-FR"));
+
+        var exception = Assert.Throws<NotSupportedException>(
             () => TimeSpanHumanizeExtensions.FormatTimePart(
                 new LegacyFormatter(),
                 TimeUnit.Day,
                 1,
                 null,
                 false,
                 false,
                 GrammaticalCase.Dative));
-
-            Assert.Contains(CultureInfo.CurrentCulture.Name, exception.Message);
-            Assert.DoesNotContain(CultureInfo.CurrentUICulture.Name, exception.Message);
-        }
-        finally
-        {
-            CultureInfo.CurrentUICulture = originalUiCulture;
-        }
+
+        Assert.Contains(CultureInfo.CurrentCulture.Name, exception.Message);
+        Assert.DoesNotContain(CultureInfo.CurrentUICulture.Name, exception.Message);

Based on learnings: "when validating behavior that depends on divergent culture settings … prefer the DistinctCultureSwap helper to set distinct values for both."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Humanizer.Tests/TimeSpanHumanizeTests.cs` around lines 789 - 814,
Update UnsupportedFormatterReportsTheFormattingCulture to use the existing
DistinctCultureSwap helper for the divergent-culture setup, assigning distinct
cultures through the helper and disposing it for cleanup. Remove the manual
CurrentUICulture capture, mutation, and finally restoration while preserving the
assertions that the exception uses CurrentCulture rather than CurrentUICulture.

Source: Learnings

src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs (1)

374-387: 📐 Maintainability & Code Quality | 🔵 Trivial

Hardcoded zh-CN/zh-Hans script-compatibility exception.

HasCompatibleScript special-cases exactly one locale pair instead of a general rule for "no explicit script subtag ⇒ inherits the parent's default script." Any future locale needing the same accommodation (e.g., another zh-* region variant, or a similar pattern for another language) will require another hardcoded branch here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs` around lines 374
- 387, Generalize HasCompatibleScript so locales without explicit script subtags
are compatible when the locale’s language/region default script matches the
inherited locale’s effective script, rather than hardcoding the zh-CN/zh-Hans
pair. Reuse the catalog’s existing locale/script-resolution data or helper,
preserve exact-script comparison when both subtags exist, and retain
incompatibility for unresolved mismatches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Humanizer/Locales/cs.yml`:
- Around line 143-259: Correct the Czech plural values in the affected case
overlays for the authored unit entries, including millisecond, second, minute,
hour, day, week, month, and year. Replace singular or nominative-looking plural
forms with the proper case-specific forms, such as dative sekundám, instrumental
sekundami, locative sekundách, and genitive týdnů and měsíců. Apply the same
corrections across all indicated overlay sections while preserving values that
are already correct.

In `@src/Humanizer/Locales/kn.yml`:
- Around line 497-500: Update the Kannada duration configuration under
durationCases.vocative so it explicitly models the CLDR-supported vocative
unit/count combinations, including one-year and multiple-month durations, or
encode the partial coverage as an intentional unsupported state recognized by
the schema/runtime; do not leave notApplicable implying that all Kannada
durations are unsupported.

In `@src/Humanizer/Locales/lv.yml`:
- Around line 93-110: Add singular nominative forms to the multiple.forms
mappings for the month and year locale entries: set month’s singular form to
“mēnesis” and year’s to “gads”, while preserving their existing default forms.

In `@src/Humanizer/Locales/pl.yml`:
- Around line 157-252: Correct the dual form values for the Polish duration
entries under second, minute, hour, day, week, month, and year, including the
corresponding entries in the other affected duration blocks. Use the
grammatically correct Polish forms for each declared case, such as dative -om,
instrumental -ami, and locative -ach, rather than reusing singular, nominative,
or genitive text.

In `@src/Humanizer/Locales/ps.yml`:
- Around line 30-41: Rename one of the duplicate corpus-source keys in the
sources mapping, such as changing the UD Pashto Prince entry to treebank, and
update every matching provenance reference to use the new identifier while
preserving both source definitions.

In `@src/Humanizer/Locales/ro.yml`:
- Around line 46-53: Update the Romanian locale duration entries in the
`multiple.forms` definitions so each `dual` template uses a bare count and unit
without `de`, including the other genitive duration units. Keep the `plural`
templates unchanged so fallback plural counts continue using `de`.

In `@src/Humanizer/Locales/ru.yml`:
- Around line 148-264: Update the dual forms for every duration unit in the
dative, instrumental, prepositional, and locative sections, including the later
corresponding sections, to use the case-specific CLDR form rather than genitive
singular. Ensure each unit’s dual ending matches its case, such as dative
“{count} …м”, instrumental “{count} …ми”, and prepositional/locative “{count}
…х”, while leaving other plural forms unchanged.

In `@src/Humanizer/Locales/sk.yml`:
- Around line 152-157: Update the affected Slovak unit entries in the locale
plural-form overlays so each genitive, dative, instrumental, and locative plural
uses its case-specific form, including “sekúnd”, “sekundami”, and “sekundách”
where applicable. Leave already-correct values and nominative-delegated
accusative entries unchanged.

In `@src/Humanizer/Locales/sv.yml`:
- Around line 166-177: Update the Swedish day duration mapping in the day phrase
configuration to use the genitive of dag: set the singular numeric form to “1
dags” and every count-based form to “{count} dagars”, replacing the current
dygns values.

In
`@tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs`:
- Around line 1206-1210: Update ParseDurationCases to normalize raw literal line
endings before splitting, matching the approach used by
ReleaseDurationCasesRequireEveryReachablePluralForm, so every line receives the
intended indentation regardless of checkout platform. Also update
NonDistinctDurationClassificationsRejectCaseData to join lines with "\n" instead
of Environment.NewLine.

In `@website/docs/languages/supported-cultures.mdx`:
- Around line 73-77: Clarify the supported-cultures documentation near the
duration disposition guidance by distinguishing release-validation rejection of
Unsupported grammar entries from runtime coverage gaps that legitimately produce
NotSupportedException. State that Unsupported entries remain part of the
parser/normalizer representation, while runtime tables may intentionally lack
specific case or duration-unit support.

---

Nitpick comments:
In `@src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs`:
- Around line 374-387: Generalize HasCompatibleScript so locales without
explicit script subtags are compatible when the locale’s language/region default
script matches the inherited locale’s effective script, rather than hardcoding
the zh-CN/zh-Hans pair. Reuse the catalog’s existing locale/script-resolution
data or helper, preserve exact-script comparison when both subtags exist, and
retain incompatibility for unresolved mismatches.

In
`@tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.cs`:
- Around line 2141-2169: Update AddDefaultDurationEvidence to detect when
enabled fixtures declare durationCases but the expected marker is not found, and
fail explicitly instead of silently returning the unchanged candidateText.
Preserve the current evidence replacement behavior when the marker matches and
the no-op behavior when the feature is disabled.

In
`@tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs`:
- Around line 1123-1150: Replace the duplicated inline phrasesText YAML in the
affected test with the existing CompleteCitationDurationPhrases constant,
preserving the test’s behavior and assertion setup.

In
`@tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs`:
- Around line 273-274: Update the assertion in the locale test around the
non-English branch to compare the generated result with the English table’s
rendering for the same duration unit and case, rather than checking for the
fragile substring "one ". Preserve the case-insensitive locale handling and
ensure the assertion verifies that non-English output differs from the English
fallback.
- Around line 25-27: Update the duration construction in both test cases around
the existing conditional expressions to use a switch over TimeUnit, mapping each
supported unit—including minute, hour, day, week, month, and year—to its
appropriate TimeSpan conversion. Preserve the existing count-based duration
behavior while ensuring newly added data rows cannot silently use an incorrect
fallback.

In `@tests/Humanizer.Tests/TimeSpanHumanizeTests.cs`:
- Around line 700-748: Combine NominativeCaseRejectsLegacyCustomStrategy and
NonNominativeCaseRejectsLegacyCustomStrategy into one parameterized theory with
GrammaticalCase.Nominative and GrammaticalCase.Dative as inputs. Preserve the
shared strategy setup, exception assertions, CallCount check, and configuration
restoration for both cases.
- Around line 789-814: Update UnsupportedFormatterReportsTheFormattingCulture to
use the existing DistinctCultureSwap helper for the divergent-culture setup,
assigning distinct cultures through the helper and disposing it for cleanup.
Remove the manual CurrentUICulture capture, mutation, and finally restoration
while preserving the assertions that the exception uses CurrentCulture rather
than CurrentUICulture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 62d30703-cb9f-4ef5-aa66-6cf74b0ddf8b

📥 Commits

Reviewing files that changed from the base of the PR and between a8944c7 and cf10904.

📒 Files selected for processing (143)
  • readme.md
  • src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs
  • src/Humanizer.SourceGenerators/Common/DurationCaseModels.cs
  • src/Humanizer.SourceGenerators/Common/LocalePhraseModels.cs
  • src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs
  • src/Humanizer.SourceGenerators/Common/LocaleYamlCatalog.cs
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/FormatterProfileCatalogInput.cs
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocalePhraseTableCatalogInput.cs
  • src/Humanizer.SourceGenerators/HumanizerSourceGenerator.cs
  • src/Humanizer/GrammaticalCase.cs
  • src/Humanizer/Locales/af.yml
  • src/Humanizer/Locales/am.yml
  • src/Humanizer/Locales/ar.yml
  • src/Humanizer/Locales/as.yml
  • src/Humanizer/Locales/az.yml
  • src/Humanizer/Locales/be.yml
  • src/Humanizer/Locales/bg.yml
  • src/Humanizer/Locales/bn.yml
  • src/Humanizer/Locales/bs.yml
  • src/Humanizer/Locales/ca.yml
  • src/Humanizer/Locales/cs.yml
  • src/Humanizer/Locales/cy.yml
  • src/Humanizer/Locales/da.yml
  • src/Humanizer/Locales/de.yml
  • src/Humanizer/Locales/el.yml
  • src/Humanizer/Locales/en.yml
  • src/Humanizer/Locales/es.yml
  • src/Humanizer/Locales/et.yml
  • src/Humanizer/Locales/eu.yml
  • src/Humanizer/Locales/fa.yml
  • src/Humanizer/Locales/fi.yml
  • src/Humanizer/Locales/fil.yml
  • src/Humanizer/Locales/fr.yml
  • src/Humanizer/Locales/ga.yml
  • src/Humanizer/Locales/gl.yml
  • src/Humanizer/Locales/gu.yml
  • src/Humanizer/Locales/ha.yml
  • src/Humanizer/Locales/he.yml
  • src/Humanizer/Locales/hi.yml
  • src/Humanizer/Locales/hr.yml
  • src/Humanizer/Locales/hu.yml
  • src/Humanizer/Locales/hy.yml
  • src/Humanizer/Locales/id.yml
  • src/Humanizer/Locales/ig.yml
  • src/Humanizer/Locales/is.yml
  • src/Humanizer/Locales/it.yml
  • src/Humanizer/Locales/ja.yml
  • src/Humanizer/Locales/ka.yml
  • src/Humanizer/Locales/kk.yml
  • src/Humanizer/Locales/km.yml
  • src/Humanizer/Locales/kn.yml
  • src/Humanizer/Locales/ko.yml
  • src/Humanizer/Locales/kok.yml
  • src/Humanizer/Locales/ku.yml
  • src/Humanizer/Locales/ky.yml
  • src/Humanizer/Locales/lb.yml
  • src/Humanizer/Locales/lo.yml
  • src/Humanizer/Locales/lt.yml
  • src/Humanizer/Locales/lv.yml
  • src/Humanizer/Locales/mk.yml
  • src/Humanizer/Locales/ml.yml
  • src/Humanizer/Locales/mn.yml
  • src/Humanizer/Locales/mr.yml
  • src/Humanizer/Locales/ms.yml
  • src/Humanizer/Locales/mt.yml
  • src/Humanizer/Locales/my.yml
  • src/Humanizer/Locales/nb.yml
  • src/Humanizer/Locales/ne.yml
  • src/Humanizer/Locales/nl.yml
  • src/Humanizer/Locales/nn.yml
  • src/Humanizer/Locales/or.yml
  • src/Humanizer/Locales/pa-Arab.yml
  • src/Humanizer/Locales/pa.yml
  • src/Humanizer/Locales/pl.yml
  • src/Humanizer/Locales/ps.yml
  • src/Humanizer/Locales/pt.yml
  • src/Humanizer/Locales/ro.yml
  • src/Humanizer/Locales/ru.yml
  • src/Humanizer/Locales/si.yml
  • src/Humanizer/Locales/sk.yml
  • src/Humanizer/Locales/sl.yml
  • src/Humanizer/Locales/so.yml
  • src/Humanizer/Locales/sq.yml
  • src/Humanizer/Locales/sr-Latn.yml
  • src/Humanizer/Locales/sr.yml
  • src/Humanizer/Locales/sv.yml
  • src/Humanizer/Locales/sw.yml
  • src/Humanizer/Locales/ta.yml
  • src/Humanizer/Locales/te.yml
  • src/Humanizer/Locales/th.yml
  • src/Humanizer/Locales/tk.yml
  • src/Humanizer/Locales/tr.yml
  • src/Humanizer/Locales/uk.yml
  • src/Humanizer/Locales/ur.yml
  • src/Humanizer/Locales/uz-Cyrl-UZ.yml
  • src/Humanizer/Locales/uz-Latn-UZ.yml
  • src/Humanizer/Locales/vi.yml
  • src/Humanizer/Locales/yo.yml
  • src/Humanizer/Locales/zh-Hans.yml
  • src/Humanizer/Locales/zh-Hant.yml
  • src/Humanizer/Locales/zu-ZA.yml
  • src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
  • src/Humanizer/Localisation/Formatters/IGrammaticalCaseTimeSpanFormatter.cs
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs
  • src/Humanizer/Localisation/Formatters/LocalePhraseTable.cs
  • src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs
  • src/Humanizer/TimeSpanHumanizeExtensions.cs
  • src/Humanizer/TimeSpanHumanizeStrategy/DefaultTimeSpanHumanizeStrategy.cs
  • src/Humanizer/TimeSpanHumanizeStrategy/IGrammaticalCaseTimeSpanHumanizeStrategy.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/CanonicalLocaleSchemaTests.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.CanonicalSchema.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/LocalePhraseNormalizationTests.cs
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet11_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt
  • tests/Humanizer.Tests/CoverageGapTests.cs
  • tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/de/GermanDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/fi/FinnishDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/hu/HungarianDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/ml/MalayalamDurationCaseTests.cs
  • tests/Humanizer.Tests/TimeSpanHumanizeTests.cs
  • tools/docs/generate-language-coverage.ps1
  • website/docs/api/Humanizer.DefaultFormatter.md
  • website/docs/api/Humanizer.DefaultTimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.GrammaticalCase.md
  • website/docs/api/Humanizer.IFormatter.md
  • website/docs/api/Humanizer.IGrammaticalCaseTimeSpanFormatter.md
  • website/docs/api/Humanizer.IGrammaticalCaseTimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.ITimeSpanHumanizeStrategy.md
  • website/docs/api/Humanizer.TimeSpanHumanizeExtensions.md
  • website/docs/api/Humanizer.md
  • website/docs/contributing/locale-yaml-reference.mdx
  • website/docs/languages/custom-localization.mdx
  • website/docs/languages/grammar-and-platform-formatting.mdx
  • website/docs/languages/supported-cultures.mdx
  • website/docs/scenarios/dates-times-durations-and-age.mdx
  • website/docs/scenarios/durations-and-ages.mdx
  • website/docs/scenarios/localization-and-extensibility.mdx
  • website/scenario-api-contract.json
🚧 Files skipped from review as they are similar to previous changes (23)
  • tests/Humanizer.Tests/Localisation/hu/HungarianDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/fi/FinnishDurationCaseTests.cs
  • tests/Humanizer.Tests/Localisation/de/GermanDurationCaseTests.cs
  • website/scenario-api-contract.json
  • src/Humanizer/Localisation/Formatters/IGrammaticalCaseTimeSpanFormatter.cs
  • website/docs/api/Humanizer.md
  • src/Humanizer/TimeSpanHumanizeStrategy/IGrammaticalCaseTimeSpanHumanizeStrategy.cs
  • website/docs/scenarios/dates-times-durations-and-age.mdx
  • tests/Humanizer.Tests/Localisation/ml/MalayalamDurationCaseTests.cs
  • readme.md
  • website/docs/api/Humanizer.ITimeSpanHumanizeStrategy.md
  • website/docs/languages/grammar-and-platform-formatting.mdx
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs
  • src/Humanizer.SourceGenerators/HumanizerSourceGenerator.cs
  • src/Humanizer/GrammaticalCase.cs
  • src/Humanizer.SourceGenerators/Common/CanonicalLocaleAuthoring.cs
  • website/docs/scenarios/durations-and-ages.mdx
  • website/docs/scenarios/localization-and-extensibility.mdx
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.Net4_8.verified.txt
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/CanonicalLocaleSchemaTests.cs
  • tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt
  • src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
  • src/Humanizer/TimeSpanHumanizeExtensions.cs

Comment thread src/Humanizer/Locales/cs.yml
Comment thread src/Humanizer/Locales/kn.yml
Comment thread src/Humanizer/Locales/lv.yml
Comment thread src/Humanizer/Locales/pl.yml
Comment thread src/Humanizer/Locales/ps.yml
Comment thread src/Humanizer/Locales/ru.yml
Comment thread src/Humanizer/Locales/sk.yml
Comment thread src/Humanizer/Locales/sv.yml
Comment thread website/docs/languages/supported-cultures.mdx
Copilot AI review requested due to automatic review settings July 29, 2026 13:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Humanizer/Locales/ro.yml (1)

37-37: 🎯 Functional Correctness | 🟠 Major

Use bare Romanian dual genitive forms.

The dual templates still render 2 de <unit> (for example, 2 de secunde). Use {count} <unit> for the dual form across all units, while retaining de in the fallback plural/default forms.

Also applies to: 51-51, 65-65, 79-79, 93-93, 107-107, 121-121, 135-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/Locales/ro.yml` at line 37, Update the Romanian locale’s dual
templates for all affected time units to use the bare form “{count} <unit>”
without “de”; retain “de” in the fallback plural/default templates.
♻️ Duplicate comments (1)
src/Humanizer/Locales/ru.yml (1)

148-264: 🎯 Functional Correctness | 🟠 Major

Use case-specific Russian dual forms.

The dual templates in the dative, instrumental, prepositional, and locative sections still use genitive-singular forms. Replace them with the corresponding case forms for every unit—for example, {count} дням, {count} днями, and {count} днях—without changing the other plural forms.

Also applies to: 342-458, 459-575, 576-692

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Humanizer/Locales/ru.yml` around lines 148 - 264, Update the dual
templates under the Russian locale’s dative, instrumental, prepositional, and
locative sections for every time unit so they use the corresponding
case-specific dual forms rather than genitive-singular forms. For example, use
dative “{count} дням,” instrumental “{count} днями,” and prepositional/locative
“{count} днях,” while preserving all existing single, default, singular, paucal,
and plural templates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Humanizer/Locales/ro.yml`:
- Line 37: Update the Romanian locale’s dual templates for all affected time
units to use the bare form “{count} <unit>” without “de”; retain “de” in the
fallback plural/default templates.

---

Duplicate comments:
In `@src/Humanizer/Locales/ru.yml`:
- Around line 148-264: Update the dual templates under the Russian locale’s
dative, instrumental, prepositional, and locative sections for every time unit
so they use the corresponding case-specific dual forms rather than
genitive-singular forms. For example, use dative “{count} дням,” instrumental
“{count} днями,” and prepositional/locative “{count} днях,” while preserving all
existing single, default, singular, paucal, and plural templates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24e6a394-01cc-4119-852a-374c80117cdf

📥 Commits

Reviewing files that changed from the base of the PR and between cf10904 and 9f3a01c.

📒 Files selected for processing (90)
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs
  • src/Humanizer/Locales/af.yml
  • src/Humanizer/Locales/am.yml
  • src/Humanizer/Locales/ar.yml
  • src/Humanizer/Locales/as.yml
  • src/Humanizer/Locales/be.yml
  • src/Humanizer/Locales/bg.yml
  • src/Humanizer/Locales/bn.yml
  • src/Humanizer/Locales/ca.yml
  • src/Humanizer/Locales/cs.yml
  • src/Humanizer/Locales/cy.yml
  • src/Humanizer/Locales/da.yml
  • src/Humanizer/Locales/de.yml
  • src/Humanizer/Locales/el.yml
  • src/Humanizer/Locales/en.yml
  • src/Humanizer/Locales/es.yml
  • src/Humanizer/Locales/et.yml
  • src/Humanizer/Locales/eu.yml
  • src/Humanizer/Locales/fa.yml
  • src/Humanizer/Locales/fi.yml
  • src/Humanizer/Locales/fil.yml
  • src/Humanizer/Locales/fr.yml
  • src/Humanizer/Locales/ga.yml
  • src/Humanizer/Locales/gl.yml
  • src/Humanizer/Locales/gu.yml
  • src/Humanizer/Locales/ha.yml
  • src/Humanizer/Locales/he.yml
  • src/Humanizer/Locales/hi.yml
  • src/Humanizer/Locales/hr.yml
  • src/Humanizer/Locales/hu.yml
  • src/Humanizer/Locales/hy.yml
  • src/Humanizer/Locales/id.yml
  • src/Humanizer/Locales/ig.yml
  • src/Humanizer/Locales/is.yml
  • src/Humanizer/Locales/it.yml
  • src/Humanizer/Locales/ja.yml
  • src/Humanizer/Locales/ka.yml
  • src/Humanizer/Locales/km.yml
  • src/Humanizer/Locales/kn.yml
  • src/Humanizer/Locales/ko.yml
  • src/Humanizer/Locales/kok.yml
  • src/Humanizer/Locales/ku.yml
  • src/Humanizer/Locales/lb.yml
  • src/Humanizer/Locales/lo.yml
  • src/Humanizer/Locales/lt.yml
  • src/Humanizer/Locales/lv.yml
  • src/Humanizer/Locales/mk.yml
  • src/Humanizer/Locales/ml.yml
  • src/Humanizer/Locales/mr.yml
  • src/Humanizer/Locales/ms.yml
  • src/Humanizer/Locales/mt.yml
  • src/Humanizer/Locales/my.yml
  • src/Humanizer/Locales/nb.yml
  • src/Humanizer/Locales/ne.yml
  • src/Humanizer/Locales/nl.yml
  • src/Humanizer/Locales/nn.yml
  • src/Humanizer/Locales/pa-Arab.yml
  • src/Humanizer/Locales/pa.yml
  • src/Humanizer/Locales/pl.yml
  • src/Humanizer/Locales/ps.yml
  • src/Humanizer/Locales/pt.yml
  • src/Humanizer/Locales/ro.yml
  • src/Humanizer/Locales/ru.yml
  • src/Humanizer/Locales/sk.yml
  • src/Humanizer/Locales/sl.yml
  • src/Humanizer/Locales/sq.yml
  • src/Humanizer/Locales/sr-Latn.yml
  • src/Humanizer/Locales/sr.yml
  • src/Humanizer/Locales/sv.yml
  • src/Humanizer/Locales/sw.yml
  • src/Humanizer/Locales/ta.yml
  • src/Humanizer/Locales/te.yml
  • src/Humanizer/Locales/th.yml
  • src/Humanizer/Locales/tr.yml
  • src/Humanizer/Locales/uk.yml
  • src/Humanizer/Locales/ur.yml
  • src/Humanizer/Locales/vi.yml
  • src/Humanizer/Locales/yo.yml
  • src/Humanizer/Locales/zh-Hans.yml
  • src/Humanizer/Locales/zh-Hant.yml
  • src/Humanizer/Locales/zu-ZA.yml
  • src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs
  • src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs
  • src/Humanizer/TimeSpanHumanizeExtensions.cs
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.CanonicalSchema.cs
  • tests/Humanizer.Tests/Localisation/GeneratedLocaleData/CldrDurationCaseTests.cs
  • tests/Humanizer.Tests/TimeSpanHumanizeTests.cs
  • website/docs/contributing/locale-yaml-reference.mdx
  • website/docs/scenarios/durations-and-ages.mdx
🚧 Files skipped from review as they are similar to previous changes (75)
  • src/Humanizer/Locales/ms.yml
  • src/Humanizer/Locales/ko.yml
  • src/Humanizer/Locales/gl.yml
  • src/Humanizer/Locales/he.yml
  • src/Humanizer/Locales/fr.yml
  • src/Humanizer/Locales/es.yml
  • src/Humanizer/Locales/sq.yml
  • src/Humanizer/Locales/zh-Hans.yml
  • src/Humanizer/Locales/zu-ZA.yml
  • src/Humanizer/Locales/ha.yml
  • src/Humanizer/Locales/ku.yml
  • src/Humanizer/Locales/mt.yml
  • src/Humanizer/Locales/cy.yml
  • src/Humanizer/Locales/ja.yml
  • src/Humanizer/Locales/id.yml
  • src/Humanizer/Locales/en.yml
  • src/Humanizer/Locales/fa.yml
  • src/Humanizer/Locales/am.yml
  • src/Humanizer/Locales/bg.yml
  • src/Humanizer/Locales/el.yml
  • src/Humanizer/Locales/lo.yml
  • src/Humanizer/Locales/ne.yml
  • src/Humanizer/Locales/zh-Hant.yml
  • src/Humanizer/Locales/as.yml
  • src/Humanizer/Locales/ca.yml
  • src/Humanizer/Locales/vi.yml
  • src/Humanizer/Locales/yo.yml
  • src/Humanizer/Locales/sw.yml
  • src/Humanizer/Locales/my.yml
  • src/Humanizer/Locales/km.yml
  • src/Humanizer/Localisation/Formatters/LocaleDurationCaseTable.cs
  • src/Humanizer/Locales/pt.yml
  • src/Humanizer/Locales/mk.yml
  • src/Humanizer/Locales/te.yml
  • src/Humanizer/Locales/hy.yml
  • src/Humanizer/Locales/nb.yml
  • src/Humanizer/Locales/th.yml
  • src/Humanizer/Locales/af.yml
  • src/Humanizer/Locales/sv.yml
  • src/Humanizer/Locales/nl.yml
  • src/Humanizer/Locales/sl.yml
  • src/Humanizer/Locales/sr-Latn.yml
  • src/Humanizer/Locales/lt.yml
  • src/Humanizer/Locales/cs.yml
  • src/Humanizer/Locales/sk.yml
  • src/Humanizer/Locales/ga.yml
  • src/Humanizer/Locales/da.yml
  • src/Humanizer/Locales/lv.yml
  • src/Humanizer/Locales/fi.yml
  • src/Humanizer/Locales/pa.yml
  • src/Humanizer/Locales/hr.yml
  • src/Humanizer/Locales/hi.yml
  • src/Humanizer/Locales/mr.yml
  • src/Humanizer/Locales/ta.yml
  • src/Humanizer/Locales/lb.yml
  • src/Humanizer/Locales/ar.yml
  • src/Humanizer/Locales/fil.yml
  • src/Humanizer/Locales/gu.yml
  • src/Humanizer/Locales/ur.yml
  • website/docs/scenarios/durations-and-ages.mdx
  • src/Humanizer/Locales/bn.yml
  • src/Humanizer/Locales/et.yml
  • src/Humanizer/Locales/ps.yml
  • src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs
  • src/Humanizer/Locales/pa-Arab.yml
  • src/Humanizer/Localisation/Formatters/DefaultFormatter.cs
  • website/docs/contributing/locale-yaml-reference.mdx
  • src/Humanizer/Locales/ml.yml
  • src/Humanizer/Locales/eu.yml
  • tests/Humanizer.SourceGenerators.Tests/SourceGenerators/HumanizerSourceGeneratorTests.CanonicalSchema.cs
  • src/Humanizer/Locales/uk.yml
  • src/Humanizer/TimeSpanHumanizeExtensions.cs
  • src/Humanizer/Locales/kn.yml
  • src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/LocaleDurationCaseTableCatalogInput.cs
  • tests/Humanizer.Tests/TimeSpanHumanizeTests.cs

Copilot AI review requested due to automatic review settings July 29, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Comment thread src/Humanizer.SourceGenerators/Common/LocalePhraseNormalization.cs Fixed
Copilot AI review requested due to automatic review settings July 29, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings July 29, 2026 15:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Copilot AI review requested due to automatic review settings July 29, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: df4e9ac643

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@clairernovotny
clairernovotny merged commit 6da3216 into main Jul 29, 2026
17 checks passed
@clairernovotny
clairernovotny deleted the codex/timespan-grammatical-cases-686 branch July 29, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Adds or improves user-facing functionality localisation Changes translations, locale data, or culture-specific behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grammatical Cases for TimeSpan

3 participants