@l10nmonster/server 3.0.0-alpha.8 → 3.1.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.
- package/.releaserc.json +1 -1
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +808 -0
- package/index.js +121 -17
- package/package.json +9 -8
- package/routes/dispatcher.js +116 -0
- package/routes/info.js +25 -0
- package/routes/providers.js +17 -0
- package/routes/sources.js +49 -7
- package/routes/status.js +27 -27
- package/routes/tm.js +156 -6
- package/ui/dist/assets/Cart-CiY5V__G.js +1 -0
- package/ui/dist/assets/Job-D-5ikxga.js +1 -0
- package/ui/dist/assets/Providers-BZVmclS1.js +1 -0
- package/ui/dist/assets/Sources-BwZ8Vub0.js +1 -0
- package/ui/dist/assets/SourcesDetail-CXgslRDb.js +1 -0
- package/ui/dist/assets/SourcesResource-Br3Bspz2.js +1 -0
- package/ui/dist/assets/Status-Bx0Ui7d2.js +1 -0
- package/ui/dist/assets/StatusDetail-BzJ2TIme.js +1 -0
- package/ui/dist/assets/TMByProvider-B2MrTxO0.js +1 -0
- package/ui/dist/assets/TMDetail-BxbKr57p.js +1 -0
- package/ui/dist/assets/TMToc-CQ1zhmPh.js +1 -0
- package/ui/dist/assets/Welcome-Tp-UfiIW.js +1 -0
- package/ui/dist/assets/index-543A5WcJ.js +1 -0
- package/ui/dist/assets/index-CPrLFF-N.js +2 -0
- package/ui/dist/assets/vendor-BVgSJH5C.js +19 -0
- package/ui/dist/index.html +3 -1
- package/ui/dist/assets/Sources-D0R-Sgwf.js +0 -1
- package/ui/dist/assets/Status-XBRD-MuK.js +0 -1
- package/ui/dist/assets/TM-DZ2x6--n.js +0 -1
- package/ui/dist/assets/Welcome-p4gi31Lo.js +0 -1
- package/ui/dist/assets/api-DXOYnFyU.js +0 -1
- package/ui/dist/assets/badge-CveKztw5.js +0 -1
- package/ui/dist/assets/grid-DetiGbYY.js +0 -1
- package/ui/dist/assets/index-Ce8PP-0Z.js +0 -76
- package/ui/dist/assets/v-stack-CQ6LIfdw.js +0 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
# Chakra UI v3 Development Guide
|
|
2
|
+
|
|
3
|
+
This file provides specific guidance for working with Chakra UI v3.x in this project. **IMPORTANT: Chakra UI v3 is NOT backward compatible with v2.x - always check the v3 documentation.**
|
|
4
|
+
|
|
5
|
+
## Current Version
|
|
6
|
+
|
|
7
|
+
This project uses Chakra UI v3.2.0. Always verify component APIs in the official v3 documentation.
|
|
8
|
+
|
|
9
|
+
## Key Breaking Changes from v2 to v3
|
|
10
|
+
|
|
11
|
+
### 1. Compound Component Pattern
|
|
12
|
+
Most components now use a compound pattern instead of single components:
|
|
13
|
+
|
|
14
|
+
```jsx
|
|
15
|
+
// ❌ v2 Pattern (DON'T USE)
|
|
16
|
+
<Switch isChecked={value} onChange={setValue} />
|
|
17
|
+
|
|
18
|
+
// ✅ v3 Pattern (CORRECT)
|
|
19
|
+
<Switch.Root checked={value} onCheckedChange={(details) => setValue(details.checked)}>
|
|
20
|
+
<Switch.HiddenInput />
|
|
21
|
+
<Switch.Control>
|
|
22
|
+
<Switch.Thumb />
|
|
23
|
+
</Switch.Control>
|
|
24
|
+
<Switch.Label>Label text</Switch.Label>
|
|
25
|
+
</Switch.Root>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Event Handler Changes
|
|
29
|
+
Event handlers often receive objects instead of direct values:
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
// ❌ v2: Direct value
|
|
33
|
+
onChange={(value) => setValue(value)}
|
|
34
|
+
|
|
35
|
+
// ✅ v3: Object with details
|
|
36
|
+
onCheckedChange={(details) => setValue(details.checked)}
|
|
37
|
+
onValueChange={(details) => setValue(details.value)}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 3. Prop Name Changes
|
|
41
|
+
Many prop names have changed:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
// ❌ v2 Props
|
|
45
|
+
isChecked, isOpen, isDisabled, isInvalid
|
|
46
|
+
|
|
47
|
+
// ✅ v3 Props
|
|
48
|
+
checked, open, disabled, invalid
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 4. Color System Changes
|
|
52
|
+
Chakra UI v3 uses semantic color tokens instead of numbered scales:
|
|
53
|
+
|
|
54
|
+
```jsx
|
|
55
|
+
// ❌ v2 Color System (DON'T USE)
|
|
56
|
+
bg="gray.50" // Light gray background
|
|
57
|
+
bg="gray.100" // Slightly darker gray
|
|
58
|
+
color="gray.500" // Medium gray text
|
|
59
|
+
color="gray.600" // Darker gray text
|
|
60
|
+
color="blue.600" // Blue text
|
|
61
|
+
bg="blue.50" // Light blue background
|
|
62
|
+
|
|
63
|
+
// ✅ v3 Semantic Colors (CORRECT)
|
|
64
|
+
bg="bg.muted" // Light background
|
|
65
|
+
bg="bg.subtle" // Subtle background
|
|
66
|
+
color="fg.muted" // Muted text
|
|
67
|
+
color="fg.default" // Default text
|
|
68
|
+
color="blue.600" // Blue text (some numbered colors still work)
|
|
69
|
+
bg="blue.subtle" // Light blue background
|
|
70
|
+
bg="blue.muted" // Muted blue background
|
|
71
|
+
borderColor="border.default" // Default border color
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Common v3 semantic tokens:**
|
|
75
|
+
- `bg.default` - Default background
|
|
76
|
+
- `bg.muted` - Muted background
|
|
77
|
+
- `bg.subtle` - Subtle background
|
|
78
|
+
- `fg.default` - Default text
|
|
79
|
+
- `fg.muted` - Muted text
|
|
80
|
+
- `border.default` - Default border
|
|
81
|
+
- `blue.subtle` - Light blue
|
|
82
|
+
- `blue.muted` - Muted blue
|
|
83
|
+
- `gray.subtle` - Light gray
|
|
84
|
+
- `gray.muted` - Muted gray
|
|
85
|
+
|
|
86
|
+
## Common Components Reference
|
|
87
|
+
|
|
88
|
+
### Switch
|
|
89
|
+
```jsx
|
|
90
|
+
<Switch.Root checked={value} onCheckedChange={(details) => setValue(details.checked)}>
|
|
91
|
+
<Switch.HiddenInput />
|
|
92
|
+
<Switch.Control>
|
|
93
|
+
<Switch.Thumb />
|
|
94
|
+
</Switch.Control>
|
|
95
|
+
<Switch.Label>Switch Label</Switch.Label>
|
|
96
|
+
</Switch.Root>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Select
|
|
100
|
+
```jsx
|
|
101
|
+
<Select.Root
|
|
102
|
+
value={selectedValue ? [selectedValue] : []}
|
|
103
|
+
onValueChange={(details) => {
|
|
104
|
+
// Note: onValueChange may not work reliably in Chakra UI v3
|
|
105
|
+
// Use direct onClick on Select.Item as workaround
|
|
106
|
+
}}
|
|
107
|
+
positioning={{
|
|
108
|
+
strategy: "absolute",
|
|
109
|
+
placement: "bottom-start",
|
|
110
|
+
flip: true,
|
|
111
|
+
gutter: 4
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<Select.Trigger>
|
|
115
|
+
{/* Use Text component for reliable value display */}
|
|
116
|
+
<Text fontSize="sm" flex="1" textAlign="left">
|
|
117
|
+
{selectedValue || "Select option"}
|
|
118
|
+
</Text>
|
|
119
|
+
<Select.Indicator />
|
|
120
|
+
</Select.Trigger>
|
|
121
|
+
<Select.Positioner>
|
|
122
|
+
<Select.Content
|
|
123
|
+
zIndex={1000}
|
|
124
|
+
bg="white"
|
|
125
|
+
borderWidth="1px"
|
|
126
|
+
borderColor="border.default"
|
|
127
|
+
borderRadius="md"
|
|
128
|
+
shadow="lg"
|
|
129
|
+
maxH="200px"
|
|
130
|
+
overflow="auto"
|
|
131
|
+
>
|
|
132
|
+
{options.map((option) => (
|
|
133
|
+
<Select.Item
|
|
134
|
+
key={option.id}
|
|
135
|
+
item={option.id}
|
|
136
|
+
value={option.id}
|
|
137
|
+
onClick={() => setSelectedValue(option.id)}
|
|
138
|
+
>
|
|
139
|
+
<Select.ItemText>{option.label}</Select.ItemText>
|
|
140
|
+
<Select.ItemIndicator />
|
|
141
|
+
</Select.Item>
|
|
142
|
+
))}
|
|
143
|
+
</Select.Content>
|
|
144
|
+
</Select.Positioner>
|
|
145
|
+
</Select.Root>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Select Component Known Issues:**
|
|
149
|
+
- `onValueChange` may not trigger reliably in Chakra UI v3
|
|
150
|
+
- `Select.ValueText` doesn't display selected values consistently
|
|
151
|
+
- **Workarounds:**
|
|
152
|
+
- Use `Text` component in trigger for value display
|
|
153
|
+
- Use direct `onClick` on `Select.Item` for selection handling
|
|
154
|
+
- Keep `value` prop for maintaining component state
|
|
155
|
+
|
|
156
|
+
### Button
|
|
157
|
+
```jsx
|
|
158
|
+
// Simple button
|
|
159
|
+
<Button variant="solid" size="md">Click me</Button>
|
|
160
|
+
|
|
161
|
+
// Button with active state
|
|
162
|
+
<Button data-active>Active Button</Button>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Input with Field
|
|
166
|
+
```jsx
|
|
167
|
+
<Field.Root invalid={hasError}>
|
|
168
|
+
<Field.Label>Email</Field.Label>
|
|
169
|
+
<Input placeholder="Enter email" />
|
|
170
|
+
<Field.ErrorText>This field is required</Field.ErrorText>
|
|
171
|
+
</Field.Root>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Accordion
|
|
175
|
+
```jsx
|
|
176
|
+
<Accordion.Root>
|
|
177
|
+
<Accordion.Item>
|
|
178
|
+
<Accordion.ItemTrigger />
|
|
179
|
+
<Accordion.ItemContent />
|
|
180
|
+
</Accordion.Item>
|
|
181
|
+
</Accordion.Root>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Modal/Dialog
|
|
185
|
+
```jsx
|
|
186
|
+
<Dialog.Root open={isOpen} onOpenChange={(details) => setIsOpen(details.open)}>
|
|
187
|
+
<Dialog.Trigger asChild>
|
|
188
|
+
<Button>Open Dialog</Button>
|
|
189
|
+
</Dialog.Trigger>
|
|
190
|
+
<Dialog.Backdrop />
|
|
191
|
+
<Dialog.Positioner>
|
|
192
|
+
<Dialog.Content>
|
|
193
|
+
<Dialog.Header>
|
|
194
|
+
<Dialog.Title>Title</Dialog.Title>
|
|
195
|
+
<Dialog.CloseTrigger />
|
|
196
|
+
</Dialog.Header>
|
|
197
|
+
<Dialog.Body>Content</Dialog.Body>
|
|
198
|
+
</Dialog.Content>
|
|
199
|
+
</Dialog.Positioner>
|
|
200
|
+
</Dialog.Root>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Development Guidelines
|
|
204
|
+
|
|
205
|
+
### 1. Always Check Documentation First
|
|
206
|
+
- **Before using any component**, check the official Chakra UI v3 documentation
|
|
207
|
+
- **Never assume** v2 patterns will work in v3
|
|
208
|
+
- Use the MCP Context7 tool to get up-to-date documentation
|
|
209
|
+
- **Use the Chakra UI MCP server** - This project has access to specialized Chakra UI MCP tools for getting component examples, props, themes, and migration guidance
|
|
210
|
+
|
|
211
|
+
### 2. Common Pitfalls to Avoid
|
|
212
|
+
- Don't use `isChecked`, use `checked`
|
|
213
|
+
- Don't use `onChange` for switches, use `onCheckedChange`
|
|
214
|
+
- Don't use single components like `<Switch />`, use compound patterns
|
|
215
|
+
- Don't assume event handlers receive direct values - they often receive objects
|
|
216
|
+
|
|
217
|
+
### 3. Testing Patterns
|
|
218
|
+
- Always test both states (on/off, open/closed, etc.)
|
|
219
|
+
- Check browser console for any React warnings about invalid props
|
|
220
|
+
- If you get "Element type is invalid" errors, check import paths and component names
|
|
221
|
+
|
|
222
|
+
### 4. Import Patterns
|
|
223
|
+
```jsx
|
|
224
|
+
// ✅ Correct v3 imports
|
|
225
|
+
import { Switch, Button, Field, Dialog } from '@chakra-ui/react'
|
|
226
|
+
|
|
227
|
+
// Use compound components
|
|
228
|
+
<Switch.Root>
|
|
229
|
+
<Field.Root>
|
|
230
|
+
<Dialog.Root>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 5. Checkbox Component (Compound Pattern)
|
|
234
|
+
Checkbox follows the compound component pattern in v3:
|
|
235
|
+
|
|
236
|
+
```jsx
|
|
237
|
+
// ✅ v3: Checkbox compound component pattern
|
|
238
|
+
import { Checkbox } from '@chakra-ui/react'
|
|
239
|
+
|
|
240
|
+
// Basic checkbox
|
|
241
|
+
<Checkbox.Root
|
|
242
|
+
checked={isChecked}
|
|
243
|
+
onCheckedChange={(details) => setIsChecked(details.checked)}
|
|
244
|
+
>
|
|
245
|
+
<Checkbox.HiddenInput />
|
|
246
|
+
<Checkbox.Control />
|
|
247
|
+
<Checkbox.Label>Check me</Checkbox.Label>
|
|
248
|
+
</Checkbox.Root>
|
|
249
|
+
|
|
250
|
+
// Checkbox with indeterminate state
|
|
251
|
+
<Checkbox.Root
|
|
252
|
+
checked={isAllSelected}
|
|
253
|
+
indeterminate={isIndeterminate}
|
|
254
|
+
onCheckedChange={(details) => handleSelectAll(details.checked)}
|
|
255
|
+
>
|
|
256
|
+
<Checkbox.HiddenInput />
|
|
257
|
+
<Checkbox.Control />
|
|
258
|
+
</Checkbox.Root>
|
|
259
|
+
|
|
260
|
+
// ❌ Alternative: Native input (if compound pattern doesn't work)
|
|
261
|
+
<Box
|
|
262
|
+
as="input"
|
|
263
|
+
type="checkbox"
|
|
264
|
+
checked={isChecked}
|
|
265
|
+
onChange={(e) => setIsChecked(e.target.checked)}
|
|
266
|
+
cursor="pointer"
|
|
267
|
+
/>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Debugging Tips
|
|
271
|
+
|
|
272
|
+
1. **Invalid Element Type Errors**: Usually means wrong import or component name
|
|
273
|
+
2. **Props Not Working**: Check if prop names changed from v2 to v3
|
|
274
|
+
3. **Event Handlers Not Firing**: Check if handler expects an object instead of direct value
|
|
275
|
+
4. **Styling Issues**: v3 uses different CSS architecture - check if style props changed
|
|
276
|
+
5. **Missing Component**: If a component doesn't exist, use native HTML elements with `Box as="element"`
|
|
277
|
+
|
|
278
|
+
## Resources
|
|
279
|
+
|
|
280
|
+
- [Chakra UI v3 Migration Guide](https://v3.chakra-ui.com/docs/migration)
|
|
281
|
+
- [Chakra UI v3 Components](https://v3.chakra-ui.com/docs/components)
|
|
282
|
+
- Use MCP Context7 tool with library ID `/llmstxt/chakra-ui-llms-full.txt` for latest docs
|
|
283
|
+
- **Chakra UI MCP Server Tools:**
|
|
284
|
+
- `mcp__chakra-ui__get_component_props` - Get component properties and configuration options
|
|
285
|
+
- `mcp__chakra-ui__get_component_example` - Get practical implementation examples with code snippets
|
|
286
|
+
- `mcp__chakra-ui__list_components` - List all available Chakra UI components
|
|
287
|
+
- `mcp__chakra-ui__get_theme` - Retrieve theme specification (colors, fonts, textStyles)
|
|
288
|
+
- `mcp__chakra-ui__customize_theme` - Setup custom theme tokens
|
|
289
|
+
- `mcp__chakra-ui__v2_to_v3_code_review` - Get migration guidance for specific v2→v3 scenarios
|
|
290
|
+
- `mcp__chakra-ui__installation` - Get setup instructions for different frameworks
|
|
291
|
+
|
|
292
|
+
## Data Fetching Architecture
|
|
293
|
+
|
|
294
|
+
### React Query Implementation
|
|
295
|
+
**IMPORTANT**: This project uses React Query (@tanstack/react-query) for all data fetching. Never use manual useEffect patterns for API calls.
|
|
296
|
+
|
|
297
|
+
```jsx
|
|
298
|
+
// ✅ Correct: Use React Query
|
|
299
|
+
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
|
|
300
|
+
|
|
301
|
+
// Simple data fetching
|
|
302
|
+
const { data, isLoading, error } = useQuery({
|
|
303
|
+
queryKey: ['tmStats'],
|
|
304
|
+
queryFn: () => fetchApi('/api/tm/stats'),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Infinite scrolling with pagination
|
|
308
|
+
const {
|
|
309
|
+
data: infiniteData,
|
|
310
|
+
isLoading,
|
|
311
|
+
fetchNextPage,
|
|
312
|
+
hasNextPage,
|
|
313
|
+
isFetchingNextPage,
|
|
314
|
+
} = useInfiniteQuery({
|
|
315
|
+
queryKey: ['tmSearch', sourceLang, targetLang, filters],
|
|
316
|
+
queryFn: async ({ pageParam = 1 }) => {
|
|
317
|
+
const queryParams = new URLSearchParams({
|
|
318
|
+
sourceLang,
|
|
319
|
+
targetLang,
|
|
320
|
+
page: pageParam.toString(),
|
|
321
|
+
limit: '100',
|
|
322
|
+
...Object.fromEntries(Object.entries(filters).filter(([_, value]) => value.trim() !== ''))
|
|
323
|
+
});
|
|
324
|
+
return fetchApi(`/api/tm/search?${queryParams}`);
|
|
325
|
+
},
|
|
326
|
+
getNextPageParam: (lastPage, allPages) => {
|
|
327
|
+
const hasMore = lastPage.data.length === parseInt(lastPage.limit);
|
|
328
|
+
return hasMore ? allPages.length + 1 : undefined;
|
|
329
|
+
},
|
|
330
|
+
staleTime: 30000, // 30 seconds for search results
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Flatten infinite query data
|
|
334
|
+
const data = useMemo(() => {
|
|
335
|
+
return infiniteData?.pages.flatMap(page => page.data) || [];
|
|
336
|
+
}, [infiniteData]);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Query Key Patterns
|
|
340
|
+
Use consistent query keys that include all relevant parameters:
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
// ✅ Good query keys - include all parameters that affect the data
|
|
344
|
+
queryKey: ['status']
|
|
345
|
+
queryKey: ['tmStats']
|
|
346
|
+
queryKey: ['info']
|
|
347
|
+
queryKey: ['tmSearch', sourceLang, targetLang, filters]
|
|
348
|
+
|
|
349
|
+
// ❌ Bad - missing parameters
|
|
350
|
+
queryKey: ['tmSearch'] // Missing language and filter context
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Configuration
|
|
354
|
+
React Query is configured globally in App.jsx:
|
|
355
|
+
|
|
356
|
+
```jsx
|
|
357
|
+
const queryClient = new QueryClient({
|
|
358
|
+
defaultOptions: {
|
|
359
|
+
queries: {
|
|
360
|
+
staleTime: 5 * 60 * 1000, // 5 minutes - data considered fresh
|
|
361
|
+
gcTime: 10 * 60 * 1000, // 10 minutes - cache retention
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Automatic Benefits
|
|
368
|
+
React Query provides automatic:
|
|
369
|
+
- **Request deduplication** - Multiple identical requests become one
|
|
370
|
+
- **Background refetching** - Data stays fresh without user seeing loading
|
|
371
|
+
- **Caching** - Data persists across navigation and component unmounting
|
|
372
|
+
- **Error handling** - Consistent error states across components
|
|
373
|
+
- **Loading states** - Built-in loading indicators
|
|
374
|
+
|
|
375
|
+
### Filter Integration
|
|
376
|
+
For filtered data (like search), include filters in query key:
|
|
377
|
+
|
|
378
|
+
```jsx
|
|
379
|
+
// ✅ Filters in query key trigger automatic refetch
|
|
380
|
+
const queryKey = useMemo(() => [
|
|
381
|
+
'tmSearch',
|
|
382
|
+
sourceLang,
|
|
383
|
+
targetLang,
|
|
384
|
+
filters
|
|
385
|
+
], [sourceLang, targetLang, filters]);
|
|
386
|
+
|
|
387
|
+
// Debounced filter updates
|
|
388
|
+
const handleFilterChange = (column, value) => {
|
|
389
|
+
const newFilters = { ...filters, [column]: value };
|
|
390
|
+
setSelectedRows(new Set());
|
|
391
|
+
|
|
392
|
+
clearTimeout(window.tmFilterTimeout);
|
|
393
|
+
window.tmFilterTimeout = setTimeout(() => {
|
|
394
|
+
setFilters(newFilters); // This triggers React Query refetch automatically
|
|
395
|
+
}, 300);
|
|
396
|
+
};
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Legacy Patterns to Avoid
|
|
400
|
+
❌ **Don't use these patterns anymore:**
|
|
401
|
+
|
|
402
|
+
```jsx
|
|
403
|
+
// ❌ Manual useEffect data fetching
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
const fetchData = async () => {
|
|
406
|
+
try {
|
|
407
|
+
const data = await fetchApi('/api/endpoint');
|
|
408
|
+
setData(data);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
setError(err);
|
|
411
|
+
} finally {
|
|
412
|
+
setLoading(false);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
fetchData();
|
|
416
|
+
}, []);
|
|
417
|
+
|
|
418
|
+
// ❌ Manual loading/error state management
|
|
419
|
+
const [data, setData] = useState(null);
|
|
420
|
+
const [loading, setLoading] = useState(true);
|
|
421
|
+
const [error, setError] = useState(null);
|
|
422
|
+
|
|
423
|
+
// ❌ Location-based API guards
|
|
424
|
+
if (location.pathname !== '/target-path') return;
|
|
425
|
+
|
|
426
|
+
// ❌ Manual AbortController management
|
|
427
|
+
const abortControllerRef = useRef(null);
|
|
428
|
+
|
|
429
|
+
// ❌ Manual fetch guards
|
|
430
|
+
const fetchedRef = useRef(false);
|
|
431
|
+
if (fetchedRef.current) return;
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Navigation Architecture
|
|
435
|
+
|
|
436
|
+
### React Router Implementation
|
|
437
|
+
**IMPORTANT**: This project uses pure React Router navigation. No tab systems or conditional rendering based on location.
|
|
438
|
+
|
|
439
|
+
```jsx
|
|
440
|
+
// ✅ Correct: Pure React Router structure
|
|
441
|
+
function MainLayout() {
|
|
442
|
+
return (
|
|
443
|
+
<Box minH="100vh" bg="gray.50">
|
|
444
|
+
{/* Header with navigation buttons */}
|
|
445
|
+
<Box bg="white" borderBottom="1px" borderColor="gray.200">
|
|
446
|
+
<Container maxWidth="6xl" py={3}>
|
|
447
|
+
<Flex align="center" justify="space-between">
|
|
448
|
+
{/* Navigation Links */}
|
|
449
|
+
<Flex align="center" gap={4}>
|
|
450
|
+
<Box
|
|
451
|
+
cursor="pointer"
|
|
452
|
+
px={3}
|
|
453
|
+
py={1}
|
|
454
|
+
borderRadius="md"
|
|
455
|
+
bg={isActiveRoute('/') ? "blue.100" : "transparent"}
|
|
456
|
+
_hover={{ bg: isActiveRoute('/') ? "blue.100" : "gray.100" }}
|
|
457
|
+
onClick={() => navigate('/')}
|
|
458
|
+
>
|
|
459
|
+
<Text fontSize="sm" fontWeight="medium">Home</Text>
|
|
460
|
+
</Box>
|
|
461
|
+
{/* More nav items... */}
|
|
462
|
+
</Flex>
|
|
463
|
+
</Flex>
|
|
464
|
+
</Container>
|
|
465
|
+
</Box>
|
|
466
|
+
|
|
467
|
+
{/* Route-based Content */}
|
|
468
|
+
<Suspense fallback={<Spinner />}>
|
|
469
|
+
<Routes>
|
|
470
|
+
<Route path="/" element={<Welcome />} />
|
|
471
|
+
<Route path="/status" element={<Status />} />
|
|
472
|
+
<Route path="/sources" element={<Sources />} />
|
|
473
|
+
<Route path="/tm" element={<TM />} />
|
|
474
|
+
<Route path="/tm/:sourceLang/:targetLang" element={<TMDetail />} />
|
|
475
|
+
<Route path="/cart" element={<Cart />} />
|
|
476
|
+
</Routes>
|
|
477
|
+
</Suspense>
|
|
478
|
+
</Box>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// App.jsx routing setup
|
|
483
|
+
function App() {
|
|
484
|
+
return (
|
|
485
|
+
<QueryClientProvider client={queryClient}>
|
|
486
|
+
<Router>
|
|
487
|
+
<Routes>
|
|
488
|
+
<Route path="/*" element={<MainLayout />} />
|
|
489
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
490
|
+
</Routes>
|
|
491
|
+
</Router>
|
|
492
|
+
</QueryClientProvider>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Active Route Detection
|
|
498
|
+
Simple helper function for navigation state:
|
|
499
|
+
|
|
500
|
+
```jsx
|
|
501
|
+
// ✅ Clean active route detection
|
|
502
|
+
const isActiveRoute = (path) => {
|
|
503
|
+
if (path === '/') return location.pathname === '/';
|
|
504
|
+
return location.pathname.startsWith(path);
|
|
505
|
+
};
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Navigation Benefits
|
|
509
|
+
This architecture provides:
|
|
510
|
+
- **Route-based mounting** - Only current route component mounts
|
|
511
|
+
- **Proper lazy loading** - Components load only when visited
|
|
512
|
+
- **Clean URLs** - Standard web navigation patterns
|
|
513
|
+
- **Browser back/forward** - Natural browser behavior
|
|
514
|
+
- **SEO friendly** - Proper route-based structure
|
|
515
|
+
|
|
516
|
+
### Legacy Patterns to Avoid
|
|
517
|
+
❌ **Don't use these patterns anymore:**
|
|
518
|
+
|
|
519
|
+
```jsx
|
|
520
|
+
// ❌ Tab system with conditional rendering
|
|
521
|
+
<Tabs.Root value={activeTab} onValueChange={handleTabChange}>
|
|
522
|
+
<Tabs.Content value="tm">
|
|
523
|
+
{location.pathname.startsWith('/tm/') ? <TMDetail /> : <TM />}
|
|
524
|
+
</Tabs.Content>
|
|
525
|
+
</Tabs.Root>
|
|
526
|
+
|
|
527
|
+
// ❌ Complex tab change handlers
|
|
528
|
+
const handleTabChange = (details) => {
|
|
529
|
+
const value = typeof details === 'object' ? details.value : details;
|
|
530
|
+
setActiveTab(value);
|
|
531
|
+
if (value === 'home') {
|
|
532
|
+
navigate('/');
|
|
533
|
+
} else if (value === 'tm') {
|
|
534
|
+
navigate('/tm');
|
|
535
|
+
} else {
|
|
536
|
+
navigate(`/${value}`);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// ❌ Location-based conditional rendering
|
|
541
|
+
if (location.pathname === '/cart') {
|
|
542
|
+
return <CartSpecialLayout />;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ❌ Manual active tab state management
|
|
546
|
+
const [activeTab, setActiveTab] = useState(getTabFromPath(location.pathname));
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
## Session Storage for Cart Functionality
|
|
550
|
+
Store cart data grouped by keys for persistence across navigation:
|
|
551
|
+
|
|
552
|
+
```jsx
|
|
553
|
+
// ✅ Session storage cart pattern
|
|
554
|
+
const getCart = () => {
|
|
555
|
+
const cartData = sessionStorage.getItem('cartKey');
|
|
556
|
+
return cartData ? JSON.parse(cartData) : {};
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const saveCart = (cart) => {
|
|
560
|
+
sessionStorage.setItem('cartKey', JSON.stringify(cart));
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const addToCart = (items, groupKey) => {
|
|
564
|
+
const cart = getCart();
|
|
565
|
+
if (!cart[groupKey]) cart[groupKey] = [];
|
|
566
|
+
cart[groupKey].push(...items);
|
|
567
|
+
saveCart(cart);
|
|
568
|
+
};
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Architecture Summary
|
|
572
|
+
|
|
573
|
+
This application now follows modern React best practices:
|
|
574
|
+
|
|
575
|
+
### Current Stack
|
|
576
|
+
- **React 19** with functional components and hooks
|
|
577
|
+
- **Chakra UI v3** with compound component patterns
|
|
578
|
+
- **React Router v6** for navigation (no tab system)
|
|
579
|
+
- **React Query** for all data fetching and caching
|
|
580
|
+
- **TypeScript** for type safety (where applicable)
|
|
581
|
+
|
|
582
|
+
### Key Architectural Decisions
|
|
583
|
+
1. **Pure React Router Navigation** - No mixing with tab systems
|
|
584
|
+
2. **React Query for All Data** - No manual useEffect data fetching
|
|
585
|
+
3. **Route-based Component Mounting** - Components only mount when visited
|
|
586
|
+
4. **Session Storage for Cart** - Persistent cart across navigation
|
|
587
|
+
5. **Compound Components** - Chakra UI v3 patterns throughout
|
|
588
|
+
|
|
589
|
+
### Performance Characteristics
|
|
590
|
+
- **Zero duplicate API calls** - React Query deduplication
|
|
591
|
+
- **Automatic caching** - Data persists across navigation
|
|
592
|
+
- **Route-based code splitting** - Only load what's needed
|
|
593
|
+
- **Background refetching** - Data stays fresh without user awareness
|
|
594
|
+
- **Memory efficient** - Unused components unmount properly
|
|
595
|
+
|
|
596
|
+
### Development Guidelines
|
|
597
|
+
1. **Always use React Query** for API calls
|
|
598
|
+
2. **Always use Chakra UI v3 patterns** (compound components)
|
|
599
|
+
3. **Never use manual useEffect** for data fetching
|
|
600
|
+
4. **Never mix tab systems** with React Router
|
|
601
|
+
5. **Always include relevant parameters** in query keys
|
|
602
|
+
6. **Use debouncing** for filter inputs (300ms)
|
|
603
|
+
7. **Handle both old and new data formats** for backward compatibility
|
|
604
|
+
|
|
605
|
+
## API Routes Documentation
|
|
606
|
+
|
|
607
|
+
The L10n Monster server provides RESTful API endpoints for managing translation projects. All routes are prefixed with `/api`.
|
|
608
|
+
|
|
609
|
+
### Route Structure
|
|
610
|
+
|
|
611
|
+
Routes are organized by functionality in separate files under `/server/routes/`:
|
|
612
|
+
|
|
613
|
+
```
|
|
614
|
+
/server/routes/
|
|
615
|
+
├── info.js # System information endpoints
|
|
616
|
+
├── status.js # Project status endpoints
|
|
617
|
+
├── sources.js # Source content (channels, projects, resources)
|
|
618
|
+
├── tm.js # Translation memory endpoints
|
|
619
|
+
├── providers.js # Translation provider endpoints
|
|
620
|
+
└── dispatcher.js # Job creation and management
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Available Endpoints
|
|
624
|
+
|
|
625
|
+
#### System Information
|
|
626
|
+
- **GET `/api/info`** - Returns system information including:
|
|
627
|
+
- `version`: Server version
|
|
628
|
+
- `providers`: Array of available provider IDs
|
|
629
|
+
- `config`: System configuration
|
|
630
|
+
|
|
631
|
+
#### Project Status
|
|
632
|
+
- **GET `/api/status`** - Returns project status and statistics
|
|
633
|
+
|
|
634
|
+
#### Source Content
|
|
635
|
+
- **GET `/api/channel/:channelId`** - Returns channel metadata and active content statistics
|
|
636
|
+
- **Response:** `{ ts: number, store: string, projects: [...] }`
|
|
637
|
+
- **GET `/api/channel/:channelId/:prj`** - Returns project table of contents
|
|
638
|
+
- **Query Parameters:** `offset`, `limit` (pagination)
|
|
639
|
+
- **Response:** Array of resource handles for the project
|
|
640
|
+
- **GET `/api/resource/:channelId?rid=<resourceId>`** - Returns resource details with segments
|
|
641
|
+
- **Query Parameters:** `rid` (required) - Resource ID
|
|
642
|
+
- **Response:** Resource handle with segments array
|
|
643
|
+
|
|
644
|
+
#### Translation Memory
|
|
645
|
+
- **GET `/api/tm/stats`** - Returns available language pairs (sorted array)
|
|
646
|
+
- **GET `/api/tm/stats/:sourceLang/:targetLang`** - Returns TM statistics for specific language pair
|
|
647
|
+
- **Response:** Statistics object with counts, quality distribution, etc.
|
|
648
|
+
- **GET `/api/tm/lowCardinalityColumns/:sourceLang/:targetLang`** - Returns available filter options
|
|
649
|
+
- **Response:** `{ channel: [...], translationProvider: [...], ... }`
|
|
650
|
+
- **GET `/api/tm/search`** - Search translation memory entries with advanced filtering
|
|
651
|
+
- **Query Parameters:**
|
|
652
|
+
- `sourceLang`, `targetLang` (required)
|
|
653
|
+
- `page`, `limit` (pagination, default: page=1, limit=100)
|
|
654
|
+
- `guid`, `nid`, `jobGuid`, `rid`, `sid`, `channel` (exact or partial match)
|
|
655
|
+
- `nsrc`, `ntgt`, `notes`, `tconf` (text search - supports quoted exact match)
|
|
656
|
+
- `q` (quality score filtering)
|
|
657
|
+
- `translationProvider` (provider filtering)
|
|
658
|
+
- `onlyTNotes` (boolean: "1" to show only TUs with translator notes)
|
|
659
|
+
- `minTS`, `maxTS` (timestamp range filtering - milliseconds since epoch)
|
|
660
|
+
- **Text Search:** Use quotes for exact match (e.g., `nsrc="hello world"`), otherwise partial match
|
|
661
|
+
- **Date Range Filtering:** Filter by timestamp range using minTS and maxTS (milliseconds). UI displays clickable date range (M/D format without leading zeros, uses current year). Click to open popover with From/To date pickers and Apply/Clear buttons.
|
|
662
|
+
- **Response:** `{ data: [...], page: number, limit: number }`
|
|
663
|
+
- **GET `/api/tm/job/:jobGuid`** - Returns job details by GUID
|
|
664
|
+
- **Response:** Job object with metadata, TUs, timestamps, etc.
|
|
665
|
+
|
|
666
|
+
#### Translation Providers
|
|
667
|
+
- **GET `/api/providers`** - Returns detailed provider information (slower)
|
|
668
|
+
- Use `/api/info` for just provider IDs (faster)
|
|
669
|
+
|
|
670
|
+
#### Job Management
|
|
671
|
+
- **POST `/api/dispatcher/createJobs`** - Create translation jobs
|
|
672
|
+
- **Body:**
|
|
673
|
+
```json
|
|
674
|
+
{
|
|
675
|
+
"sourceLang": "en",
|
|
676
|
+
"targetLang": "es",
|
|
677
|
+
"tus": [...], // Array of translation units
|
|
678
|
+
"providerList": [...] // Array of provider IDs
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
- **Response:** Array of created job objects with properties like `jobGuid`, `tus`, `estimatedCost`, `translationProvider`
|
|
682
|
+
- **Options:** Automatically applies `skipQualityCheck: true` and `skipGroupCheck: true`
|
|
683
|
+
|
|
684
|
+
- **POST `/api/dispatcher/startJobs`** - Start created jobs
|
|
685
|
+
- **Body:**
|
|
686
|
+
```json
|
|
687
|
+
{
|
|
688
|
+
"jobs": [...], // Array of job objects from createJobs
|
|
689
|
+
"instructions": "..." // Optional job-specific instructions
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
- **Response:** Array of job status objects: `{ sourceLang, targetLang, jobGuid, translationProvider, status }`
|
|
693
|
+
|
|
694
|
+
### Route Implementation Pattern
|
|
695
|
+
|
|
696
|
+
Each route file exports a setup function:
|
|
697
|
+
|
|
698
|
+
```javascript
|
|
699
|
+
// routes/example.js
|
|
700
|
+
import { logInfo, logVerbose } from '@l10nmonster/core';
|
|
701
|
+
|
|
702
|
+
export function setupExampleRoutes(router, mm) {
|
|
703
|
+
router.get('/example', async (req, res) => {
|
|
704
|
+
logInfo`/example`;
|
|
705
|
+
try {
|
|
706
|
+
// Use MonsterManager (mm) to access core functionality
|
|
707
|
+
const result = await mm.someMethod();
|
|
708
|
+
logVerbose`Processed ${result.length} items`;
|
|
709
|
+
res.json(result);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logInfo`Error: ${error.message}`;
|
|
712
|
+
res.status(500).json({ error: error.message });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Integration in main server (index.js):
|
|
719
|
+
|
|
720
|
+
```javascript
|
|
721
|
+
import { setupExampleRoutes } from './routes/example.js';
|
|
722
|
+
|
|
723
|
+
// In API setup
|
|
724
|
+
setupExampleRoutes(apiRouter, mm);
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Error Handling Standards
|
|
728
|
+
|
|
729
|
+
- **400**: Bad Request (missing/invalid parameters)
|
|
730
|
+
- **500**: Internal Server Error (caught exceptions)
|
|
731
|
+
- Always use try/catch blocks for async operations
|
|
732
|
+
- Log requests with `logInfo` and details with `logVerbose`
|
|
733
|
+
- Return JSON error objects: `{ error: "message", details?: "..." }`
|
|
734
|
+
|
|
735
|
+
### MonsterManager Access
|
|
736
|
+
|
|
737
|
+
Routes receive the MonsterManager instance (`mm`) which provides access to:
|
|
738
|
+
- `mm.tmm` - Translation Memory Manager
|
|
739
|
+
- `mm.dispatcher` - Job Dispatcher (`createJobs`, `startJobs`)
|
|
740
|
+
- `mm.rm` - Resource Manager
|
|
741
|
+
- `mm.ops` - Operations Manager
|
|
742
|
+
- `mm.currencyFormatter` - Currency formatting utility
|
|
743
|
+
|
|
744
|
+
### Job Workflow
|
|
745
|
+
|
|
746
|
+
The job management system follows a two-step process:
|
|
747
|
+
|
|
748
|
+
1. **Create Jobs** (`/api/dispatcher/createJobs`):
|
|
749
|
+
- Takes TUs and provider list
|
|
750
|
+
- Returns job objects with estimated costs and accepted TUs
|
|
751
|
+
- Jobs are created but not yet started
|
|
752
|
+
- Providers may accept some, all, or none of the submitted TUs
|
|
753
|
+
|
|
754
|
+
2. **Start Jobs** (`/api/dispatcher/startJobs`):
|
|
755
|
+
- Takes job objects from step 1 and optional instructions
|
|
756
|
+
- Actually initiates the translation process
|
|
757
|
+
- Returns status information for tracking job progress
|
|
758
|
+
|
|
759
|
+
## Production Build Configuration
|
|
760
|
+
|
|
761
|
+
The Vite build is configured with a hybrid chunking strategy for optimal performance:
|
|
762
|
+
|
|
763
|
+
```javascript
|
|
764
|
+
// vite.config.js
|
|
765
|
+
build: {
|
|
766
|
+
rollupOptions: {
|
|
767
|
+
output: {
|
|
768
|
+
manualChunks: (id) => {
|
|
769
|
+
// All node_modules in one vendor bundle
|
|
770
|
+
if (id.includes('node_modules')) {
|
|
771
|
+
return 'vendor';
|
|
772
|
+
}
|
|
773
|
+
// Bundle utility files with index instead of creating tiny chunks
|
|
774
|
+
if (id.includes('/utils/') || id.includes('/src/components/')) {
|
|
775
|
+
return 'index';
|
|
776
|
+
}
|
|
777
|
+
// Pages remain separate for lazy loading
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Build Output:**
|
|
785
|
+
- **1 vendor bundle** (~725 KB / 210 KB gzipped) - All dependencies (React, Chakra UI, React Router, etc.)
|
|
786
|
+
- **1 index bundle** (~28 KB) - App core, utilities, and shared components
|
|
787
|
+
- **~11 page chunks** (1-17 KB each) - Individual pages for code splitting
|
|
788
|
+
|
|
789
|
+
**Benefits:**
|
|
790
|
+
- ✅ Vendor bundle rarely changes (excellent for caching)
|
|
791
|
+
- ✅ Pages lazy load independently for faster initial render
|
|
792
|
+
- ✅ Shared components bundled efficiently
|
|
793
|
+
- ✅ Fewer files than default Vite chunking (~16 vs ~40)
|
|
794
|
+
|
|
795
|
+
**Commands:**
|
|
796
|
+
```bash
|
|
797
|
+
npm run build # Build production bundle
|
|
798
|
+
npm run preview # Preview production build locally
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
## Remember
|
|
802
|
+
|
|
803
|
+
**When in doubt:**
|
|
804
|
+
1. **Check React Query docs** for data fetching patterns
|
|
805
|
+
2. **Check Chakra UI v3 docs** for component usage
|
|
806
|
+
3. **Use pure React Router** for navigation
|
|
807
|
+
4. **Never guess based on v2 Chakra UI knowledge**
|
|
808
|
+
5. **Use Chakra UI MCP tools** for accurate component examples and props
|