@react-spa-scaffold/mcp 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +423 -0
  2. package/dist/features/index.d.ts +5 -0
  3. package/dist/features/index.d.ts.map +1 -0
  4. package/dist/features/index.js +3 -0
  5. package/dist/features/index.js.map +1 -0
  6. package/dist/features/registry.d.ts +10 -0
  7. package/dist/features/registry.d.ts.map +1 -0
  8. package/dist/features/registry.js +508 -0
  9. package/dist/features/registry.js.map +1 -0
  10. package/dist/features/types.d.ts +45 -0
  11. package/dist/features/types.d.ts.map +1 -0
  12. package/dist/features/types.js +5 -0
  13. package/dist/features/types.js.map +1 -0
  14. package/dist/features/versions.d.ts +16 -0
  15. package/dist/features/versions.d.ts.map +1 -0
  16. package/dist/features/versions.js +46 -0
  17. package/dist/features/versions.js.map +1 -0
  18. package/dist/features/versions.json +5 -0
  19. package/dist/index.d.ts +22 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +43 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/resources/docs.d.ts +29 -0
  24. package/dist/resources/docs.d.ts.map +1 -0
  25. package/dist/resources/docs.js +105 -0
  26. package/dist/resources/docs.js.map +1 -0
  27. package/dist/resources/index.d.ts +2 -0
  28. package/dist/resources/index.d.ts.map +1 -0
  29. package/dist/resources/index.js +2 -0
  30. package/dist/resources/index.js.map +1 -0
  31. package/dist/server.d.ts +12 -0
  32. package/dist/server.d.ts.map +1 -0
  33. package/dist/server.js +115 -0
  34. package/dist/server.js.map +1 -0
  35. package/dist/tools/get-example.d.ts +51 -0
  36. package/dist/tools/get-example.d.ts.map +1 -0
  37. package/dist/tools/get-example.js +90 -0
  38. package/dist/tools/get-example.js.map +1 -0
  39. package/dist/tools/get-features.d.ts +30 -0
  40. package/dist/tools/get-features.d.ts.map +1 -0
  41. package/dist/tools/get-features.js +46 -0
  42. package/dist/tools/get-features.js.map +1 -0
  43. package/dist/tools/get-scaffold.d.ts +77 -0
  44. package/dist/tools/get-scaffold.d.ts.map +1 -0
  45. package/dist/tools/get-scaffold.js +153 -0
  46. package/dist/tools/get-scaffold.js.map +1 -0
  47. package/dist/tools/index.d.ts +4 -0
  48. package/dist/tools/index.d.ts.map +1 -0
  49. package/dist/tools/index.js +4 -0
  50. package/dist/tools/index.js.map +1 -0
  51. package/dist/utils/docs.d.ts +14 -0
  52. package/dist/utils/docs.d.ts.map +1 -0
  53. package/dist/utils/docs.js +64 -0
  54. package/dist/utils/docs.js.map +1 -0
  55. package/dist/utils/examples.d.ts +27 -0
  56. package/dist/utils/examples.d.ts.map +1 -0
  57. package/dist/utils/examples.js +399 -0
  58. package/dist/utils/examples.js.map +1 -0
  59. package/dist/utils/index.d.ts +5 -0
  60. package/dist/utils/index.d.ts.map +1 -0
  61. package/dist/utils/index.js +5 -0
  62. package/dist/utils/index.js.map +1 -0
  63. package/dist/utils/paths.d.ts +28 -0
  64. package/dist/utils/paths.d.ts.map +1 -0
  65. package/dist/utils/paths.js +40 -0
  66. package/dist/utils/paths.js.map +1 -0
  67. package/dist/utils/scaffold.d.ts +50 -0
  68. package/dist/utils/scaffold.d.ts.map +1 -0
  69. package/dist/utils/scaffold.js +500 -0
  70. package/dist/utils/scaffold.js.map +1 -0
  71. package/dist/version.d.ts +5 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +19 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +63 -0
  76. package/templates/.bundled +0 -0
  77. package/templates/CLAUDE.md +145 -0
  78. package/templates/docs/API_REFERENCE.md +58 -0
  79. package/templates/docs/ARCHITECTURE.md +185 -0
  80. package/templates/docs/CODING_STANDARDS.md +53 -0
  81. package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
  82. package/templates/docs/E2E_TESTING.md +116 -0
  83. package/templates/docs/INTERNATIONALIZATION.md +67 -0
  84. package/templates/docs/TESTING.md +259 -0
  85. package/templates/docs/WORKFLOW.md +170 -0
  86. package/templates/src/App.tsx +42 -0
  87. package/templates/src/components/layout/Header.tsx +19 -0
  88. package/templates/src/components/layout/index.ts +1 -0
  89. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
  90. package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
  91. package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
  92. package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
  93. package/templates/src/components/shared/SEO/SEO.tsx +55 -0
  94. package/templates/src/components/shared/SEO/index.ts +1 -0
  95. package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
  96. package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
  97. package/templates/src/components/shared/index.ts +4 -0
  98. package/templates/src/components/ui/button.tsx +48 -0
  99. package/templates/src/components/ui/dropdown-menu.tsx +228 -0
  100. package/templates/src/components/ui/form-error.tsx +95 -0
  101. package/templates/src/components/ui/loading.tsx +58 -0
  102. package/templates/src/components/ui/skeleton.tsx +52 -0
  103. package/templates/src/components/ui/sonner.tsx +34 -0
  104. package/templates/src/components/ui/spinner.tsx +40 -0
  105. package/templates/src/components/ui/visually-hidden.tsx +51 -0
  106. package/templates/src/contexts/mobileContext.tsx +66 -0
  107. package/templates/src/contexts/queryContext.tsx +28 -0
  108. package/templates/src/hooks/index.ts +7 -0
  109. package/templates/src/hooks/useContactForm.ts +33 -0
  110. package/templates/src/hooks/useExampleQuery.ts +20 -0
  111. package/templates/src/hooks/useLanguage.ts +23 -0
  112. package/templates/src/hooks/useMediaQuery.ts +53 -0
  113. package/templates/src/hooks/useThemeEffect.ts +31 -0
  114. package/templates/src/hooks/useTouchSizes.ts +16 -0
  115. package/templates/src/i18n/config.ts +11 -0
  116. package/templates/src/i18n/detectLanguage.ts +57 -0
  117. package/templates/src/i18n/index.ts +20 -0
  118. package/templates/src/i18n/loadCatalog.ts +30 -0
  119. package/templates/src/index.css +98 -0
  120. package/templates/src/lib/api.ts +142 -0
  121. package/templates/src/lib/config.ts +15 -0
  122. package/templates/src/lib/constants.ts +8 -0
  123. package/templates/src/lib/env.ts +53 -0
  124. package/templates/src/lib/format.ts +119 -0
  125. package/templates/src/lib/index.ts +24 -0
  126. package/templates/src/lib/routes.ts +11 -0
  127. package/templates/src/lib/storage.ts +91 -0
  128. package/templates/src/lib/storageKeys.ts +10 -0
  129. package/templates/src/lib/utils.ts +6 -0
  130. package/templates/src/lib/validations.ts +39 -0
  131. package/templates/src/locales/de.po +65 -0
  132. package/templates/src/locales/en.po +65 -0
  133. package/templates/src/locales/es.po +65 -0
  134. package/templates/src/main.tsx +107 -0
  135. package/templates/src/mocks/fixtures/index.ts +1 -0
  136. package/templates/src/mocks/fixtures/todos.ts +40 -0
  137. package/templates/src/mocks/handlers/index.ts +7 -0
  138. package/templates/src/mocks/handlers/todos.ts +59 -0
  139. package/templates/src/mocks/index.ts +3 -0
  140. package/templates/src/mocks/node.ts +9 -0
  141. package/templates/src/pages/Home.tsx +27 -0
  142. package/templates/src/pages/NotFound.tsx +28 -0
  143. package/templates/src/pages/index.ts +2 -0
  144. package/templates/src/stores/index.ts +2 -0
  145. package/templates/src/stores/preferencesStore.ts +85 -0
  146. package/templates/src/test/index.ts +8 -0
  147. package/templates/src/test/mocks.ts +17 -0
  148. package/templates/src/test/providers.tsx +54 -0
  149. package/templates/src/test-setup.ts +54 -0
  150. package/templates/src/types/api.ts +31 -0
  151. package/templates/src/types/index.ts +2 -0
  152. package/templates/src/types/preferences.ts +5 -0
  153. package/templates/src/vite-env.d.ts +10 -0
  154. package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
  155. package/templates/tests/unit/components/Header.test.tsx +33 -0
  156. package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
  157. package/templates/tests/unit/components/Loading.test.tsx +76 -0
  158. package/templates/tests/unit/components/SEO.test.tsx +80 -0
  159. package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
  160. package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
  161. package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
  162. package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
  163. package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
  164. package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
  165. package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
  166. package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
  167. package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
  168. package/templates/tests/unit/lib/api.test.ts +142 -0
  169. package/templates/tests/unit/lib/format.test.ts +100 -0
  170. package/templates/tests/unit/lib/storage.test.ts +90 -0
  171. package/templates/tests/unit/lib/utils.test.ts +19 -0
  172. package/templates/tests/unit/lib/validations.test.ts +56 -0
  173. package/templates/tests/unit/stores/preferencesStore.test.ts +75 -0
