-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Better pluralization for data table filters #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import assert from 'node:assert'; | ||
| import { describe, it } from 'node:test'; | ||
| import { pluralize } from './i18n'; | ||
|
|
||
| describe('pluralize', () => { | ||
| it('pluralizes English words correctly', () => { | ||
| assert.strictEqual(pluralize('box', 'en'), 'boxes'); | ||
| assert.strictEqual(pluralize('category', 'en'), 'categories'); | ||
| assert.strictEqual(pluralize('dog', 'en'), 'dogs'); | ||
| assert.strictEqual(pluralize('watch', 'en'), 'watches'); | ||
| assert.strictEqual(pluralize('bus', 'en'), 'buses'); | ||
| assert.strictEqual(pluralize('wish', 'en'), 'wishes'); | ||
| // Simple words | ||
| assert.strictEqual(pluralize('column', 'en'), 'columns'); | ||
| }); | ||
|
|
||
| it('does not pluralize for languages with no plural form', () => { | ||
| assert.strictEqual(pluralize('数据', 'zh'), '数据'); // Chinese | ||
| assert.strictEqual(pluralize('データ', 'ja'), 'データ'); // Japanese | ||
| }); | ||
|
|
||
| it('falls back to appending s for other languages', () => { | ||
| // Spanish has plurals. "dato" -> "datos". | ||
| assert.strictEqual(pluralize('dato', 'es'), 'datos'); | ||
|
|
||
| // "papel" -> "papeles" in proper Spanish, but our fallback is naive | ||
| // So we expect "papels" because we only improved 'en'. | ||
| // If we passed 'en' it would handle 'papel' -> 'papels' anyway unless it hit sibilant? | ||
| // 'papel' ends in 'l', so 's'. | ||
| assert.strictEqual(pluralize('papel', 'es'), 'papels'); | ||
| }); | ||
|
|
||
| it('handles defaults when locale is missing', () => { | ||
| // @ts-ignore | ||
| assert.strictEqual(pluralize('cat', undefined), 'cats'); | ||
| }); | ||
|
|
||
| it('handles y endings correctly for English', () => { | ||
| assert.strictEqual(pluralize('boy', 'en'), 'boys'); // Vowel + y -> s | ||
| assert.strictEqual(pluralize('day', 'en'), 'days'); | ||
| assert.strictEqual(pluralize('city', 'en'), 'cities'); // Consonant + y -> ies | ||
| }); | ||
|
Comment on lines
+38
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a test for multi-word display names to catch the anchoring bug. The it('handles multi-word display names', () => {
assert.strictEqual(pluralize('payment category', 'en'), 'payment categories');
assert.strictEqual(pluralize('key display', 'en'), 'key displays');
});🤖 Prompt for AI Agents |
||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,48 @@ | ||||||||||||||||||
| import en from '../locales/en.json'; | ||||||||||||||||||
|
|
||||||||||||||||||
| export type Locale = 'en'; | ||||||||||||||||||
| export type Locale = 'en' | string; | ||||||||||||||||||
|
|
||||||||||||||||||
| type Translations = Record<string, string>; | ||||||||||||||||||
|
|
||||||||||||||||||
| const translations: Record<Locale, Translations> = { | ||||||||||||||||||
| const translations: Record<string, Translations> = { | ||||||||||||||||||
| en, | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| export function t(key: string, locale: Locale): string { | ||||||||||||||||||
| return translations[locale][key] ?? key; | ||||||||||||||||||
| return translations[locale]?.[key] ?? translations.en[key] ?? key; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export function pluralize(text: string, locale: Locale): string { | ||||||||||||||||||
| // If no locale provided, default to english behavior | ||||||||||||||||||
| const loc = locale || 'en'; | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| const rules = new Intl.PluralRules(loc); | ||||||||||||||||||
| const options = rules.resolvedOptions(); | ||||||||||||||||||
|
|
||||||||||||||||||
| // If the language has only one category (e.g. 'other'), it doesn't distinguish plural forms (like Japanese, Chinese) | ||||||||||||||||||
| if (options.pluralCategories.length === 1) { | ||||||||||||||||||
| return text; | ||||||||||||||||||
| } | ||||||||||||||||||
| } catch (e) { | ||||||||||||||||||
| // Fallback if Intl.PluralRules fails or locale is invalid | ||||||||||||||||||
| console.warn(`[i18n] Invalid locale for PluralRules: ${loc}`, e); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Improved English pluralization | ||||||||||||||||||
| if (loc.startsWith('en')) { | ||||||||||||||||||
| // Handle 'y' ending (e.g. category -> categories, but boy -> boys) | ||||||||||||||||||
| if (text.endsWith('y') && !/[aeiou]y/i.test(text)) { | ||||||||||||||||||
| return `${text.slice(0, -1)}ies`; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+34
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: unanchored regex causes incorrect pluralization for multi-word display names. The regex Anchor the regex to the end of the string: 🐛 Proposed fix- if (text.endsWith('y') && !/[aeiou]y/i.test(text)) {
+ if (text.endsWith('y') && !/[aeiou]y$/i.test(text)) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // Handle sibilants (e.g. box -> boxes, bus -> buses, watch -> watches) | ||||||||||||||||||
| if (/[sxz]$/i.test(text) || /[cs]h$/i.test(text)) { | ||||||||||||||||||
| return `${text}es`; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Default: append 's' | ||||||||||||||||||
| // This matches the original behavior but is now safer for non-plural languages (checked above) | ||||||||||||||||||
| return `${text}s`; | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: lambda-curry/forms
Length of output: 1377
🏁 Script executed:
Repository: lambda-curry/forms
Length of output: 741
🏁 Script executed:
Repository: lambda-curry/forms
Length of output: 2286
🏁 Script executed:
Repository: lambda-curry/forms
Length of output: 1341
Remove this test file or add a test runner to packages/components.
The file uses
node:testwhich is not configured in the project. The project has no test infrastructure inpackages/components— there's no test script or test runner configured in itspackage.json. When the rootyarn testcommand runs, turbo's test task has no test command to execute in this package. Additionally, the project's other test files (inapps/docs) use@testing-library/react, notnode:test. Either remove this orphaned test file or add proper test infrastructure (vitest, Jest, or explicit node:test setup) topackages/components.🤖 Prompt for AI Agents