Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@react-router/dev": "^7.0.0",
"@react-router/node": "^7.0.0",
"@types/glob": "^8.1.0",
"@types/node": "^25.2.1",
"@types/react": "^19.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
import { useDebounceCallback } from '../hooks/use-debounce-callback';
import { take } from '../lib/array';
import { createNumberRange } from '../lib/helpers';
import { type Locale, t } from '../lib/i18n';
import { type Locale, pluralize, t } from '../lib/i18n';

interface FilterValueProps<TData, TType extends ColumnDataType> {
filter: FilterModel<TType>;
Expand Down Expand Up @@ -137,7 +137,7 @@ export function FilterValueOptionDisplay<TData>({
filter,
column,
actions,
locale: _locale = 'en',
locale = 'en',
}: FilterValueDisplayProps<TData, 'option'>) {
const options = useMemo(() => column.getOptions(), [column]);
const selected = options.filter((o) => filter?.values.includes(o.value));
Expand All @@ -160,8 +160,7 @@ export function FilterValueOptionDisplay<TData>({
);
}
const name = column.displayName.toLowerCase();
// TODO: Better pluralization for different languages
const pluralName = name.endsWith('s') ? `${name}es` : `${name}s`;
const pluralName = column.pluralDisplayName ?? pluralize(name, locale);

const hasOptionIcons = !options?.some((o) => !o.icon);

Expand All @@ -183,7 +182,7 @@ export function FilterValueMultiOptionDisplay<TData>({
filter,
column,
actions,
locale: _locale = 'en',
locale = 'en',
}: FilterValueDisplayProps<TData, 'multiOption'>) {
const options = useMemo(() => column.getOptions(), [column]);
const selected = options.filter((o) => filter.values.includes(o.value));
Expand All @@ -201,6 +200,7 @@ export function FilterValueMultiOptionDisplay<TData>({
}

const name = column.displayName.toLowerCase();
const pluralName = column.pluralDisplayName ?? pluralize(name, locale);

const hasOptionIcons = !options?.some((o) => !o.icon);

Expand All @@ -215,7 +215,7 @@ export function FilterValueMultiOptionDisplay<TData>({
</div>
)}
<span>
{selected.length} {name}
{selected.length} {pluralName}
</span>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/ui/data-table-filter/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export type ColumnConfig<TData, TType extends ColumnDataType = any, TVal = unkno
id: TId;
accessor: TAccessorFn<TData, TVal>;
displayName: string;
pluralDisplayName?: string;
icon: ReactElementType;
type: TType;
options?: TType extends OptionBasedColumnDataType ? ColumnOption[] : never;
Expand Down
43 changes: 43 additions & 0 deletions packages/components/src/ui/data-table-filter/lib/i18n.test.ts
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';
Comment on lines +1 to +2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what test runner / framework is used in the project
echo "=== package.json test scripts ==="
fd 'package.json' --max-depth 3 --exec grep -l "test" {} \; | head -5 | xargs -I{} sh -c 'echo "--- {} ---" && jq ".scripts" {}'

echo ""
echo "=== vitest or jest config files ==="
fd -e config.ts -e config.js -e config.mjs | grep -iE "vitest|jest" | head -10

echo ""
echo "=== Other test files for comparison ==="
fd '.test.' packages/components/src --type f | head -10 | xargs -I{} sh -c 'echo "--- {} ---" && head -5 {}'

Repository: lambda-curry/forms

Length of output: 1377


🏁 Script executed:

#!/bin/bash
# More targeted search for test configuration

echo "=== packages/components/package.json ==="
cat packages/components/package.json | jq ".scripts" 2>/dev/null || cat packages/components/package.json

echo ""
echo "=== turbo.json test task configuration ==="
if [ -f turbo.json ]; then
  jq '.tasks.test' turbo.json
fi

echo ""
echo "=== Search for ALL test files in the repo ==="
fd '\.test\.|\.spec\.' --type f | head -20

echo ""
echo "=== Look for vitest/jest in root and nested package.json files ==="
rg '"vitest"|"jest"' package.json --type json -A 1 -B 1 | head -30

echo ""
echo "=== Check if node:test is explicitly configured ==="
rg 'node:test|@node/test-runner' --type json

Repository: lambda-curry/forms

Length of output: 741


🏁 Script executed:

#!/bin/bash
# Check what test runner is used in apps/docs (the other test files)

echo "=== apps/docs/package.json test script ==="
cat apps/docs/package.json | jq ".scripts"

echo ""
echo "=== First 10 lines of each apps/docs test file ==="
for file in apps/docs/src/remix-hook-form/form-error.test.tsx apps/docs/src/remix-hook-form/phone-input.test.tsx apps/docs/src/remix-hook-form/select.test.tsx; do
  echo "--- $file ---"
  head -10 "$file"
  echo ""
done

echo ""
echo "=== Check root package.json devDependencies for test runners ==="
jq ".devDependencies | keys | .[] | select(. | contains(\"vitest\") or contains(\"jest\") or contains(\"test\"))" package.json

Repository: lambda-curry/forms

Length of output: 2286


🏁 Script executed:

#!/bin/bash
# Check full devDependencies to see all test runners available

echo "=== Full devDependencies from root package.json ==="
jq ".devDependencies" package.json | grep -E "vitest|jest|test" | head -20

echo ""
echo "=== All devDependencies (full list) ==="
jq ".devDependencies | keys" package.json

echo ""
echo "=== Check if there are workspace-level test configs ==="
fd 'vitest.config|jest.config' --type f

echo ""
echo "=== Examine turbo.json more fully for test task details ==="
cat turbo.json | jq '.'

Repository: lambda-curry/forms

Length of output: 1341


Remove this test file or add a test runner to packages/components.

The file uses node:test which is not configured in the project. The project has no test infrastructure in packages/components — there's no test script or test runner configured in its package.json. When the root yarn test command runs, turbo's test task has no test command to execute in this package. Additionally, the project's other test files (in apps/docs) use @testing-library/react, not node:test. Either remove this orphaned test file or add proper test infrastructure (vitest, Jest, or explicit node:test setup) to packages/components.

🤖 Prompt for AI Agents
In `@packages/components/src/ui/data-table-filter/lib/i18n.test.ts` around lines 1
- 2, The orphaned test imports node:test (symbols: describe, it) and assert but
there is no test runner configured for packages/components; either remove this
test file entirely or wire up a test runner: add a test script and devDependency
(e.g., vitest or jest) to packages/components package.json, install and
configure the runner, and update the test to use the chosen runner (or convert
it to the project’s existing testing style like `@testing-library/react`) so the
imports describe/it and assert run under the configured test command.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a test for multi-word display names to catch the anchoring bug.

The y-ending tests only cover single words. In practice, pluralize receives full displayName values like "payment category". Adding a multi-word test would expose the unanchored-regex bug flagged in i18n.ts:

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
In `@packages/components/src/ui/data-table-filter/lib/i18n.test.ts` around lines
38 - 42, Add tests in i18n.test.ts that pass multi-word display names into
pluralize (e.g., "payment category", "key display") to ensure pluralization
applies to the last word only; this will catch the unanchored-regex bug in
pluralize (referencing the pluralize function in i18n.ts) — add assertions like
pluralize('payment category','en') === 'payment categories' and pluralize('key
display','en') === 'key displays'.

});
41 changes: 38 additions & 3 deletions packages/components/src/ui/data-table-filter/lib/i18n.ts
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: unanchored regex causes incorrect pluralization for multi-word display names.

The regex /[aeiou]y/i matches a vowel-y pair anywhere in the string, not just at the end. Since pluralize receives full display names (e.g., "payment category" from displayName.toLowerCase()), the "ay" in "payment" triggers a false positive, skipping the -ies rule and producing "payment categorys" instead of "payment categories".

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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`;
}
// 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`;
}
🤖 Prompt for AI Agents
In `@packages/components/src/ui/data-table-filter/lib/i18n.ts` around lines 34 -
37, The pluralization logic in pluralize (in i18n.ts) incorrectly uses
/[aeiou]y/i which matches a vowel+y anywhere in multi-word strings; update the
check to anchor the regex to the end (e.g. use /[aeiou]y$/i) so it only detects
a vowel immediately before the final 'y' and correctly turns "payment category"
into "payment categories".


// 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`;
}
2 changes: 1 addition & 1 deletion packages/components/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"strict": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"types": ["@react-router/node", "vite/client"],
"types": ["@react-router/node", "vite/client", "node"],
"rootDirs": [".", "./.react-router/types"]
},
"include": ["src", ".react-router/types/**/*"],
Expand Down
3 changes: 3 additions & 0 deletions packages/components/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export default defineConfig({
.sync('src/**/*.{ts,tsx}', {
ignore: [
'src/**/*.d.ts',
'src/**/*.test.ts',
'src/**/*.test.tsx',
'src/**/*.stories.tsx',
'src/**/core/types.ts', // Exclude type-only files to avoid empty chunks
],
})
Expand Down
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,7 @@ __metadata:
"@react-router/node": "npm:^7.0.0"
"@tanstack/react-table": "npm:^8.21.2"
"@types/glob": "npm:^8.1.0"
"@types/node": "npm:^25.2.1"
"@types/react": "npm:^19.0.0"
"@typescript-eslint/eslint-plugin": "npm:^6.21.0"
"@typescript-eslint/parser": "npm:^6.21.0"
Expand Down Expand Up @@ -4738,6 +4739,15 @@ __metadata:
languageName: node
linkType: hard

"@types/node@npm:^25.2.1":
version: 25.2.1
resolution: "@types/node@npm:25.2.1"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/ce42fa07495093c55b6398e3c4346d644a61b8c4f59d2e0c0ed152ea0e4327c60a41d5fdfa3e0fc4f4776eb925e2b783b6b942501fc044328a44980bc2de4dc6
languageName: node
linkType: hard

"@types/react-dom@npm:^19":
version: 19.1.9
resolution: "@types/react-dom@npm:19.1.9"
Expand Down Expand Up @@ -11760,6 +11770,13 @@ __metadata:
languageName: node
linkType: hard

"undici-types@npm:~7.16.0":
version: 7.16.0
resolution: "undici-types@npm:7.16.0"
checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a
languageName: node
linkType: hard

"unicorn-magic@npm:^0.1.0":
version: 0.1.0
resolution: "unicorn-magic@npm:0.1.0"
Expand Down