@@ -0,0 +1,301 @@
1
+ # React + TypeScript Component Guidelines
2
+
3
+ A focused blueprint for writing React components. For related patterns, see:
4
+
5
+ - [Coding Standards](./CODING_STANDARDS.md) - TypeScript, state management, hooks
6
+ - [Testing](./TESTING.md) - Unit testing patterns
7
+ - [Internationalization](./INTERNATIONALIZATION.md) - i18n with Lingui
8
+
9
+ ---
10
+
11
+ ## Component Anatomy
12
+
13
+ Every component follows this structure:
14
+
15
+ ```tsx
16
+ // 1. Imports (grouped: external → internal → types)
17
+ import { useState } from 'react';
18
+
19
+ import { Button } from '@/components/ui/button';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ // 2. Types/Interfaces
23
+ export interface MyComponentProps {
24
+ title: string;
25
+ count?: number;
26
+ onAction?: () => void;
27
+ }
28
+
29
+ // 3. Component Implementation
30
+ export function MyComponent({ title, count = 0, onAction }: MyComponentProps) {
31
+ // a. Hooks (all hooks must be called unconditionally)
32
+ const [isOpen, setIsOpen] = useState(false);
33
+
34
+ // b. Derived state / computations
35
+ const displayCount = count > 99 ? '99+' : count;
36
+
37
+ // c. Event handlers
38
+ const handleClick = () => {
39
+ setIsOpen(true);
40
+ onAction?.();
41
+ };
42
+
43
+ // d. Early returns (only AFTER all hooks)
44
+ if (!title) return null;
45
+
46
+ // e. Render
47
+ return (
48
+ <div>
49
+ <h2>{title}</h2>
50
+ <span>{displayCount}</span>
51
+ <Button onClick={handleClick}>Action</Button>
52
+ </div>
53
+ );
54
+ }
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Props Patterns
60
+
61
+ ### Extending HTML Attributes
62
+
63
+ For components wrapping native elements:
64
+
65
+ ```tsx
66
+ import { forwardRef } from 'react';
67
+
68
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
69
+ variant?: 'primary' | 'secondary';
70
+ isLoading?: boolean;
71
+ }
72
+
73
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
74
+ ({ variant = 'primary', isLoading, children, className, ...props }, ref) => {
75
+ return (
76
+ <button ref={ref} className={cn(buttonVariants({ variant }), className)} {...props}>
77
+ {isLoading ? <Spinner /> : children}
78
+ </button>
79
+ );
80
+ },
81
+ );
82
+
83
+ Button.displayName = 'Button';
84
+ ```
85
+
86
+ ### Generic Components
87
+
88
+ ```tsx
89
+ interface ListProps<T> {
90
+ items: T[];
91
+ renderItem: (item: T) => ReactNode;
92
+ keyExtractor: (item: T) => string;
93
+ }
94
+
95
+ export function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
96
+ return (
97
+ <ul>
98
+ {items.map((item) => (
99
+ <li key={keyExtractor(item)}>{renderItem(item)}</li>
100
+ ))}
101
+ </ul>
102
+ );
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Component Categories
109
+
110
+ | Category | Location | Naming | Export | Barrel |
111
+ | ------------------ | ---------------------------- | -------------------------- | ------- | ------ |
112
+ | UI Primitives | `components/ui/` | `button.tsx` (lowercase) | Named | No\* |
113
+ | Feature Components | `components/shared/Feature/` | `Feature.tsx` (PascalCase) | Named | Yes |
114
+ | Layout | `components/layout/` | `Header.tsx` (PascalCase) | Named | Yes |
115
+ | Pages | `pages/` | `Home.tsx` (PascalCase) | Default | No |
116
+
117
+ \*UI primitives use direct imports (`@/components/ui/button`) following shadcn/ui conventions.
118
+
119
+ ### Feature Component Structure
120
+
121
+ ```
122
+ src/components/shared/
123
+ ├── ThemeToggle/
124
+ │ ├── ThemeToggle.tsx
125
+ │ └── index.ts # export { ThemeToggle } from './ThemeToggle';
126
+ ├── LanguageSwitcher/
127
+ │ └── ...
128
+ └── index.ts # Re-exports all shared components
129
+ ```
130
+
131
+ ---
132
+
133
+ ## File Organization
134
+
135
+ ### Default: Single File
136
+
137
+ Keep types, helpers, and component together in one file. This is the pattern used throughout this codebase—even `dropdown-menu.tsx` at 228 lines remains a single file.
138
+
139
+ ```tsx
140
+ // button.tsx - types and component together
141
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
142
+ variant?: 'primary' | 'secondary';
143
+ }
144
+
145
+ const buttonVariants = cva(/* ... */);
146
+
147
+ export function Button({ variant, className, ...props }: ButtonProps) {
148
+ return <button className={cn(buttonVariants({ variant }), className)} {...props} />;
149
+ }
150
+ ```
151
+
152
+ ### When to Split
153
+
154
+ Split only when you have a **concrete reason**, not preemptively:
155
+
156
+ | Situation | Action |
157
+ | -------------------------------- | --------------------------------- |
158
+ | Types reused by other components | Move to `@/types/` |
159
+ | Helper reused elsewhere | Move to `@/lib/` |
160
+ | File exceeds ~300 lines | Consider splitting by concern |
161
+ | Multiple sub-components | Create folder with separate files |
162
+
163
+ ### Splitting Example
164
+
165
+ For a complex component like a data table:
166
+
167
+ ```
168
+ src/components/shared/DataTable/
169
+ ├── DataTable.tsx # Main component
170
+ ├── DataTableHeader.tsx # Sub-component
171
+ ├── DataTableRow.tsx # Sub-component
172
+ ├── columns.tsx # Column configuration
173
+ └── index.ts # export { DataTable } from './DataTable';
174
+ ```
175
+
176
+ **Avoid**: Separate `.types.ts` or `.helpers.ts` files for code used only by that component. Keep related code together.
177
+
178
+ ---
179
+
180
+ ## Styling with CVA
181
+
182
+ Use [Class Variance Authority](https://cva.style/docs) for variant-based styling:
183
+
184
+ ```tsx
185
+ import { cva, type VariantProps } from 'class-variance-authority';
186
+ import { cn } from '@/lib/utils';
187
+
188
+ const badgeVariants = cva(
189
+ // Base styles
190
+ 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold',
191
+ {
192
+ variants: {
193
+ variant: {
194
+ default: 'bg-primary text-primary-foreground',
195
+ secondary: 'bg-secondary text-secondary-foreground',
196
+ destructive: 'bg-destructive text-destructive-foreground',
197
+ outline: 'border border-input bg-transparent',
198
+ },
199
+ size: {
200
+ sm: 'px-2 py-0.5 text-xs',
201
+ md: 'px-2.5 py-0.5 text-sm',
202
+ lg: 'px-3 py-1 text-base',
203
+ },
204
+ },
205
+ defaultVariants: {
206
+ variant: 'default',
207
+ size: 'md',
208
+ },
209
+ },
210
+ );
211
+
212
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
213
+
214
+ export function Badge({ variant, size, className, ...props }: BadgeProps) {
215
+ return <span className={cn(badgeVariants({ variant, size }), className)} {...props} />;
216
+ }
217
+ ```
218
+
219
+ ### `cn()` Utility
220
+
221
+ ```tsx
222
+ cn('px-4 py-2', 'px-6'); // → 'py-2 px-6' (later overrides)
223
+ cn('text-red-500', className); // → allows prop override
224
+ cn(isActive && 'bg-primary'); // → conditional classes
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Loading & Error States
230
+
231
+ Components fetching data should handle all states:
232
+
233
+ ```tsx
234
+ export function UserProfile({ userId }: { userId: string }) {
235
+ const { data: user, isLoading, error } = useUserQuery(userId);
236
+
237
+ if (isLoading) {
238
+ return (
239
+ <div className="flex items-center gap-3">
240
+ <Skeleton className="h-10 w-10 rounded-full" />
241
+ <Skeleton className="h-4 w-24" />
242
+ </div>
243
+ );
244
+ }
245
+
246
+ if (error || !user) {
247
+ return (
248
+ <div className="text-destructive">
249
+ <Trans comment="Error when user profile fails to load">Failed to load profile</Trans>
250
+ </div>
251
+ );
252
+ }
253
+
254
+ return (
255
+ <div className="flex items-center gap-3">
256
+ <Avatar src={user.avatar} alt={user.name} />
257
+ <span>{user.name}</span>
258
+ </div>
259
+ );
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Accessibility
266
+
267
+ ### Required Practices
268
+
269
+ ```tsx
270
+ // Icons need labels
271
+ <button aria-label={t({ message: 'Close', comment: 'Close dialog button' })}>
272
+ <XIcon />
273
+ </button>
274
+
275
+ // Use semantic roles
276
+ <header role="banner">...</header>
277
+ <nav role="navigation">...</nav>
278
+ <main role="main">...</main>
279
+
280
+ // Loading states
281
+ <Spinner role="status" aria-label="Loading" />
282
+
283
+ // Interactive elements need focus styles (handled by Tailwind's focus-visible)
284
+ <button className="focus-visible:ring-2 focus-visible:ring-ring">...</button>
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Checklist
290
+
291
+ Before submitting a component:
292
+
293
+ - [ ] Props use `interface` with `Props` suffix
294
+ - [ ] Named export (default only for pages)
295
+ - [ ] Imports use `@/` path alias
296
+ - [ ] User-facing text has translator comments
297
+ - [ ] Handles loading/error states for async data
298
+ - [ ] Uses `cn()` for className merging
299
+ - [ ] Accessible (roles, aria-labels, keyboard nav)
300
+ - [ ] Barrel export in `index.ts` (except UI primitives)
301
+ - [ ] Test file in `tests/unit/`
@@ -0,0 +1,116 @@
1
+ # E2E Testing Guidelines
2
+
3
+ ## Overview
4
+
5
+ - **Framework**: Playwright
6
+ - **Test location**: `e2e/tests/`
7
+ - **Utilities**: `e2e/fixtures/`
8
+
9
+ ## File Structure
10
+
11
+ ```
12
+ e2e/
13
+ ├── fixtures/
14
+ │ └── index.ts # setupPage, clearAppState
15
+ └── tests/
16
+ ├── home.spec.ts # Page structure, accessibility
17
+ ├── theme.spec.ts # Theme toggle, persistence
18
+ ├── language.spec.ts # Language switcher
19
+ └── navigation.spec.ts # Routing, 404
20
+ ```
21
+
22
+ ## Imports
23
+
24
+ ```typescript
25
+ import { expect, test } from '@playwright/test';
26
+
27
+ // For tests that need state clearing
28
+ import { setupPage } from '../fixtures';
29
+ ```
30
+
31
+ ## Core Patterns
32
+
33
+ ### Simple Page Tests
34
+
35
+ ```typescript
36
+ test.describe('Home Page', () => {
37
+ test.beforeEach(async ({ page }) => {
38
+ await page.goto('/');
39
+ });
40
+
41
+ test('displays welcome heading', async ({ page }) => {
42
+ await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
43
+ });
44
+ });
45
+ ```
46
+
47
+ ### Tests Requiring Clean State
48
+
49
+ ```typescript
50
+ import { setupPage } from '../fixtures';
51
+
52
+ test.describe('Theme Toggle', () => {
53
+ test.beforeEach(async ({ page }) => {
54
+ await setupPage(page); // Clears localStorage, reloads
55
+ });
56
+
57
+ test('persists preference across reload', async ({ page }) => {
58
+ await page.getByRole('button', { name: /dark mode/i }).click();
59
+ await page.reload();
60
+ await expect(page.locator('html')).toHaveClass(/dark/);
61
+ });
62
+ });
63
+ ```
64
+
65
+ ### Selecting Elements
66
+
67
+ ```typescript
68
+ // ✅ Good - accessible selectors
69
+ page.getByRole('button', { name: /submit/i });
70
+ page.getByRole('heading', { name: /welcome/i });
71
+ page.getByText('English');
72
+
73
+ // ❌ Avoid
74
+ page.locator('.btn-primary');
75
+ page.locator('div > span.title');
76
+ ```
77
+
78
+ ### Waiting for State
79
+
80
+ ```typescript
81
+ // ✅ Good - wait for visible state change
82
+ await expect(page.getByText('Success')).toBeVisible();
83
+ await expect(page.locator('html')).toHaveClass(/dark/);
84
+
85
+ // ❌ Avoid - arbitrary timeouts
86
+ await page.waitForTimeout(500);
87
+ ```
88
+
89
+ ## What to Test
90
+
91
+ | Type | Examples |
92
+ | ----------- | --------------------------------- |
93
+ | Navigation | Routes work, 404 handling |
94
+ | User flows | Toggle theme, change language |
95
+ | Persistence | Settings survive reload |
96
+ | Structure | Header present, main content area |
97
+
98
+ ## What NOT to Test
99
+
100
+ - CSS class names (implementation detail)
101
+ - Internal component state
102
+ - API response content (use unit tests)
103
+
104
+ ## Running Tests
105
+
106
+ ```bash
107
+ npm run e2e # Run all
108
+ npm run e2e:ui # Interactive UI
109
+ ```
110
+
111
+ ## Checklist
112
+
113
+ - [ ] Uses accessible selectors
114
+ - [ ] No arbitrary timeouts
115
+ - [ ] Tests behavior, not implementation
116
+ - [ ] Uses `setupPage` when testing persistence
@@ -0,0 +1,67 @@
1
+ # Internationalization (i18n)
2
+
3
+ This project uses [Lingui](https://lingui.dev/) for internationalization. **ESLint will warn about untranslated strings.**
4
+
5
+ ## Quick Reference
6
+
7
+ ```tsx
8
+ // Trans component - for JSX content
9
+ <Trans comment="Error shown when upload exceeds 10MB">File too large</Trans>;
10
+
11
+ // t() function - for strings (aria-labels, placeholders, etc.)
12
+ const { t } = useLingui();
13
+ <Button aria-label={t({ message: 'Close', comment: 'Close dialog button' })} />;
14
+
15
+ // msg() - for messages defined outside components
16
+ const labels = {
17
+ save: msg({ message: 'Save', comment: 'Form submit button' }),
18
+ };
19
+ ```
20
+
21
+ ## Adding Translations
22
+
23
+ 1. Wrap text with `<Trans>` or `t()` and add a `comment`
24
+ 2. Run `pnpm i18n:extract` to update PO files
25
+ 3. Translate in `src/locales/{locale}.po`
26
+ 4. Build compiles translations automatically
27
+
28
+ ## Writing Comments
29
+
30
+ Comments help translators understand context. They appear in PO files as `#.` lines.
31
+
32
+ ```tsx
33
+ // Good - explains where and why
34
+ <Trans comment="Error message when file upload exceeds 10MB limit">
35
+ File too large
36
+ </Trans>
37
+
38
+ // Good - explains the variable
39
+ <Trans comment="Badge showing unread count, {count} is 1-99 or 99+">
40
+ {count} new
41
+ </Trans>
42
+
43
+ // Bad - obvious or vague
44
+ <Trans comment="A message">Error</Trans>
45
+ <Trans comment="Title">Welcome</Trans>
46
+ ```
47
+
48
+ **Tip:** Use `context` prop when the same word needs different translations:
49
+
50
+ ```tsx
51
+ <Trans context="calendar">Date</Trans>
52
+ <Trans context="romantic">Date</Trans>
53
+ ```
54
+
55
+ ## ESLint Rules
56
+
57
+ The `eslint-plugin-lingui` enforces translations:
58
+
59
+ - `no-unlocalized-strings` - Warns about untranslated JSX text
60
+ - `t-call-in-function` - Ensures `t()` is called inside functions
61
+ - `no-trans-inside-trans` - Prevents nested Trans components
62
+
63
+ Excluded from checks: tests, mocks, UI primitives, config files.
64
+
65
+ ## Adding a New Locale
66
+
67
+ Edit `lingui.config.js` and add the locale code to the `locales` array.