From 764c974600ee50bc1312948b7010022777ddcc43 Mon Sep 17 00:00:00 2001 From: v-byte-cpu <65545655+v-byte-cpu@users.noreply.github.com> Date: Mon, 8 Jun 2026 03:46:05 +0400 Subject: [PATCH] feat(validation): add structured field validation Replace validation fieldErrors/message payloads with path-based validation issues across the OpenAPI contract, Rust domain errors, mock API runtime, and UI error mapping. validation DomainError payloads now use issues[] with path/code/params instead of message plus fieldErrors. Add React Hook Form/Zod validation and localized field messages to editor and settings surfaces, with skill guidance for the form, i18n, and error patterns. --- .mockapi/behavior.md | 232 +++ .mockapi/profile.toml | 437 +++++ api/mock-server/pnpm-lock.yaml | 1530 +++++++++++++++++ api/mock-server/pnpm-workspace.yaml | 2 + api/mock-server/src/app.test.ts | 129 ++ api/mock-server/src/features/decks/service.ts | 46 +- .../src/features/folders/service.ts | 41 +- .../src/features/workspaces/service.ts | 33 +- .../generated/clear-web-api/contract/index.ts | 2 +- .../clear-web-api/contract/types.gen.ts | 39 +- .../clear-web-api/contract/zod.gen.ts | 74 +- .../src/generated/mock-admin/state/seed.ts | 2 +- api/mock-server/src/lib/errors.ts | 82 +- api/mock-server/src/lib/honoMockRuntime.ts | 86 +- api/mock-server/src/lib/validation.ts | 13 + api/mock-server/tsconfig.build.json | 12 + api/openapi/shared/components.yaml | 99 +- pnpm-lock.yaml | 35 +- rust/crates/clear-core/Cargo.toml | 4 +- rust/crates/clear-core/src/domain_error.rs | 81 +- rust/crates/clear-migrator/src/lib.rs | 2 +- rust/tauri/src/lib.rs | 13 +- skills/react-vite-structure/SKILL.md | 8 +- .../react-vite-structure/agents/openai.yaml | 4 +- .../references/error-handling.md | 112 +- .../references/feature-workflow.md | 28 +- .../references/forms-and-validation.md | 95 + .../react-vite-structure/references/i18n.md | 174 ++ .../references/structure.md | 19 +- .../references/ui-error-states.md | 4 +- ui/package.json | 2 + ui/src/core/i18n/resources/ar.ts | 23 +- ui/src/core/i18n/resources/bg.ts | 23 +- ui/src/core/i18n/resources/bs.ts | 23 +- ui/src/core/i18n/resources/ca.ts | 23 +- ui/src/core/i18n/resources/cs.ts | 23 +- ui/src/core/i18n/resources/da.ts | 23 +- ui/src/core/i18n/resources/de.ts | 23 +- ui/src/core/i18n/resources/el.ts | 23 +- ui/src/core/i18n/resources/en-US.ts | 23 +- ui/src/core/i18n/resources/es.ts | 23 +- ui/src/core/i18n/resources/et.ts | 23 +- ui/src/core/i18n/resources/fa.ts | 23 +- ui/src/core/i18n/resources/fi.ts | 23 +- ui/src/core/i18n/resources/fr.ts | 23 +- ui/src/core/i18n/resources/he.ts | 23 +- ui/src/core/i18n/resources/hr.ts | 23 +- ui/src/core/i18n/resources/hu.ts | 23 +- ui/src/core/i18n/resources/id.ts | 23 +- ui/src/core/i18n/resources/it.ts | 23 +- ui/src/core/i18n/resources/ja.ts | 23 +- ui/src/core/i18n/resources/ko.ts | 23 +- ui/src/core/i18n/resources/lt.ts | 23 +- ui/src/core/i18n/resources/lv.ts | 23 +- ui/src/core/i18n/resources/nb.ts | 23 +- ui/src/core/i18n/resources/nl.ts | 23 +- ui/src/core/i18n/resources/pl.ts | 23 +- ui/src/core/i18n/resources/pt-BR.ts | 23 +- ui/src/core/i18n/resources/ro.ts | 23 +- ui/src/core/i18n/resources/ru.ts | 23 +- ui/src/core/i18n/resources/sk.ts | 23 +- ui/src/core/i18n/resources/sl.ts | 23 +- ui/src/core/i18n/resources/sr-Latn.ts | 23 +- ui/src/core/i18n/resources/sv.ts | 23 +- ui/src/core/i18n/resources/th.ts | 23 +- ui/src/core/i18n/resources/tr.ts | 23 +- ui/src/core/i18n/resources/uk.ts | 23 +- ui/src/core/i18n/resources/vi.ts | 23 +- ui/src/core/i18n/resources/zh-Hans.ts | 23 +- ui/src/core/i18n/resources/zh-Hant.ts | 23 +- .../decks/components/DeckCard.stories.tsx | 9 - ui/src/features/decks/components/DeckCard.tsx | 2 +- .../components/DeckEditorForm.stories.tsx | 10 + .../decks/components/DeckEditorForm.test.tsx | 27 + .../decks/components/DeckEditorForm.tsx | 25 + .../decks/pages/CreatePage.stories.tsx | 10 + .../features/decks/pages/CreatePage.test.tsx | 29 +- ui/src/features/decks/pages/CreatePage.tsx | 122 +- .../features/decks/pages/EditPage.stories.tsx | 12 + ui/src/features/decks/pages/EditPage.test.tsx | 25 +- ui/src/features/decks/pages/EditPage.tsx | 133 +- .../components/FolderEditorForm.stories.tsx | 9 + .../components/FolderEditorForm.test.tsx | 23 + .../folders/components/FolderEditorForm.tsx | 20 + .../folders/pages/CreatePage.stories.tsx | 10 + .../folders/pages/CreatePage.test.tsx | 29 +- ui/src/features/folders/pages/CreatePage.tsx | 92 +- .../folders/pages/EditPage.stories.tsx | 12 + .../features/folders/pages/EditPage.test.tsx | 25 +- ui/src/features/folders/pages/EditPage.tsx | 101 +- .../components/NoteEditorForm.stories.tsx | 22 + .../notes/components/NoteEditorForm.test.tsx | 37 +- .../notes/components/NoteEditorForm.tsx | 131 +- .../notes/pages/EditorPage.stories.tsx | 81 + .../features/notes/pages/EditorPage.test.tsx | 117 +- ui/src/features/notes/pages/EditorPage.tsx | 266 ++- ui/src/features/review/pages/SummaryPage.tsx | 2 +- .../components/SettingsPageContent.tsx | 13 + .../settings/components/SettingsRows.test.tsx | 33 + .../settings/components/SettingsRows.tsx | 162 +- .../features/settings/pages/SettingsPage.tsx | 54 + .../WorkspaceEditorForm.stories.tsx | 10 + .../components/WorkspaceEditorForm.test.tsx | 27 + .../components/WorkspaceEditorForm.tsx | 25 + .../workspaces/pages/CreatePage.stories.tsx | 10 + .../workspaces/pages/CreatePage.test.tsx | 30 +- .../features/workspaces/pages/CreatePage.tsx | 125 +- .../workspaces/pages/EditPage.stories.tsx | 12 + .../workspaces/pages/EditPage.test.tsx | 25 +- ui/src/features/workspaces/pages/EditPage.tsx | 136 +- ui/src/platform/mock/mockDomainResult.ts | 28 +- .../services/decks/web/deckService.test.ts | 10 +- .../forms/FieldValidationMessages.tsx | 21 + ui/src/shared/components/forms/validation.ts | 27 + .../shared/components/layout/EditorShell.tsx | 110 +- ui/src/shared/errors/index.test.ts | 40 +- ui/src/shared/errors/index.ts | 95 +- ui/src/shared/errors/translation.test.ts | 65 +- ui/src/shared/errors/translation.ts | 119 +- .../shared/services/api/error-mapping.test.ts | 25 +- ui/src/shared/services/api/error-mapping.ts | 14 +- .../services/api/generated/clear-api/index.ts | 2 +- .../api/generated/clear-api/types.gen.ts | 39 +- .../api/generated/clear-api/zod.gen.ts | 74 +- ui/src/test/storybook/page-services.test.ts | 8 +- ui/src/test/storybook/page-services.ts | 2 +- 126 files changed, 5988 insertions(+), 1071 deletions(-) create mode 100644 .mockapi/behavior.md create mode 100644 .mockapi/profile.toml create mode 100644 api/mock-server/pnpm-lock.yaml create mode 100644 api/mock-server/pnpm-workspace.yaml create mode 100644 api/mock-server/src/lib/validation.ts create mode 100644 api/mock-server/tsconfig.build.json create mode 100644 skills/react-vite-structure/references/forms-and-validation.md create mode 100644 skills/react-vite-structure/references/i18n.md create mode 100644 ui/src/shared/components/forms/FieldValidationMessages.tsx create mode 100644 ui/src/shared/components/forms/validation.ts diff --git a/.mockapi/behavior.md b/.mockapi/behavior.md new file mode 100644 index 0000000..38a3806 --- /dev/null +++ b/.mockapi/behavior.md @@ -0,0 +1,232 @@ +# Clear Mock API Behavior + +This sidecar was reconstructed from the current `api/mock-server` implementation. +The feature services, repositories, and seed files are the behavior source of truth. + +## operation:bootstrap + +Status: inferred + +Return a desktop/web runtime profile without mutating state. + +## operation:listWorkspaces + +Status: inferred + +Return visible workspaces and the active workspace id. If the stored active workspace is missing or deleted, return `null` for the active id. + +## operation:createWorkspace + +Status: inferred + +Create a visible workspace with a counter-based `workspace` id and the current mock clock timestamp. Reject duplicate visible workspace titles with conflict. + +## operation:getActiveWorkspace + +Status: inferred + +Return the active workspace id after requiring that the referenced workspace still exists and is visible. + +## operation:setActiveWorkspace + +Status: inferred + +Require the target workspace to exist and be visible, then update the singleton active workspace slice. + +## operation:deleteWorkspace + +Status: inferred + +Soft-delete the workspace and all visible descendant folders, decks, and notes. Add trash entries with location paths for each deleted item. If the deleted workspace was active, switch to the first remaining visible workspace when one exists. + +## operation:getWorkspace + +Status: inferred + +Return the requested visible workspace or a not-found domain error. + +## operation:updateWorkspace + +Status: inferred + +Require the visible workspace, reject duplicate visible titles, then update title, description, icon, and `updatedAt` from the mock clock. + +## operation:listWorkspaceFolders + +Status: inferred + +Require the visible workspace and return visible top-level folders for that workspace. Support `sortField=title|updated` and `sortDirection=asc|desc`, defaulting to insertion order when no supported sort field is provided. + +## operation:listWorkspaceDecks + +Status: inferred + +Require the visible workspace and return visible top-level decks for that workspace. Support `sortField=dueToday|title|updated` and `sortDirection=asc|desc`, defaulting to insertion order when no supported sort field is provided. + +## operation:createFolder + +Status: inferred + +Create a visible folder under a workspace or folder parent using a counter-based `folder` id and the mock clock timestamp. Reject duplicate visible folder names within the same parent. Touch affected folder ancestors and workspace. + +## operation:deleteFolder + +Status: inferred + +Soft-delete the folder, all descendant folders, descendant decks, and descendant notes. Add trash entries with location paths for each deleted item, then touch affected ancestors and workspace. + +## operation:getFolder + +Status: inferred + +Return the requested visible folder or a not-found domain error. + +## operation:updateFolder + +Status: inferred + +Require the visible folder and next parent, reject duplicate visible names in the target parent, update folder fields and `updatedAt`, then touch old and new ancestors/workspaces as needed. + +## operation:listFolderFolders + +Status: inferred + +Require the visible folder and return visible child folders. Support `sortField=title|updated` and `sortDirection=asc|desc`. + +## operation:listFolderDecks + +Status: inferred + +Require the visible folder and return visible child decks. Support `sortField=dueToday|title|updated` and `sortDirection=asc|desc`. + +## operation:getFolderPath + +Status: inferred + +Require the visible folder and return display path segments from workspace through ancestor folders. + +## operation:createDeck + +Status: inferred + +Create a visible deck under a workspace or folder parent using a counter-based `deck` id. Initialize `dueToday`, `progress`, and `totalNotes` to zero, set timestamps from the mock clock, reject duplicate visible deck titles in the same parent, and touch ancestors/workspace. + +## operation:deleteDeck + +Status: inferred + +Soft-delete the deck and all visible notes in the deck. Add trash entries with location paths for the deck and deleted notes, then touch affected ancestors/workspace. + +## operation:getDeck + +Status: inferred + +Return the requested visible deck or a not-found domain error. + +## operation:updateDeck + +Status: inferred + +Require the visible deck and next parent, reject duplicate visible titles in the target parent, update deck fields and `updatedAt`, then touch old and new ancestors/workspaces as needed. + +## operation:listNotesByDeck + +Status: inferred + +Require the visible deck and return visible note list items for that deck. Support `sortField=title|updated` and `sortDirection=asc|desc`. + +## operation:createNote + +Status: inferred + +Require the visible deck, create a note with a counter-based `note` id and mock clock timestamps, and recompute deck stats. Basic notes produce one review card using the note id. Cloze notes derive cards from `{{cN::...}}` markers, or create a default `c1` card when no marker exists, using counter-based `card` ids. + +## operation:deleteNote + +Status: inferred + +Soft-delete the visible note, add a trash entry with its location path, and recompute the parent deck stats. + +## operation:getNote + +Status: inferred + +Return the requested visible note detail or a not-found domain error. + +## operation:updateNote + +Status: inferred + +Require the existing visible note and target visible deck, rebuild the note detail from the draft while preserving the note id, derive new cloze cards with counter-based `card` ids when needed, update timestamps, and recompute old and new deck stats. + +## operation:startReviewSession + +Status: inferred + +Require the visible deck and build a queue from visible deck notes/cards. If no cards exist, return unavailable with `empty-deck`. If due cards exist, create a due session with planned count and first due card. Otherwise create a practice session with the first card. Use a counter-based `review` id and mock clock timestamps. + +## operation:getReviewSession + +Status: inferred + +Return the requested review session or a not-found domain error. + +## operation:gradeReviewSessionCard + +Status: inferred + +Require the session and current card. Reject grading a non-current card with conflict. Apply grade progress changes to the underlying note/card, schedule next due date from the mock clock, recompute deck stats, update session duration/reviewed count, complete due sessions when planned/due cards are exhausted, and cycle practice sessions through the queue. + +## operation:getSettings + +Status: inferred + +Return the current settings singleton. + +## operation:updateSettings + +Status: inferred + +Replace the settings singleton with the request body inside a state transaction. + +## operation:getDefaultSettings + +Status: inferred + +Return the feature default settings without mutating state. + +## operation:resetSettings + +Status: inferred + +Replace the settings singleton with the feature default settings inside a state transaction. + +## operation:emptyTrash + +Status: inferred + +Permanently remove every underlying record referenced by trash items, clear the trash item list, and update `lastEmptiedAt` from the mock clock. + +## operation:getTrash + +Status: inferred + +Return the trash singleton. + +## operation:restoreTrashItem + +Status: inferred + +Require the trash item, restore the matching soft-deleted workspace, folder, deck, or note when its parent context still exists and no visible duplicate conflicts exist. Touch restored records and affected ancestors/workspaces, recompute deck stats for restored decks/notes, then remove the trash item. + +## operation:deleteTrashItem + +Status: inferred + +Require the trash item, permanently remove the underlying record, then remove the trash item. + +## operation:searchContent + +Status: inferred + +Trim the query and search visible folders, decks, and notes within the requested workspace, folder subtree, or deck scope. Match case-insensitively against folder/deck title and description, basic note title/front/back, and cloze note title/body. Sort each result group by `updatedAt` descending and include location paths. diff --git a/.mockapi/profile.toml b/.mockapi/profile.toml new file mode 100644 index 0000000..25f4ec6 --- /dev/null +++ b/.mockapi/profile.toml @@ -0,0 +1,437 @@ +schemaVersion = 1 + +[generator] +name = "mockapi" +version = "0.1.0" + +[project] +root = "." + +[project.target] +packagePath = "api/mock-server" +packageName = "@local/mock-server" +serverName = "Mock API" + +[[apis]] +name = "clear-web-api" +openapi = "api/openapi/openapi.yaml" +basePath = "/api/v1" +contractOutput = "src/generated/clear-web-api/contract" +runtimeOutput = "src/generated/clear-web-api/mock-runtime.ts" + +[[features]] +name = "bootstrap" +operations = ["bootstrap"] + +[[features]] +name = "workspaces" +stateSlices = ["workspaces", "activeWorkspace", "idCounters"] +operations = [ + "listWorkspaces", + "createWorkspace", + "getActiveWorkspace", + "setActiveWorkspace", + "deleteWorkspace", + "getWorkspace", + "updateWorkspace", +] + +[[features]] +name = "folders" +stateSlices = ["folders"] +operations = [ + "listWorkspaceFolders", + "listFolderFolders", + "createFolder", + "deleteFolder", + "getFolder", + "updateFolder", + "getFolderPath", +] + +[[features]] +name = "decks" +stateSlices = ["decks"] +operations = [ + "listWorkspaceDecks", + "listFolderDecks", + "createDeck", + "deleteDeck", + "getDeck", + "updateDeck", +] + +[[features]] +name = "notes" +stateSlices = ["notes"] +operations = [ + "listNotesByDeck", + "createNote", + "deleteNote", + "getNote", + "updateNote", +] + +[[features]] +name = "review" +stateSlices = ["reviewSessions"] +operations = [ + "startReviewSession", + "getReviewSession", + "gradeReviewSessionCard", +] + +[[features]] +name = "settings" +stateSlices = ["settings"] +operations = [ + "getSettings", + "updateSettings", + "getDefaultSettings", + "resetSettings", +] + +[[features]] +name = "trash" +stateSlices = ["trash"] +operations = [ + "emptyTrash", + "getTrash", + "restoreTrashItem", + "deleteTrashItem", +] + +[[features]] +name = "search" +operations = ["searchContent"] + +[state] +schemaVersion = 1 + +[[state.slices]] +name = "workspaces" +recordType = "Workspace" +schemaRef = "clear-web-api#/components/schemas/Workspace" +array = true +idField = "id" +softDeleteField = "deletedAt" + +[[state.slices]] +name = "activeWorkspace" +recordType = "ActiveWorkspace" +schemaRef = "clear-web-api#/components/schemas/ActiveWorkspace" +array = false + +[[state.slices]] +name = "folders" +recordType = "Folder" +schemaRef = "clear-web-api#/components/schemas/Folder" +array = true +idField = "id" +softDeleteField = "deletedAt" + +[[state.slices]] +name = "decks" +recordType = "Deck" +schemaRef = "clear-web-api#/components/schemas/Deck" +array = true +idField = "id" +softDeleteField = "deletedAt" + +[[state.slices]] +name = "notes" +recordType = "NoteDetail" +schemaRef = "clear-web-api#/components/schemas/NoteDetail" +array = true +idField = "id" +softDeleteField = "deletedAt" + +[[state.slices]] +name = "reviewSessions" +recordType = "ReviewSession" +schemaRef = "clear-web-api#/components/schemas/ReviewSession" +array = true +idField = "id" + +[[state.slices]] +name = "settings" +recordType = "Settings" +schemaRef = "clear-web-api#/components/schemas/Settings" +array = false + +[[state.slices]] +name = "trash" +recordType = "TrashState" +schemaRef = "clear-web-api#/components/schemas/TrashState" +array = false + +[[state.slices]] +name = "idCounters" +recordType = "Record" +array = false + +[[operations]] +operationId = "bootstrap" +api = "clear-web-api" +feature = "bootstrap" +method = "POST" +path = "/bootstrap" + +[[operations]] +operationId = "listWorkspaces" +api = "clear-web-api" +feature = "workspaces" +method = "GET" +path = "/workspaces" + +[[operations]] +operationId = "createWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "POST" +path = "/workspaces" + +[[operations]] +operationId = "getActiveWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "GET" +path = "/workspaces/active" + +[[operations]] +operationId = "setActiveWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "PUT" +path = "/workspaces/active" + +[[operations]] +operationId = "deleteWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "DELETE" +path = "/workspaces/{workspaceId}" + +[[operations]] +operationId = "getWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "GET" +path = "/workspaces/{workspaceId}" + +[[operations]] +operationId = "updateWorkspace" +api = "clear-web-api" +feature = "workspaces" +method = "PUT" +path = "/workspaces/{workspaceId}" + +[[operations]] +operationId = "listWorkspaceFolders" +api = "clear-web-api" +feature = "folders" +method = "GET" +path = "/workspaces/{workspaceId}/folders" + +[[operations]] +operationId = "listWorkspaceDecks" +api = "clear-web-api" +feature = "decks" +method = "GET" +path = "/workspaces/{workspaceId}/decks" + +[[operations]] +operationId = "createFolder" +api = "clear-web-api" +feature = "folders" +method = "POST" +path = "/folders" + +[[operations]] +operationId = "deleteFolder" +api = "clear-web-api" +feature = "folders" +method = "DELETE" +path = "/folders/{folderId}" + +[[operations]] +operationId = "getFolder" +api = "clear-web-api" +feature = "folders" +method = "GET" +path = "/folders/{folderId}" + +[[operations]] +operationId = "updateFolder" +api = "clear-web-api" +feature = "folders" +method = "PUT" +path = "/folders/{folderId}" + +[[operations]] +operationId = "listFolderFolders" +api = "clear-web-api" +feature = "folders" +method = "GET" +path = "/folders/{folderId}/folders" + +[[operations]] +operationId = "listFolderDecks" +api = "clear-web-api" +feature = "decks" +method = "GET" +path = "/folders/{folderId}/decks" + +[[operations]] +operationId = "getFolderPath" +api = "clear-web-api" +feature = "folders" +method = "GET" +path = "/folders/{folderId}/path" + +[[operations]] +operationId = "createDeck" +api = "clear-web-api" +feature = "decks" +method = "POST" +path = "/decks" + +[[operations]] +operationId = "deleteDeck" +api = "clear-web-api" +feature = "decks" +method = "DELETE" +path = "/decks/{deckId}" + +[[operations]] +operationId = "getDeck" +api = "clear-web-api" +feature = "decks" +method = "GET" +path = "/decks/{deckId}" + +[[operations]] +operationId = "updateDeck" +api = "clear-web-api" +feature = "decks" +method = "PUT" +path = "/decks/{deckId}" + +[[operations]] +operationId = "listNotesByDeck" +api = "clear-web-api" +feature = "notes" +method = "GET" +path = "/decks/{deckId}/notes" + +[[operations]] +operationId = "startReviewSession" +api = "clear-web-api" +feature = "review" +method = "POST" +path = "/decks/{deckId}/reviews" + +[[operations]] +operationId = "getReviewSession" +api = "clear-web-api" +feature = "review" +method = "GET" +path = "/reviews/{reviewId}" + +[[operations]] +operationId = "createNote" +api = "clear-web-api" +feature = "notes" +method = "POST" +path = "/notes" + +[[operations]] +operationId = "deleteNote" +api = "clear-web-api" +feature = "notes" +method = "DELETE" +path = "/notes/{noteId}" + +[[operations]] +operationId = "getNote" +api = "clear-web-api" +feature = "notes" +method = "GET" +path = "/notes/{noteId}" + +[[operations]] +operationId = "updateNote" +api = "clear-web-api" +feature = "notes" +method = "PUT" +path = "/notes/{noteId}" + +[[operations]] +operationId = "gradeReviewSessionCard" +api = "clear-web-api" +feature = "review" +method = "POST" +path = "/reviews/{reviewId}/cards/{cardId}/grade" + +[[operations]] +operationId = "getSettings" +api = "clear-web-api" +feature = "settings" +method = "GET" +path = "/settings" + +[[operations]] +operationId = "updateSettings" +api = "clear-web-api" +feature = "settings" +method = "PUT" +path = "/settings" + +[[operations]] +operationId = "getDefaultSettings" +api = "clear-web-api" +feature = "settings" +method = "GET" +path = "/settings/defaults" + +[[operations]] +operationId = "resetSettings" +api = "clear-web-api" +feature = "settings" +method = "POST" +path = "/settings/reset" + +[[operations]] +operationId = "emptyTrash" +api = "clear-web-api" +feature = "trash" +method = "DELETE" +path = "/trash" + +[[operations]] +operationId = "getTrash" +api = "clear-web-api" +feature = "trash" +method = "GET" +path = "/trash" + +[[operations]] +operationId = "restoreTrashItem" +api = "clear-web-api" +feature = "trash" +method = "POST" +path = "/trash/items/{itemId}/restore" + +[[operations]] +operationId = "deleteTrashItem" +api = "clear-web-api" +feature = "trash" +method = "DELETE" +path = "/trash/items/{itemId}" + +[[operations]] +operationId = "searchContent" +api = "clear-web-api" +feature = "search" +method = "POST" +path = "/search" diff --git a/api/mock-server/pnpm-lock.yaml b/api/mock-server/pnpm-lock.yaml new file mode 100644 index 0000000..3c86bba --- /dev/null +++ b/api/mock-server/pnpm-lock.yaml @@ -0,0 +1,1530 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hono/node-server': + specifier: ^1.19.7 + version: 1.19.14(hono@4.12.23) + '@msw/data': + specifier: 1.1.6 + version: 1.1.6 + hono: + specifier: ^4.12.23 + version: 4.12.23 + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@hey-api/openapi-ts': + specifier: ^0.97.1 + version: 0.97.3(typescript@6.0.3) + '@types/node': + specifier: ^20.0.0 + version: 20.19.42 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 + tsx: + specifier: ^4.22.4 + version: 4.22.4 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8(@types/node@20.19.42)(vite@8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + yaml: + specifier: ^2.8.1 + version: 2.9.0 + +packages: + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hey-api/codegen-core@0.8.2': + resolution: {integrity: sha512-R2NMf3wq97rh1mjz33WJQU8svz3F0RYUjvx/QzXucjpSqQ3O5huTdDjErG4fMxSr1X+X56NuDrqtGfHmo1TRUQ==} + engines: {node: '>=22.13.0'} + + '@hey-api/json-schema-ref-parser@1.4.2': + resolution: {integrity: sha512-ZhCFSKI2ipZHEbgmtUHdyddvRU3wJ4elgCfYUC7T7hZa4EivSrVflTQf2w+v3TuaYxR1Y2V2kq3otqTttrrK8Q==} + engines: {node: '>=22.13.0'} + + '@hey-api/openapi-ts@0.97.3': + resolution: {integrity: sha512-4sR6/E/POuy7aPZW9DDjhObzZCq7eSJWiW0+epXeKNczoTWEwdOyWFy9Ca/CnXYlZ3oJsrv0ZD0OO+YuczT7CA==} + engines: {node: '>=22.13.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' + + '@hey-api/shared@0.4.5': + resolution: {integrity: sha512-au4eHpBXAe1du0iMp6ESYuEaMS2jsoEyrbcT246btRhI9rMeQFEs7ZjtcMGXGsxhpaR38A8cPGNHx7QOrWAdMw==} + engines: {node: '>=22.13.0'} + + '@hey-api/spec-types@0.2.0': + resolution: {integrity: sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==} + + '@hey-api/types@0.1.4': + resolution: {integrity: sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@msw/data@1.1.6': + resolution: {integrity: sha512-Kp0JhuaQBgMR4C6sXdoDYUNeaF/JhhzLToumYiV9flciOwSzZe7FSnlMNcL9Yj1tm1R/3f8mktDR4M6y1xVS8Q==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.19.42': + resolution: {integrity: sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==} + + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-toolkit@1.47.0: + resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==} + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + hasBin: true + + hono@4.12.23: + resolution: {integrity: sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==} + engines: {node: '>=16.9.0'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mutative@1.3.0: + resolution: {integrity: sha512-8MJj6URmOZAV70dpFe1YnSppRTKC4DsMkXQiBDFayLcDI4ljGokHxmpqaBQuDWa4iAxWaJJ1PS8vAmbntjjKmQ==} + engines: {node: '>=14.0'} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.2: + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + + rolldown@1.0.3: + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + vite@8.0.16: + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@hey-api/codegen-core@0.8.2': + dependencies: + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + c12: 3.3.4 + color-support: 1.1.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.4.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + + '@hey-api/openapi-ts@0.97.3(typescript@6.0.3)': + dependencies: + '@hey-api/codegen-core': 0.8.2 + '@hey-api/json-schema-ref-parser': 1.4.2 + '@hey-api/shared': 0.4.5 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + '@lukeed/ms': 2.0.2 + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 14.0.3 + get-tsconfig: 4.14.0 + typescript: 6.0.3 + transitivePeerDependencies: + - magicast + + '@hey-api/shared@0.4.5': + dependencies: + '@hey-api/codegen-core': 0.8.2 + '@hey-api/json-schema-ref-parser': 1.4.2 + '@hey-api/spec-types': 0.2.0 + '@hey-api/types': 0.1.4 + ansi-colors: 4.1.3 + cross-spawn: 7.0.6 + open: 11.0.0 + semver: 7.7.4 + transitivePeerDependencies: + - magicast + + '@hey-api/spec-types@0.2.0': + dependencies: + '@hey-api/types': 0.1.4 + + '@hey-api/types@0.1.4': {} + + '@hono/node-server@1.19.14(hono@4.12.23)': + dependencies: + hono: 4.12.23 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jsdevtools/ono@7.1.3': {} + + '@lukeed/ms@2.0.2': {} + + '@msw/data@1.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + es-toolkit: 1.47.0 + mutative: 1.3.0 + outvariant: 1.4.3 + rettime: 0.11.11 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.133.0': {} + + '@rolldown/binding-android-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.3': + optional: true + + '@rolldown/binding-darwin-x64@1.0.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.3': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@20.19.42': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.8': {} + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + ansi-colors@4.1.3: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.0.8 + giget: 3.2.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + + chai@6.2.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + color-support@1.1.3: {} + + commander@14.0.3: {} + + confbox@0.2.4: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + dotenv@17.4.2: {} + + es-module-lexer@2.1.0: {} + + es-toolkit@1.47.0: {} + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + giget@3.2.0: {} + + hono@4.12.23: {} + + is-docker@3.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jiti@2.7.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mutative@1.3.0: {} + + nanoid@3.3.12: {} + + obug@2.1.2: {} + + ohash@2.0.11: {} + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + outvariant@1.4.3: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + readdirp@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rettime@0.11.11: {} + + rolldown@1.0.3: + dependencies: + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 + + run-applescript@7.1.0: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tslib@2.8.1: + optional: true + + tsx@4.22.4: + dependencies: + esbuild: 0.28.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@6.0.3: {} + + undici-types@6.21.0: {} + + vite@8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.42 + esbuild: 0.28.0 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.22.4 + yaml: 2.9.0 + + vitest@4.1.8(@types/node@20.19.42)(vite@8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.2 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.0.16(@types/node@20.19.42)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.42 + transitivePeerDependencies: + - msw + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + yaml@2.9.0: {} + + zod@4.4.3: {} diff --git a/api/mock-server/pnpm-workspace.yaml b/api/mock-server/pnpm-workspace.yaml new file mode 100644 index 0000000..5ed0b5a --- /dev/null +++ b/api/mock-server/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: true diff --git a/api/mock-server/src/app.test.ts b/api/mock-server/src/app.test.ts index f6d6884..a065c86 100644 --- a/api/mock-server/src/app.test.ts +++ b/api/mock-server/src/app.test.ts @@ -49,6 +49,135 @@ describe('mock api app', () => { }) }) + it('returns validation issue facts for invalid request bodies', async () => { + const app = await newMockApiApp() + + const response = await app.fetch( + new Request('http://localhost/api/v1/workspaces', { + body: JSON.stringify({ + description: 'Temporary research workspace.', + icon: 'bookmark', + title: '', + }), + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }), + ) + + expect(response.status).toBe(422) + await expect(json(response)).resolves.toMatchObject({ + issues: [ + { + code: 'min_length', + params: { + min: 1, + valueType: 'string', + }, + path: ['title'], + }, + ], + retryable: false, + type: 'validation', + }) + }) + + it.each([ + { + body: { + description: 'Temporary research workspace.', + icon: 'bookmark', + title: ' ', + }, + label: 'workspace create title', + method: 'POST', + path: '/api/v1/workspaces', + validationPath: ['title'], + }, + { + body: { + description: 'Updated research workspace.', + icon: 'bookmark', + title: ' ', + }, + label: 'workspace update title', + method: 'PUT', + path: '/api/v1/workspaces/independent-study', + validationPath: ['title'], + }, + { + body: { + description: 'Temporary folder.', + name: ' ', + parentId: 'independent-study', + }, + label: 'folder create name', + method: 'POST', + path: '/api/v1/folders', + validationPath: ['name'], + }, + { + body: { + description: 'Updated folder.', + name: ' ', + parentId: 'reading-notes', + }, + label: 'folder update name', + method: 'PUT', + path: '/api/v1/folders/history', + validationPath: ['name'], + }, + { + body: { + description: 'Temporary deck.', + icon: 'brain', + parentId: 'independent-study', + title: ' ', + }, + label: 'deck create title', + method: 'POST', + path: '/api/v1/decks', + validationPath: ['title'], + }, + { + body: { + description: 'Updated deck.', + icon: 'brain', + parentId: 'independent-study', + title: ' ', + }, + label: 'deck update title', + method: 'PUT', + path: '/api/v1/decks/world-history', + validationPath: ['title'], + }, + ])('rejects blank $label', async ({ body, method, path, validationPath }) => { + const app = await newMockApiApp() + + const response = await app.fetch( + new Request(`http://localhost${path}`, { + body: JSON.stringify(body), + headers: { + 'content-type': 'application/json', + }, + method, + }), + ) + + expect(response.status).toBe(422) + await expect(json(response)).resolves.toMatchObject({ + issues: [ + { + code: 'required', + path: validationPath, + }, + ], + retryable: false, + type: 'validation', + }) + }) + it('keeps seeded deck stats aligned with seeded notes', async () => { const app = await newMockApiApp() diff --git a/api/mock-server/src/features/decks/service.ts b/api/mock-server/src/features/decks/service.ts index d0c5a51..4845241 100644 --- a/api/mock-server/src/features/decks/service.ts +++ b/api/mock-server/src/features/decks/service.ts @@ -3,6 +3,7 @@ import type { DeckRecord } from '../../generated/mock-admin/contract/index.ts' import { conflict } from '../../generated/clear-web-api/mock-runtime.ts' import type { MockStateStore } from '../../lib/stateStore.ts' import { newIdAllocator } from '../../lib/ids.ts' +import { requireNonBlankText, trimOptionalText } from '../../lib/validation.ts' import type { FolderRepository } from '../folders/repository.ts' import type { LocationPathResolver } from '../location-path/resolver.ts' import type { NotesRepository } from '../notes/repository.ts' @@ -48,33 +49,38 @@ export class DeckService { } async createDeck(draft: DeckDraft): Promise { - const parent = this.resolveParent(draft.parentId) + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + title: requireNonBlankText(draft.title, 'title'), + } + const parent = this.resolveParent(normalizedDraft.parentId) const duplicate = this.decks.visible().some( - (deck) => deck.parentId === draft.parentId && deck.title === draft.title, + (deck) => deck.parentId === normalizedDraft.parentId && deck.title === normalizedDraft.title, ) if (duplicate) { - throw conflict(`Deck titled ${draft.title} already exists in this location`) + throw conflict(`Deck titled ${normalizedDraft.title} already exists in this location`) } return this.stateStore.transaction(async () => { const ids = newIdAllocator(this.stateStore.getSlice('idCounters')) const now = this.stateStore.now() const deck: DeckRecord = { - description: draft.description, + description: normalizedDraft.description, dueToday: 0, - icon: draft.icon, + icon: normalizedDraft.icon, id: ids.next('deck'), - parentId: draft.parentId, + parentId: normalizedDraft.parentId, progress: 0, - title: draft.title, + title: normalizedDraft.title, totalNotes: 0, updatedAt: now, workspaceId: parent.workspaceId, } const created = await this.decks.create(deck) - await this.touchFolderAncestors(draft.parentId, now) + await this.touchFolderAncestors(normalizedDraft.parentId, now) await this.workspaces.touch(parent.workspaceId, now) return created @@ -86,30 +92,38 @@ export class DeckService { } async updateDeck(deckId: string, draft: DeckDraft) { + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + title: requireNonBlankText(draft.title, 'title'), + } const current = this.decks.require(deckId) - const nextParent = this.resolveParent(draft.parentId) + const nextParent = this.resolveParent(normalizedDraft.parentId) const duplicate = this.decks.visible().some( - (deck) => deck.id !== deckId && deck.parentId === draft.parentId && deck.title === draft.title, + (deck) => + deck.id !== deckId && + deck.parentId === normalizedDraft.parentId && + deck.title === normalizedDraft.title, ) if (duplicate) { - throw conflict(`Deck titled ${draft.title} already exists in this location`) + throw conflict(`Deck titled ${normalizedDraft.title} already exists in this location`) } return this.stateStore.transaction(async () => { const now = this.stateStore.now() const updated = await this.decks.update(deckId, (deck) => ({ ...deck, - description: draft.description, - icon: draft.icon, - parentId: draft.parentId, - title: draft.title, + description: normalizedDraft.description, + icon: normalizedDraft.icon, + parentId: normalizedDraft.parentId, + title: normalizedDraft.title, updatedAt: now, workspaceId: nextParent.workspaceId, })) await this.touchFolderAncestors(current.parentId, now) - await this.touchFolderAncestors(draft.parentId, now) + await this.touchFolderAncestors(normalizedDraft.parentId, now) await this.workspaces.touch(current.workspaceId, now) if (nextParent.workspaceId !== current.workspaceId) { await this.workspaces.touch(nextParent.workspaceId, now) diff --git a/api/mock-server/src/features/folders/service.ts b/api/mock-server/src/features/folders/service.ts index 6ec91af..6ca7a69 100644 --- a/api/mock-server/src/features/folders/service.ts +++ b/api/mock-server/src/features/folders/service.ts @@ -3,6 +3,7 @@ import type { FolderRecord } from '../../generated/mock-admin/contract/index.ts' import { conflict } from '../../generated/clear-web-api/mock-runtime.ts' import type { MockStateStore } from '../../lib/stateStore.ts' import { newIdAllocator } from '../../lib/ids.ts' +import { requireNonBlankText, trimOptionalText } from '../../lib/validation.ts' import type { DeckRepository } from '../decks/repository.ts' import type { LocationPathResolver } from '../location-path/resolver.ts' import type { NotesRepository } from '../notes/repository.ts' @@ -48,29 +49,34 @@ export class FolderService { } async createFolder(draft: FolderDraft): Promise { - const parent = this.resolveParent(draft.parentId) + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + name: requireNonBlankText(draft.name, 'name'), + } + const parent = this.resolveParent(normalizedDraft.parentId) const duplicate = this.folders.visible().some( - (folder) => folder.parentId === draft.parentId && folder.name === draft.name, + (folder) => folder.parentId === normalizedDraft.parentId && folder.name === normalizedDraft.name, ) if (duplicate) { - throw conflict(`Folder named ${draft.name} already exists in this location`) + throw conflict(`Folder named ${normalizedDraft.name} already exists in this location`) } return this.stateStore.transaction(async () => { const ids = newIdAllocator(this.stateStore.getSlice('idCounters')) const now = this.stateStore.now() const folder: FolderRecord = { - description: draft.description, + description: normalizedDraft.description, id: ids.next('folder'), - name: draft.name, - parentId: draft.parentId, + name: normalizedDraft.name, + parentId: normalizedDraft.parentId, updatedAt: now, workspaceId: parent.workspaceId, } const created = await this.folders.create(folder) - await this.touchFolderAncestors(draft.parentId, now) + await this.touchFolderAncestors(normalizedDraft.parentId, now) await this.workspaces.touch(parent.workspaceId, now) return created @@ -82,32 +88,37 @@ export class FolderService { } async updateFolder(folderId: string, draft: FolderDraft) { + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + name: requireNonBlankText(draft.name, 'name'), + } const current = this.folders.require(folderId) - const nextParent = this.resolveParent(draft.parentId) + const nextParent = this.resolveParent(normalizedDraft.parentId) const duplicate = this.folders.visible().some( (folder) => folder.id !== folderId && - folder.parentId === draft.parentId && - folder.name === draft.name, + folder.parentId === normalizedDraft.parentId && + folder.name === normalizedDraft.name, ) if (duplicate) { - throw conflict(`Folder named ${draft.name} already exists in this location`) + throw conflict(`Folder named ${normalizedDraft.name} already exists in this location`) } return this.stateStore.transaction(async () => { const now = this.stateStore.now() const updated = await this.folders.update(folderId, (folder) => ({ ...folder, - description: draft.description, - name: draft.name, - parentId: draft.parentId, + description: normalizedDraft.description, + name: normalizedDraft.name, + parentId: normalizedDraft.parentId, updatedAt: now, workspaceId: nextParent.workspaceId, })) await this.touchFolderAncestors(current.parentId, now) - await this.touchFolderAncestors(draft.parentId, now) + await this.touchFolderAncestors(normalizedDraft.parentId, now) await this.workspaces.touch(current.workspaceId, now) if (nextParent.workspaceId !== current.workspaceId) { await this.workspaces.touch(nextParent.workspaceId, now) diff --git a/api/mock-server/src/features/workspaces/service.ts b/api/mock-server/src/features/workspaces/service.ts index b93b6f8..ba1e327 100644 --- a/api/mock-server/src/features/workspaces/service.ts +++ b/api/mock-server/src/features/workspaces/service.ts @@ -8,6 +8,7 @@ import type { WorkspaceRecord } from '../../generated/mock-admin/contract/index. import { conflict } from '../../generated/clear-web-api/mock-runtime.ts' import type { MockStateStore } from '../../lib/stateStore.ts' import { newIdAllocator } from '../../lib/ids.ts' +import { requireNonBlankText, trimOptionalText } from '../../lib/validation.ts' import type { DeckRepository } from '../decks/repository.ts' import type { FolderRepository } from '../folders/repository.ts' import type { LocationPathResolver } from '../location-path/resolver.ts' @@ -52,20 +53,27 @@ export class WorkspacesService { } async createWorkspace(draft: WorkspaceDraft): Promise { - const duplicate = this.workspaces.visible().some((workspace) => workspace.title === draft.title) + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + title: requireNonBlankText(draft.title, 'title'), + } + const duplicate = this.workspaces.visible().some( + (workspace) => workspace.title === normalizedDraft.title, + ) if (duplicate) { - throw conflict(`Workspace titled ${draft.title} already exists`) + throw conflict(`Workspace titled ${normalizedDraft.title} already exists`) } return this.stateStore.transaction(async () => { const ids = newIdAllocator(this.stateStore.getSlice('idCounters')) const now = this.stateStore.now() const workspace: WorkspaceRecord = { - description: draft.description, - icon: draft.icon, + description: normalizedDraft.description, + icon: normalizedDraft.icon, id: ids.next('workspace'), - title: draft.title, + title: normalizedDraft.title, updatedAt: now, } @@ -96,21 +104,26 @@ export class WorkspacesService { async updateWorkspace(workspaceId: string, draft: WorkspaceDraft) { this.workspaces.require(workspaceId) + const normalizedDraft = { + ...draft, + description: trimOptionalText(draft.description), + title: requireNonBlankText(draft.title, 'title'), + } const duplicate = this.workspaces.visible().some( - (workspace) => workspace.id !== workspaceId && workspace.title === draft.title, + (workspace) => workspace.id !== workspaceId && workspace.title === normalizedDraft.title, ) if (duplicate) { - throw conflict(`Workspace titled ${draft.title} already exists`) + throw conflict(`Workspace titled ${normalizedDraft.title} already exists`) } return this.stateStore.transaction(async () => { const now = this.stateStore.now() return this.workspaces.update(workspaceId, (workspace) => ({ ...workspace, - description: draft.description, - icon: draft.icon, - title: draft.title, + description: normalizedDraft.description, + icon: normalizedDraft.icon, + title: normalizedDraft.title, updatedAt: now, })) }) diff --git a/api/mock-server/src/generated/clear-web-api/contract/index.ts b/api/mock-server/src/generated/clear-web-api/contract/index.ts index 05ea943..10d1685 100644 --- a/api/mock-server/src/generated/clear-web-api/contract/index.ts +++ b/api/mock-server/src/generated/clear-web-api/contract/index.ts @@ -1,3 +1,3 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { ActiveWorkspace, ActiveWorkspace2, BasicNote, BasicNoteDraft, BasicNoteEditor, BasicReviewCard, Bootstrap, BootstrapBootstrapResult, BootstrapData, BootstrapError, BootstrapErrors, BootstrapResponse, BootstrapResponses, BootstrapResult, CardId, ClientOptions, ClozeNote, ClozeNoteCard, ClozeNoteDraft, ClozeNoteEditor, ClozeReviewCard, ComponentsCardId, ComponentsDeckId, ComponentsDeckSortField, ComponentsDomainError, ComponentsFolderId, ComponentsFolderSortField, ComponentsItemId, ComponentsNoteId, ComponentsNoteSortField, ComponentsReviewId, ComponentsSortDirection, ComponentsWorkspaceId, CreateDeckData, CreateDeckError, CreateDeckErrors, CreateDeckResponse, CreateDeckResponses, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteResponse, CreateNoteResponses, CreateWorkspaceData, CreateWorkspaceError, CreateWorkspaceErrors, CreateWorkspaceResponse, CreateWorkspaceResponses, DateTime, Deck, DeckById, DeckDraft, DeckId, DeckNotes, DeckReviews, Decks, DecksDeck, DeckSearchResult, DeckSearchResultGroup, DeckSearchScope, DeckSortField, DeckSortField2, DeleteDeckData, DeleteDeckError, DeleteDeckErrors, DeleteDeckResponse, DeleteDeckResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTrashItemData, DeleteTrashItemError, DeleteTrashItemErrors, DeleteTrashItemResponse, DeleteTrashItemResponses, DeleteWorkspaceData, DeleteWorkspaceError, DeleteWorkspaceErrors, DeleteWorkspaceResponse, DeleteWorkspaceResponses, DeleteWorkspaceResult, DomainError, DomainErrorType, DueReviewSession, DueReviewSessionStatus, EmptyTrashData, EmptyTrashError, EmptyTrashErrors, EmptyTrashResponse, EmptyTrashResponses, FieldErrors, Folder, FolderById, FolderDecks, FolderDraft, FolderFolders, FolderId, FolderPath, FolderPath2, Folders, FolderSearchResult, FolderSearchResultGroup, FolderSearchScope, FoldersFolder, FolderSortField, FolderSortField2, GetActiveWorkspaceData, GetActiveWorkspaceError, GetActiveWorkspaceErrors, GetActiveWorkspaceResponse, GetActiveWorkspaceResponses, GetDeckData, GetDeckError, GetDeckErrors, GetDeckResponse, GetDeckResponses, GetDefaultSettingsData, GetDefaultSettingsError, GetDefaultSettingsErrors, GetDefaultSettingsResponse, GetDefaultSettingsResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderPathData, GetFolderPathError, GetFolderPathErrors, GetFolderPathResponse, GetFolderPathResponses, GetFolderResponse, GetFolderResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetReviewSessionData, GetReviewSessionError, GetReviewSessionErrors, GetReviewSessionResponse, GetReviewSessionResponses, GetSettingsData, GetSettingsError, GetSettingsErrors, GetSettingsResponse, GetSettingsResponses, GetTrashData, GetTrashError, GetTrashErrors, GetTrashResponse, GetTrashResponses, GetWorkspaceData, GetWorkspaceError, GetWorkspaceErrors, GetWorkspaceResponse, GetWorkspaceResponses, GradeReviewCardRequest, GradeReviewSessionCard, GradeReviewSessionCardData, GradeReviewSessionCardError, GradeReviewSessionCardErrors, GradeReviewSessionCardResponse, GradeReviewSessionCardResponses, Id, ItemId, ListFolderDecksData, ListFolderDecksError, ListFolderDecksErrors, ListFolderDecksResponse, ListFolderDecksResponses, ListFolderFoldersData, ListFolderFoldersError, ListFolderFoldersErrors, ListFolderFoldersResponse, ListFolderFoldersResponses, ListNotesByDeckData, ListNotesByDeckError, ListNotesByDeckErrors, ListNotesByDeckResponse, ListNotesByDeckResponses, ListWorkspaceDecksData, ListWorkspaceDecksError, ListWorkspaceDecksErrors, ListWorkspaceDecksResponse, ListWorkspaceDecksResponses, ListWorkspaceFoldersData, ListWorkspaceFoldersError, ListWorkspaceFoldersErrors, ListWorkspaceFoldersResponse, ListWorkspaceFoldersResponses, ListWorkspacesData, ListWorkspacesError, ListWorkspacesErrors, ListWorkspacesResponse, ListWorkspacesResponses, NoteById, NoteDetail, NoteDraft, NoteId, NoteKind, NoteListItem, NoteRef, Notes, NoteSearchResult, NoteSearchResultGroup, NotesNoteDetail, NoteSortField, NoteSortField2, NoteStatus, PracticeReviewSession, ResetSettings, ResetSettingsData, ResetSettingsError, ResetSettingsErrors, ResetSettingsResponse, ResetSettingsResponses, RestoreTrashItem, RestoreTrashItemData, RestoreTrashItemError, RestoreTrashItemErrors, RestoreTrashItemResponse, RestoreTrashItemResponses, ReviewById, ReviewCard, ReviewCardOrNull, ReviewGrade, ReviewId, ReviewReviewCard, ReviewReviewSession, ReviewSession, ReviewStartResult, ReviewUnavailable, ReviewUnavailableReason, RuntimeFormFactor, RuntimeKind, RuntimeProfile, Search, SearchContentData, SearchContentError, SearchContentErrors, SearchContentResponse, SearchContentResponses, SearchRequest, SearchResultGroup, SearchResultLocationPath, SearchScope, SearchSearchResultGroup, SetActiveWorkspaceData, SetActiveWorkspaceError, SetActiveWorkspaceErrors, SetActiveWorkspaceRequest, SetActiveWorkspaceResponse, SetActiveWorkspaceResponses, Settings, Settings2, SettingsDefaults, SettingsNewCardsOrder, SettingsSettings, SortDirection, SortDirection2, StartReviewSessionData, StartReviewSessionError, StartReviewSessionErrors, StartReviewSessionResponse, StartReviewSessionResponses, Trash, TrashItem, TrashItemById, TrashKind, TrashState, TrashTrashState, UpdateDeckData, UpdateDeckError, UpdateDeckErrors, UpdateDeckResponse, UpdateDeckResponses, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderResponse, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponse, UpdateNoteResponses, UpdateSettingsData, UpdateSettingsError, UpdateSettingsErrors, UpdateSettingsResponse, UpdateSettingsResponses, UpdateWorkspaceData, UpdateWorkspaceError, UpdateWorkspaceErrors, UpdateWorkspaceResponse, UpdateWorkspaceResponses, VisualIconName, Workspace, WorkspaceById, WorkspaceDecks, WorkspaceDraft, WorkspaceFolders, WorkspaceId, WorkspaceListResult, Workspaces, WorkspacesActiveWorkspace, WorkspaceSearchScope, WorkspacesWorkspace } from './types.gen.ts'; +export type { ActiveWorkspace, ActiveWorkspace2, BasicNote, BasicNoteDraft, BasicNoteEditor, BasicReviewCard, Bootstrap, BootstrapBootstrapResult, BootstrapData, BootstrapError, BootstrapErrors, BootstrapResponse, BootstrapResponses, BootstrapResult, CardId, ClientOptions, ClozeNote, ClozeNoteCard, ClozeNoteDraft, ClozeNoteEditor, ClozeReviewCard, ComponentsCardId, ComponentsDeckId, ComponentsDeckSortField, ComponentsDomainError, ComponentsFolderId, ComponentsFolderSortField, ComponentsItemId, ComponentsNoteId, ComponentsNoteSortField, ComponentsReviewId, ComponentsSortDirection, ComponentsWorkspaceId, CreateDeckData, CreateDeckError, CreateDeckErrors, CreateDeckResponse, CreateDeckResponses, CreateFolderData, CreateFolderError, CreateFolderErrors, CreateFolderResponse, CreateFolderResponses, CreateNoteData, CreateNoteError, CreateNoteErrors, CreateNoteResponse, CreateNoteResponses, CreateWorkspaceData, CreateWorkspaceError, CreateWorkspaceErrors, CreateWorkspaceResponse, CreateWorkspaceResponses, DateTime, Deck, DeckById, DeckDraft, DeckId, DeckNotes, DeckReviews, Decks, DecksDeck, DeckSearchResult, DeckSearchResultGroup, DeckSearchScope, DeckSortField, DeckSortField2, DeleteDeckData, DeleteDeckError, DeleteDeckErrors, DeleteDeckResponse, DeleteDeckResponses, DeleteFolderData, DeleteFolderError, DeleteFolderErrors, DeleteFolderResponse, DeleteFolderResponses, DeleteNoteData, DeleteNoteError, DeleteNoteErrors, DeleteNoteResponse, DeleteNoteResponses, DeleteTrashItemData, DeleteTrashItemError, DeleteTrashItemErrors, DeleteTrashItemResponse, DeleteTrashItemResponses, DeleteWorkspaceData, DeleteWorkspaceError, DeleteWorkspaceErrors, DeleteWorkspaceResponse, DeleteWorkspaceResponses, DeleteWorkspaceResult, DomainError, DueReviewSession, DueReviewSessionStatus, EmptyTrashData, EmptyTrashError, EmptyTrashErrors, EmptyTrashResponse, EmptyTrashResponses, Folder, FolderById, FolderDecks, FolderDraft, FolderFolders, FolderId, FolderPath, FolderPath2, Folders, FolderSearchResult, FolderSearchResultGroup, FolderSearchScope, FoldersFolder, FolderSortField, FolderSortField2, GetActiveWorkspaceData, GetActiveWorkspaceError, GetActiveWorkspaceErrors, GetActiveWorkspaceResponse, GetActiveWorkspaceResponses, GetDeckData, GetDeckError, GetDeckErrors, GetDeckResponse, GetDeckResponses, GetDefaultSettingsData, GetDefaultSettingsError, GetDefaultSettingsErrors, GetDefaultSettingsResponse, GetDefaultSettingsResponses, GetFolderData, GetFolderError, GetFolderErrors, GetFolderPathData, GetFolderPathError, GetFolderPathErrors, GetFolderPathResponse, GetFolderPathResponses, GetFolderResponse, GetFolderResponses, GetNoteData, GetNoteError, GetNoteErrors, GetNoteResponse, GetNoteResponses, GetReviewSessionData, GetReviewSessionError, GetReviewSessionErrors, GetReviewSessionResponse, GetReviewSessionResponses, GetSettingsData, GetSettingsError, GetSettingsErrors, GetSettingsResponse, GetSettingsResponses, GetTrashData, GetTrashError, GetTrashErrors, GetTrashResponse, GetTrashResponses, GetWorkspaceData, GetWorkspaceError, GetWorkspaceErrors, GetWorkspaceResponse, GetWorkspaceResponses, GradeReviewCardRequest, GradeReviewSessionCard, GradeReviewSessionCardData, GradeReviewSessionCardError, GradeReviewSessionCardErrors, GradeReviewSessionCardResponse, GradeReviewSessionCardResponses, Id, ItemId, ListFolderDecksData, ListFolderDecksError, ListFolderDecksErrors, ListFolderDecksResponse, ListFolderDecksResponses, ListFolderFoldersData, ListFolderFoldersError, ListFolderFoldersErrors, ListFolderFoldersResponse, ListFolderFoldersResponses, ListNotesByDeckData, ListNotesByDeckError, ListNotesByDeckErrors, ListNotesByDeckResponse, ListNotesByDeckResponses, ListWorkspaceDecksData, ListWorkspaceDecksError, ListWorkspaceDecksErrors, ListWorkspaceDecksResponse, ListWorkspaceDecksResponses, ListWorkspaceFoldersData, ListWorkspaceFoldersError, ListWorkspaceFoldersErrors, ListWorkspaceFoldersResponse, ListWorkspaceFoldersResponses, ListWorkspacesData, ListWorkspacesError, ListWorkspacesErrors, ListWorkspacesResponse, ListWorkspacesResponses, MessageDomainError, NoteById, NoteDetail, NoteDraft, NoteId, NoteKind, NoteListItem, NoteRef, Notes, NoteSearchResult, NoteSearchResultGroup, NotesNoteDetail, NoteSortField, NoteSortField2, NoteStatus, PracticeReviewSession, ResetSettings, ResetSettingsData, ResetSettingsError, ResetSettingsErrors, ResetSettingsResponse, ResetSettingsResponses, RestoreTrashItem, RestoreTrashItemData, RestoreTrashItemError, RestoreTrashItemErrors, RestoreTrashItemResponse, RestoreTrashItemResponses, ReviewById, ReviewCard, ReviewCardOrNull, ReviewGrade, ReviewId, ReviewReviewCard, ReviewReviewSession, ReviewSession, ReviewStartResult, ReviewUnavailable, ReviewUnavailableReason, RuntimeFormFactor, RuntimeKind, RuntimeProfile, Search, SearchContentData, SearchContentError, SearchContentErrors, SearchContentResponse, SearchContentResponses, SearchRequest, SearchResultGroup, SearchResultLocationPath, SearchScope, SearchSearchResultGroup, SetActiveWorkspaceData, SetActiveWorkspaceError, SetActiveWorkspaceErrors, SetActiveWorkspaceRequest, SetActiveWorkspaceResponse, SetActiveWorkspaceResponses, Settings, Settings2, SettingsDefaults, SettingsNewCardsOrder, SettingsSettings, SortDirection, SortDirection2, StartReviewSessionData, StartReviewSessionError, StartReviewSessionErrors, StartReviewSessionResponse, StartReviewSessionResponses, Trash, TrashItem, TrashItemById, TrashKind, TrashState, TrashTrashState, UpdateDeckData, UpdateDeckError, UpdateDeckErrors, UpdateDeckResponse, UpdateDeckResponses, UpdateFolderData, UpdateFolderError, UpdateFolderErrors, UpdateFolderResponse, UpdateFolderResponses, UpdateNoteData, UpdateNoteError, UpdateNoteErrors, UpdateNoteResponse, UpdateNoteResponses, UpdateSettingsData, UpdateSettingsError, UpdateSettingsErrors, UpdateSettingsResponse, UpdateSettingsResponses, UpdateWorkspaceData, UpdateWorkspaceError, UpdateWorkspaceErrors, UpdateWorkspaceResponse, UpdateWorkspaceResponses, ValidationDomainError, ValidationIssue, VisualIconName, Workspace, WorkspaceById, WorkspaceDecks, WorkspaceDraft, WorkspaceFolders, WorkspaceId, WorkspaceListResult, Workspaces, WorkspacesActiveWorkspace, WorkspaceSearchScope, WorkspacesWorkspace } from './types.gen.ts'; diff --git a/api/mock-server/src/generated/clear-web-api/contract/types.gen.ts b/api/mock-server/src/generated/clear-web-api/contract/types.gen.ts index 8146d74..740a679 100644 --- a/api/mock-server/src/generated/clear-web-api/contract/types.gen.ts +++ b/api/mock-server/src/generated/clear-web-api/contract/types.gen.ts @@ -449,29 +449,42 @@ export type DateTime = string; export type DeckSortField = 'dueToday' | 'title' | 'updated'; -export type ComponentsDomainError = { - type: DomainErrorType; +export type ComponentsDomainError = ({ + type: 'validation'; +} & ValidationDomainError) | ({ + type: 'conflict' | 'forbidden' | 'not_found' | 'offline' | 'timeout' | 'unauthorized' | 'unexpected' | 'unavailable'; +} & MessageDomainError); + +export type FolderSortField = 'title' | 'updated'; + +export type Id = string; + +export type MessageDomainError = { + type: 'conflict' | 'forbidden' | 'not_found' | 'offline' | 'timeout' | 'unauthorized' | 'unexpected' | 'unavailable'; message: string; retryable: boolean; - fieldErrors?: FieldErrors; entity?: string; entityId?: string; }; -export type DomainErrorType = 'conflict' | 'forbidden' | 'not_found' | 'offline' | 'timeout' | 'unauthorized' | 'unexpected' | 'unavailable' | 'validation'; - -export type FieldErrors = { - [key: string]: Array; -}; - -export type FolderSortField = 'title' | 'updated'; - -export type Id = string; - export type NoteSortField = 'title' | 'updated'; export type SortDirection = 'asc' | 'desc'; +export type ValidationDomainError = { + type: 'validation'; + issues: Array; + retryable: false; +}; + +export type ValidationIssue = { + path?: Array; + code: string; + params?: { + [key: string]: unknown; + }; +}; + /** * Lucide icon name used by the UI visual picker. */ diff --git a/api/mock-server/src/generated/clear-web-api/contract/zod.gen.ts b/api/mock-server/src/generated/clear-web-api/contract/zod.gen.ts index c184326..beb0c56 100644 --- a/api/mock-server/src/generated/clear-web-api/contract/zod.gen.ts +++ b/api/mock-server/src/generated/clear-web-api/contract/zod.gen.ts @@ -142,31 +142,6 @@ export const zDeckSortField = z.enum([ 'updated' ]); -export const zDomainErrorType = z.enum([ - 'conflict', - 'forbidden', - 'not_found', - 'offline', - 'timeout', - 'unauthorized', - 'unexpected', - 'unavailable', - 'validation' -]); - -export const zFieldErrors = z.record(z.string(), z.array(z.string())); - -export const zComponentsDomainError = z.object({ - type: zDomainErrorType, - message: z.string().min(1), - retryable: z.boolean(), - fieldErrors: zFieldErrors.optional(), - entity: z.string().optional(), - entityId: z.string().optional() -}); - -export const zDomainError = zComponentsDomainError; - export const zFolderSortField = z.enum(['title', 'updated']); export const zId = z.string().min(1); @@ -413,10 +388,59 @@ export const zSetActiveWorkspaceRequest = z.object({ workspaceId: zId }); +export const zMessageDomainError = z.object({ + type: z.enum([ + 'conflict', + 'forbidden', + 'not_found', + 'offline', + 'timeout', + 'unauthorized', + 'unexpected', + 'unavailable' + ]), + message: z.string().min(1), + retryable: z.boolean(), + entity: z.string().optional(), + entityId: z.string().optional() +}); + export const zNoteSortField = z.enum(['title', 'updated']); export const zSortDirection = z.enum(['asc', 'desc']); +export const zValidationIssue = z.object({ + path: z.array(z.string()).optional(), + code: z.string().min(1), + params: z.record(z.string(), z.unknown()).optional() +}); + +export const zValidationDomainError = z.object({ + type: z.enum(['validation']), + issues: z.array(zValidationIssue), + retryable: z.literal(false) +}); + +export const zComponentsDomainError = z.union([ + z.object({ + type: z.literal('validation') + }).and(zValidationDomainError), + z.object({ + type: z.union([ + z.literal('conflict'), + z.literal('forbidden'), + z.literal('not_found'), + z.literal('offline'), + z.literal('timeout'), + z.literal('unauthorized'), + z.literal('unexpected'), + z.literal('unavailable') + ]) + }).and(zMessageDomainError) +]); + +export const zDomainError = zComponentsDomainError; + /** * Lucide icon name used by the UI visual picker. */ diff --git a/api/mock-server/src/generated/mock-admin/state/seed.ts b/api/mock-server/src/generated/mock-admin/state/seed.ts index 41cdfc0..30637f9 100644 --- a/api/mock-server/src/generated/mock-admin/state/seed.ts +++ b/api/mock-server/src/generated/mock-admin/state/seed.ts @@ -15,7 +15,7 @@ export type SeedContext = { seedNow: string } -const seedNow = "2026-05-27T16:31:00.127Z" +const seedNow = "2026-06-07T18:58:06.242Z" const dayMs = 24 * 60 * 60 * 1000 const newSeedContext = (): SeedContext => ({ diff --git a/api/mock-server/src/lib/errors.ts b/api/mock-server/src/lib/errors.ts index c3570be..4f565f8 100644 --- a/api/mock-server/src/lib/errors.ts +++ b/api/mock-server/src/lib/errors.ts @@ -1,41 +1,85 @@ import { z } from 'zod' -export const zMockErrorBody = z.object({ - error: z.object({ - code: z.string(), - message: z.string(), - }), +export const zValidationIssue = z.object({ + path: z.array(z.string()).optional(), + code: z.string().min(1), + params: z.record(z.string(), z.unknown()).optional(), }) +const zMessageDomainError = z.object({ + type: z.enum([ + 'conflict', + 'forbidden', + 'not_found', + 'offline', + 'timeout', + 'unauthorized', + 'unexpected', + 'unavailable', + ]), + message: z.string().min(1), + retryable: z.boolean(), + entity: z.string().optional(), + entityId: z.string().optional(), +}) + +const zValidationDomainError = z.object({ + type: z.literal('validation'), + retryable: z.literal(false), + issues: z.array(zValidationIssue), +}) + +export const zMockErrorBody = z.discriminatedUnion('type', [ + zMessageDomainError, + zValidationDomainError, +]) + export type MockErrorBody = z.infer +export type ValidationIssue = z.infer export class MockHttpError extends Error { readonly body: MockErrorBody readonly status: number - constructor(status: number, code: string, message: string) { - super(message) + constructor(status: number, body: MockErrorBody) { + super(body.type === 'validation' ? 'Validation failed' : body.message) this.status = status - this.body = { - error: { - code, - message, - }, - } + this.body = body } } +const messageError = ( + status: number, + type: Exclude, + message: string, + retryable = false, + metadata?: { entity?: string; entityId?: string }, +) => + new MockHttpError(status, { + type, + message, + retryable, + ...metadata, + }) + export const badRequest = (message: string) => - new MockHttpError(400, 'bad_request', message) + messageError(400, 'unexpected', message) -export const validationError = (message: string) => - new MockHttpError(422, 'validation_error', message) +export const validationError = (issues: ValidationIssue[]) => + new MockHttpError(422, { + type: 'validation', + retryable: false, + issues, + }) export const notFound = (resource: string, id: string) => - new MockHttpError(404, 'not_found', `${resource} ${id} was not found`) + messageError(404, 'not_found', `${resource} ${id} was not found`, false, { + entity: resource, + entityId: id, + }) export const conflict = (message: string) => - new MockHttpError(409, 'conflict', message) + messageError(409, 'conflict', message) export const unexpected = (message = 'Unexpected mock server error') => - new MockHttpError(500, 'unexpected', message) + messageError(500, 'unexpected', message) diff --git a/api/mock-server/src/lib/honoMockRuntime.ts b/api/mock-server/src/lib/honoMockRuntime.ts index 0ec7ffe..e21d137 100644 --- a/api/mock-server/src/lib/honoMockRuntime.ts +++ b/api/mock-server/src/lib/honoMockRuntime.ts @@ -1,7 +1,13 @@ import type { Hono } from 'hono' -import type { z } from 'zod' +import { ZodError, type ZodIssue, type z } from 'zod' -import { badRequest, MockHttpError, unexpected } from './errors.ts' +import { + badRequest, + MockHttpError, + unexpected, + validationError, + type ValidationIssue, +} from './errors.ts' import { newMockRequestContext, type MockRequestContext } from './requestContext.ts' type RuntimeRoute = { @@ -52,7 +58,81 @@ const parseBody = async ( throw badRequest('Request body must be valid JSON.') }) - return schema ? schema.parse(rawBody) : rawBody + if (!schema) { + return rawBody + } + + try { + return schema.parse(rawBody) + } catch (error) { + if (error instanceof ZodError) { + throw validationError(error.issues.map(toValidationIssue)) + } + + throw error + } +} + +const toValidationIssue = (issue: ZodIssue): ValidationIssue => { + const rawIssue = issue as unknown as Record + const path = issue.path.map(String) + const params = readValidationParams(rawIssue) + const validationIssue: ValidationIssue = { + code: toValidationIssueCode(issue, rawIssue), + } + + if (path.length > 0) { + validationIssue.path = path + } + if (Object.keys(params).length > 0) { + validationIssue.params = params + } + + return validationIssue +} + +const toValidationIssueCode = ( + issue: ZodIssue, + rawIssue: Record, +): string => { + switch (issue.code) { + case 'invalid_type': + return typeof issue.message === 'string' && issue.message.includes('received undefined') + ? 'required' + : 'invalid' + case 'too_small': + return rawIssue.origin === 'string' ? 'min_length' : 'minimum' + case 'too_big': + return rawIssue.origin === 'string' ? 'max_length' : 'maximum' + case 'invalid_value': + return 'invalid_value' + case 'invalid_format': + return 'invalid_format' + default: + return issue.code + } +} + +const readValidationParams = (issue: Record) => { + const params: Record = {} + + if (typeof issue.origin === 'string') { + params.valueType = issue.origin + } + if (issue.minimum !== undefined) { + params.min = issue.minimum + } + if (issue.maximum !== undefined) { + params.max = issue.maximum + } + if (issue.format !== undefined) { + params.format = issue.format + } + if (issue.values !== undefined) { + params.values = issue.values + } + + return params } export const registerGeneratedMockRoutes = ( diff --git a/api/mock-server/src/lib/validation.ts b/api/mock-server/src/lib/validation.ts new file mode 100644 index 0000000..0087a4f --- /dev/null +++ b/api/mock-server/src/lib/validation.ts @@ -0,0 +1,13 @@ +import { validationError } from './errors.ts' + +export const trimOptionalText = (value: string): string => value.trim() + +export const requireNonBlankText = (value: string, path: string): string => { + const trimmed = value.trim() + + if (trimmed.length === 0) { + throw validationError([{ code: 'required', path: [path] }]) + } + + return trimmed +} diff --git a/api/mock-server/tsconfig.build.json b/api/mock-server/tsconfig.build.json new file mode 100644 index 0000000..8a5e12a --- /dev/null +++ b/api/mock-server/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowImportingTsExtensions": false, + "declaration": true, + "emitDeclarationOnly": false, + "noEmit": false, + "outDir": "dist", + "sourceMap": true + }, + "exclude": ["dist", "node_modules"] +} diff --git a/api/openapi/shared/components.yaml b/api/openapi/shared/components.yaml index 8ac36b5..caaad46 100644 --- a/api/openapi/shared/components.yaml +++ b/api/openapi/shared/components.yaml @@ -112,11 +112,11 @@ components: validation: value: type: validation - message: Invalid input. retryable: false - fieldErrors: - title: - - Title is required. + issues: + - path: + - title + code: required Unauthorized: description: Authentication is required. @@ -235,17 +235,56 @@ components: - validation example: validation - FieldErrors: + ValidationIssue: type: object - additionalProperties: - type: array - items: + additionalProperties: false + required: + - code + properties: + path: + type: array + items: + type: string + code: type: string + minLength: 1 + params: + type: object + additionalProperties: true example: - title: - - Title is required. + path: + - title + code: required - DomainError: + ValidationDomainError: + type: object + additionalProperties: false + required: + - issues + - retryable + - type + properties: + type: + type: string + enum: + - validation + issues: + type: array + items: + $ref: '#/components/schemas/ValidationIssue' + retryable: + type: boolean + enum: + - false + example: + type: validation + retryable: false + issues: + - path: + - title + code: required + + MessageDomainError: type: object additionalProperties: false required: @@ -254,25 +293,49 @@ components: - type properties: type: - $ref: '#/components/schemas/DomainErrorType' + type: string + enum: + - conflict + - forbidden + - not_found + - offline + - timeout + - unauthorized + - unexpected + - unavailable message: type: string minLength: 1 retryable: type: boolean - fieldErrors: - $ref: '#/components/schemas/FieldErrors' entity: type: string entityId: type: string + + DomainError: + oneOf: + - $ref: '#/components/schemas/ValidationDomainError' + - $ref: '#/components/schemas/MessageDomainError' + discriminator: + propertyName: type + mapping: + conflict: '#/components/schemas/MessageDomainError' + forbidden: '#/components/schemas/MessageDomainError' + not_found: '#/components/schemas/MessageDomainError' + offline: '#/components/schemas/MessageDomainError' + timeout: '#/components/schemas/MessageDomainError' + unauthorized: '#/components/schemas/MessageDomainError' + unexpected: '#/components/schemas/MessageDomainError' + unavailable: '#/components/schemas/MessageDomainError' + validation: '#/components/schemas/ValidationDomainError' examples: - type: validation - message: Invalid input. retryable: false - fieldErrors: - title: - - Title is required. + issues: + - path: + - title + code: required - type: not_found message: Note not found. retryable: false diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60cf500..0a5ed76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: ui: dependencies: + '@hookform/resolvers': + specifier: 5.4.0 + version: 5.4.0(react-hook-form@7.77.0(react@19.2.7)) '@local/mock-server': specifier: workspace:* version: link:../api/mock-server @@ -111,6 +114,9 @@ importers: react-dom: specifier: ^19.2.7 version: 19.2.7(react@19.2.7) + react-hook-form: + specifier: 7.77.0 + version: 7.77.0(react@19.2.7) react-i18next: specifier: ^17.0.8 version: 17.0.8(i18next@26.3.1(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3) @@ -968,6 +974,11 @@ packages: peerDependencies: hono: ^4 + '@hookform/resolvers@5.4.0': + resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1769,6 +1780,9 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@storybook/addon-a11y@10.4.2': resolution: {integrity: sha512-9Bm41peswHVPXm8iDBEA/sCXx+MUQV8Q10yFgNC2zTbtxKamMDb8HZCPIRDxvjBQ5v13eaXA2yZGTwQ+D8S6+g==} peerDependencies: @@ -3966,6 +3980,12 @@ packages: peerDependencies: react: ^19.2.7 + react-hook-form@7.77.0: + resolution: {integrity: sha512-Sslh9YDYc0GDlWT/lxasnIduNo4v3yyvqRGvmGKUre5AFjDs/HV9/OafHGD8d+sB2yoL4UIL9L8X9i0WlZZebg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@17.0.8: resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} peerDependencies: @@ -5373,6 +5393,11 @@ snapshots: dependencies: hono: 4.12.23 + '@hookform/resolvers@5.4.0(react-hook-form@7.77.0(react@19.2.7))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.77.0(react@19.2.7) + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -6036,6 +6061,8 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + '@storybook/addon-a11y@10.4.2(storybook@10.4.2(@testing-library/dom@10.4.1)(@types/react@19.2.16)(prettier@3.8.3)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))': dependencies: '@storybook/global': 5.0.0 @@ -6753,9 +6780,9 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@20.19.41)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@20.19.41)(typescript@6.0.3))(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.8(@types/node@25.6.0)(@vitest/browser-playwright@4.1.8)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(msw@2.14.6(@types/node@25.6.0)(typescript@6.0.3))(vite@8.0.16(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.8(msw@2.14.6(@types/node@20.19.41)(typescript@6.0.3))(vite@8.0.16(@types/node@20.19.41)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.8) + '@vitest/browser': 4.1.8(msw@2.14.6(@types/node@25.6.0)(typescript@6.0.3))(vite@8.0.16(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))(vitest@4.1.8) '@vitest/expect@3.2.4': dependencies: @@ -8656,6 +8683,10 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-hook-form@7.77.0(react@19.2.7): + dependencies: + react: 19.2.7 + react-i18next@17.0.8(i18next@26.3.1(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 diff --git a/rust/crates/clear-core/Cargo.toml b/rust/crates/clear-core/Cargo.toml index 32be535..a2db72f 100644 --- a/rust/crates/clear-core/Cargo.toml +++ b/rust/crates/clear-core/Cargo.toml @@ -6,7 +6,5 @@ rust-version.workspace = true [dependencies] serde = { version = "1.0.228", features = ["derive"] } -time = { version = "0.3", features = ["formatting"] } - -[dev-dependencies] serde_json = "1.0.145" +time = { version = "0.3", features = ["formatting"] } diff --git a/rust/crates/clear-core/src/domain_error.rs b/rust/crates/clear-core/src/domain_error.rs index 8ff9fa8..281c353 100644 --- a/rust/crates/clear-core/src/domain_error.rs +++ b/rust/crates/clear-core/src/domain_error.rs @@ -2,7 +2,43 @@ use std::collections::BTreeMap; use serde::Serialize; -pub type FieldErrors = BTreeMap>; +pub type ValidationIssueParams = BTreeMap; + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValidationIssue { + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option>, + pub code: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +impl ValidationIssue { + pub fn new(code: impl Into) -> Self { + Self { + path: None, + code: code.into(), + params: None, + } + } + + pub fn at_path( + path: impl IntoIterator>, + code: impl Into, + ) -> Self { + Self { + path: Some(path.into_iter().map(Into::into).collect()), + code: code.into(), + params: None, + } + } + + pub fn with_params(mut self, params: ValidationIssueParams) -> Self { + self.params = Some(params); + self + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] @@ -18,15 +54,16 @@ pub enum DomainErrorType { Unexpected, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct DomainError { #[serde(rename = "type")] pub error_type: DomainErrorType, - pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, pub retryable: bool, #[serde(skip_serializing_if = "Option::is_none")] - pub field_errors: Option, + pub issues: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub entity: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -34,12 +71,12 @@ pub struct DomainError { } impl DomainError { - pub fn validation(message: impl Into, field_errors: FieldErrors) -> Self { + pub fn validation(issues: Vec) -> Self { Self { error_type: DomainErrorType::Validation, - message: message.into(), + message: None, retryable: false, - field_errors: Some(field_errors), + issues: Some(issues), entity: None, entity_id: None, } @@ -60,9 +97,9 @@ impl DomainError { ) -> Self { Self { error_type: DomainErrorType::NotFound, - message: message.into(), + message: Some(message.into()), retryable: false, - field_errors: None, + issues: None, entity, entity_id, } @@ -95,9 +132,9 @@ impl DomainError { ) -> Self { Self { error_type, - message: message.into(), + message: Some(message.into()), retryable, - field_errors: None, + issues: None, entity: None, entity_id: None, } @@ -108,19 +145,11 @@ impl DomainError { mod tests { use serde_json::json; - use super::{DomainError, DomainErrorType}; + use super::{DomainError, DomainErrorType, ValidationIssue}; #[test] fn serializes_validation_errors_using_the_ui_contract_shape() { - let error = DomainError::validation( - "Invalid input.", - [( - String::from("email"), - vec![String::from("Email is required.")], - )] - .into_iter() - .collect(), - ); + let error = DomainError::validation(vec![ValidationIssue::at_path(["email"], "required")]); let value = serde_json::to_value(error).expect("serialize domain error"); @@ -128,11 +157,13 @@ mod tests { value, json!({ "type": "validation", - "message": "Invalid input.", "retryable": false, - "fieldErrors": { - "email": ["Email is required."] - } + "issues": [ + { + "path": ["email"], + "code": "required" + } + ] }) ); } diff --git a/rust/crates/clear-migrator/src/lib.rs b/rust/crates/clear-migrator/src/lib.rs index b3e2c82..c953df4 100644 --- a/rust/crates/clear-migrator/src/lib.rs +++ b/rust/crates/clear-migrator/src/lib.rs @@ -4,7 +4,7 @@ use std::{ path::{Path, PathBuf}, }; -use rusqlite::{params, Connection}; +use rusqlite::{Connection, params}; use thiserror::Error; const MIGRATION_TABLE: &str = "__clear_migrations"; diff --git a/rust/tauri/src/lib.rs b/rust/tauri/src/lib.rs index 0c3acb4..01f4c30 100644 --- a/rust/tauri/src/lib.rs +++ b/rust/tauri/src/lib.rs @@ -2,7 +2,7 @@ use std::{fs, path::PathBuf}; use clear_core::domain_error::DomainError; use clear_migrator::{ - apply_sqlite_migrations, EmbeddedMigrationSource, MigrationError, MigrationReport, + EmbeddedMigrationSource, MigrationError, MigrationReport, apply_sqlite_migrations, }; use serde::Serialize; use tauri::{AppHandle, Manager}; @@ -160,8 +160,8 @@ mod tests { use clear_migrator::MigrationError; use super::{ - current_runtime_profile, map_migration_error, runtime_form_factor_for_mobile_cfg, - RuntimeFormFactor, RuntimeKind, + RuntimeFormFactor, RuntimeKind, current_runtime_profile, map_migration_error, + runtime_form_factor_for_mobile_cfg, }; #[test] @@ -174,7 +174,12 @@ mod tests { assert_eq!(error.error_type, DomainErrorType::Conflict); assert!(!error.retryable); - assert!(error.message.contains("Duplicate migration id 1")); + assert!( + error + .message + .as_deref() + .is_some_and(|message| message.contains("Duplicate migration id 1")) + ); } #[test] diff --git a/skills/react-vite-structure/SKILL.md b/skills/react-vite-structure/SKILL.md index 2c6e176..2f63c50 100644 --- a/skills/react-vite-structure/SKILL.md +++ b/skills/react-vite-structure/SKILL.md @@ -1,6 +1,6 @@ --- name: react-vite-structure -description: React + Vite + TypeScript project structure guidance for creating, organizing, refactoring, or migrating frontend apps. Use when Codex needs folder architecture, feature-module boundaries, TanStack Router file-based route modules, hierarchical route-facing pages, platform-dependent service implementations, dependency injection, typed Result/DomainError handling, UI loading/error/empty states, web/Tauri/mock services, composite/routed services, generated code placement, OpenAPI codegen organization, shadcn/ui and Tailwind placement, Storybook stories and UI testing strategy, shared/core conventions, path aliases, naming rules, common types, feature scaffolding workflow, testing strategy, state management choices, code quality tooling, documentation templates, or migration checklists for React/Vite projects. +description: React + Vite + TypeScript project structure guidance for creating, organizing, refactoring, or migrating frontend apps. Use when Codex needs folder architecture, feature-module boundaries, TanStack Router file-based route modules, hierarchical route-facing pages, platform-dependent service implementations, dependency injection, typed Result/DomainError handling, form ownership and validation with React Hook Form/Zod, UI loading/error/empty states, web/Tauri/mock services, composite/routed services, generated code placement, OpenAPI codegen organization, shadcn/ui and Tailwind placement, Storybook stories and UI testing strategy, i18n provider/resource architecture, typed translation keys, locale handling, RTL/document metadata, shared/core conventions, path aliases, naming rules, common types, feature scaffolding workflow, testing strategy, state management choices, code quality tooling, documentation templates, or migration checklists for React/Vite projects. --- # React Vite Structure @@ -22,6 +22,8 @@ Use this skill to organize React + Vite + TypeScript apps around feature modules - Read `references/generated-code.md` for generated code placement, OpenAPI codegen output, generated DTO usage, API adapters, and generated TanStack Query hooks. - Read `references/platform-services-and-di.md` for platform-dependent services, feature service contracts, `platform/services`, composite services, object-scoped backend routing, and DI wiring. - Read `references/error-handling.md` for `Result`, `DomainResult`, `DomainErrorType`, typed domain errors, API/Tauri error mapping, and React Query unwrapping. +- Read `references/i18n.md` when adding or changing localization, typed translation resources, language selection, locale metadata, RTL handling, translated shared helpers, or Storybook i18n wiring. +- Read `references/forms-and-validation.md` when adding or changing typed forms, React Hook Form/Zod validation, form ownership boundaries, validation-message mapping, or form accessibility/tests. - Read `references/ui-error-states.md` for UI loading, empty, query error, partial-data, retry, mutation pending, and mutation error rendering policy. - Read `references/typescript-and-naming.md` for `tsconfig.json`, `vite.config.ts` path aliases, naming conventions, component/hook/service/type examples, and common shared types. - Read `references/feature-workflow.md` when adding or scaffolding a new feature module, including types, service, React Query hooks, components, pages, and public exports. @@ -39,6 +41,8 @@ Use this skill to organize React + Vite + TypeScript apps around feature modules - For every user-visible async operation, account for loading, no-data query error, stale-data query error, mutation pending, and mutation error states when those states are possible. Do not leave a query or mutation failure silent in the UI. - For new projects, use `pnpm` by default. For existing projects, follow the detected lockfile/package manager unless the user asks to migrate. - For new UI projects, prefer Tailwind via the Vite plugin and shadcn/ui primitives under `src/shared/components/ui`. For existing projects, keep the coherent detected UI stack unless the user asks to migrate. +- When localization is needed and no coherent existing stack exists, prefer `i18next + react-i18next` with typed selector resources under `src/core/i18n`. +- When form validation is needed and no coherent existing stack exists, prefer `react-hook-form` with `zod` and `@hookform/resolvers/zod`; keep simple one-field search/filter controls in local state when schema validation adds no value. - Keep `index.html` in the Vite app root next to `package.json` and `vite.config.ts`; use `ui/index.html` only when `ui/` is itself the app/package root. - Use `public/` only for static files served as-is by stable URL. Use `src/assets/` for imported or bundled images, icons, fonts, and global styles. - Use strict TypeScript and Vite path aliases that mirror `src/features`, `src/shared`, `src/core`, and `src/assets`. @@ -57,6 +61,8 @@ Use this skill to organize React + Vite + TypeScript apps around feature modules - Avoid redundant domain prefixes inside a feature. Prefer route-facing page basenames such as `ListPage.tsx`, `CreatePage.tsx`, `DetailPage.tsx`, `EditPage.tsx`, singleton `Page.tsx`, or workflow-step pages such as `SessionPage.tsx`; add aliases or prefixes only at public export boundaries or when ambiguity is real. - Keep platform-dependent service implementations in `src/platform/services/[feature]/{web,tauri,mock,composite}`. - Keep `src/core/services` for DI composition and React service providers only; do not put concrete feature service implementations there. +- Keep app-wide i18n infrastructure in `src/core/i18n`; features consume translations but do not own i18n initialization, public locale lists, fallback behavior, or document language/direction metadata. - Keep `features/[feature]/services/[feature]Service.ts` interface-only. Features must not import `apiClient`, `@tauri-apps/api`, `platform/*`, or concrete service implementations. - Use `src/platform/services/[feature]/web/[feature]Service.ts` as the default concrete implementation, even before Tauri/mock/composite implementations exist. - Use `DomainResult` for async backend/runtime service contract methods. Platform services return `ok`/`err`; React Query hooks call `unwrapDomainResult` so `query.data` is `T` and `query.error` is `DomainError`. +- Keep platform services, generated API code, backend contracts, route paths, fixture seeds, and internal test strings language-agnostic unless they are visible UI. diff --git a/skills/react-vite-structure/agents/openai.yaml b/skills/react-vite-structure/agents/openai.yaml index 223c295..8c814b9 100644 --- a/skills/react-vite-structure/agents/openai.yaml +++ b/skills/react-vite-structure/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "React Vite Structure" - short_description: "React/Vite architecture, UI states, DI, tests" - default_prompt: "Use $react-vite-structure to organize a React + Vite TypeScript app with feature modules, routing, Storybook stories, service DI, generated code, typed error handling, and UI loading/error states." + short_description: "React/Vite architecture, forms, i18n, DI, tests" + default_prompt: "Use $react-vite-structure to organize a React + Vite TypeScript app with feature modules, routing, Storybook stories, service DI, generated code, typed error handling, React Hook Form/Zod form validation, i18n provider/resource architecture, and UI loading/error states." diff --git a/skills/react-vite-structure/references/error-handling.md b/skills/react-vite-structure/references/error-handling.md index 6413fd7..4d2eb55 100644 --- a/skills/react-vite-structure/references/error-handling.md +++ b/skills/react-vite-structure/references/error-handling.md @@ -10,7 +10,7 @@ For user-facing loading, empty, partial-data, and error rendering policy, read ` - Result - Domain Errors - Domain Result -- User Messages +- User-Facing Copy - API Error Mapping - Tauri Error Mapping - React Query Boundary @@ -25,7 +25,7 @@ src/shared/errors/ result.ts # Generic Result domain-error.ts # DomainErrorType, DomainError, factories domain-result.ts # DomainResult - messages.ts # Generic user-facing fallback messages + translation.ts # Optional i18n summary translator for shared UI surfaces index.ts src/shared/services/api/ @@ -85,17 +85,24 @@ export enum DomainErrorType { Unexpected = 'unexpected', } -export type FieldErrors = Record; export type NetworkReason = 'offline' | 'timeout' | 'unavailable'; +export type ValidationIssue = { + path?: string[]; + code: string; + params?: Record; +}; + type BaseDomainError = { type: TType; message: string; retryable: boolean; }; -export type ValidationError = BaseDomainError & { - fieldErrors: FieldErrors; +export type ValidationError = { + type: DomainErrorType.Validation; + retryable: false; + issues: ValidationIssue[]; }; export type NotFoundError = BaseDomainError & { @@ -122,8 +129,8 @@ export type DomainError = | BaseDomainError; export const domainError = { - validation(message: string, fieldErrors: FieldErrors): ValidationError { - return { type: DomainErrorType.Validation, message, fieldErrors, retryable: false }; + validation(issues: ValidationIssue[]): ValidationError { + return { type: DomainErrorType.Validation, issues, retryable: false }; }, unauthorized(message = 'Unauthorized'): DomainError { @@ -161,23 +168,25 @@ export const domainError = { }, } as const; -export function isDomainError(value: unknown): value is DomainError { - return ( - typeof value === 'object' && - value !== null && - 'type' in value && - 'message' in value && - 'retryable' in value && - typeof (value as { type?: unknown }).type === 'string' && - typeof (value as { message?: unknown }).message === 'string' && - typeof (value as { retryable?: unknown }).retryable === 'boolean' - ); -} - export const isRetryableDomainError = (error: DomainError): boolean => error.retryable; ``` +Validation errors should carry stable facts, not localized copy. Use `issues` +for field-level or form-level validation facts, omit `path` for form-level +issues, use `path` for nested fields or array items, `code` for the validation +rule, and `params` for dynamic values. `path` is a data/schema path, not a +user-facing label; the owning form/page maps it to a concrete input and +translated field label. Do not make backend or service contracts emit +user-facing validation sentences such as `"Title is required."`; translate +validation issues in the owning UI surface. + +Provide an `isDomainError(value): value is DomainError` guard in the domain +error module, but keep it strict: it should only return true for values that +already satisfy the full active `DomainError` shape. Do not make it accept +partial serialized errors just because `type` is known. Partial payload recovery +belongs in the API/Tauri boundary mappers. + Do not model domain errors with classes or inheritance. Keep them as plain data objects so they serialize cleanly across HTTP/Tauri, narrow with `switch`, and compare cleanly in tests. ## Domain Result @@ -195,40 +204,21 @@ export type DomainResult = Result; export * from './result'; export * from './domain-error'; export * from './domain-result'; -export * from './messages'; ``` -## User Messages +Keep `translation.ts` separate from the domain model exports when it imports UI +or i18n libraries. UI surfaces can import it directly from +`@shared/errors/translation`. -Keep only generic fallback messages in `shared/errors/messages.ts`. Feature-specific copy belongs near the feature UI. +## User-Facing Copy -```ts -// shared/errors/messages.ts -import { DomainErrorType, type DomainError } from './domain-error'; - -export function getDomainErrorMessage(error: DomainError): string { - switch (error.type) { - case DomainErrorType.Validation: - return error.message; - case DomainErrorType.Unauthorized: - return 'Authentication required.'; - case DomainErrorType.Forbidden: - return 'You do not have permission to perform this action.'; - case DomainErrorType.NotFound: - return 'Requested resource was not found.'; - case DomainErrorType.Conflict: - return 'The data is out of date. Refresh and try again.'; - case DomainErrorType.RateLimited: - return 'Too many requests. Try again later.'; - case DomainErrorType.Network: - return error.reason === 'offline' - ? 'No network connection.' - : 'Service is temporarily unavailable.'; - case DomainErrorType.Unexpected: - return error.message; - } -} -``` +Do not add hardcoded message helpers to the domain model. `DomainError` should +carry typed facts; user-facing copy belongs at the UI boundary. + +For localized apps, keep only generic shared summaries in +`shared/errors/translation.ts`. Feature-specific copy and validation field +messages stay near the owning page, form, or component. Read `i18n.md` for the +shared error translation pattern and validation-field translation boundary. ## API Error Mapping @@ -260,6 +250,18 @@ export function mapApiErrorToDomainError( } ``` +For validation responses, map API validation facts to `ValidationIssue[]`. +Prefer stable API codes and params over server-provided display strings; keep +string messages only as debug/fallback data outside the main UI contract when a +project explicitly needs them. + +`mapApiErrorToDomainError()` is also the right place to handle partial +transport-shaped errors. If a payload has a known domain error `type` but is +missing or malformed fields, preserve the type when practical by rebuilding a +complete `DomainError` through `domainError.*(...)` with safe defaults. Fall +back to `domainError.unexpected(...)` only when the type is unknown or the +payload cannot be interpreted safely. + If the project uses Axios, Axios-specific checks stay in this file. Do not import Axios from `shared/errors`. ## Tauri Error Mapping @@ -290,6 +292,11 @@ export function mapTauriErrorToDomainError( } ``` +Apply the same partial-payload policy here as in `mapApiErrorToDomainError()`: +`isDomainError()` accepts only complete serialized domain errors, while +`mapTauriErrorToDomainError()` may preserve known domain error types by +rebuilding full errors with safe defaults. + Wrap Tauri invoke calls to remove repeated `try/catch` from services: ```ts @@ -389,7 +396,10 @@ Test: - `ok`/`err` shape and `DomainResult` assignability; - `DomainErrorType` narrowing and factories; +- validation issue paths, codes, params, and form-level issues without paths; +- `isDomainError()` rejecting malformed partial payloads instead of coercing them; - retryability rules; - API status/payload mapping for the current HTTP client; -- Tauri unknown fallback and pass-through of serialized `DomainError`; +- API/Tauri mappers preserving known domain error types from partial serialized payloads by rebuilding complete `DomainError` values; +- Tauri unknown fallback and pass-through of complete serialized `DomainError`; - `unwrapDomainResult` resolving values and rejecting with `DomainError`. diff --git a/skills/react-vite-structure/references/feature-workflow.md b/skills/react-vite-structure/references/feature-workflow.md index 91d41fa..2b10a4c 100644 --- a/skills/react-vite-structure/references/feature-workflow.md +++ b/skills/react-vite-structure/references/feature-workflow.md @@ -250,16 +250,21 @@ export const ProductList: FC = ({ products, onEdit, onDelete } ``` **8. Create Page** (`pages/ListPage.tsx`) +For localized apps, route-facing pages should use `useTranslation()` for +visible copy instead of hardcoded labels. + ```typescript import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import { useProducts, useDeleteProduct } from '../hooks/useProductData'; import { ProductList } from '../components/ProductList'; -import { LoadingSpinner } from '@components/feedback/LoadingSpinner'; -import { getDomainErrorMessage } from '@shared/errors'; +import { LoadingSpinner } from '@shared/components/feedback/LoadingSpinner'; +import { LoadErrorState } from '@shared/components/feedback/LoadErrorState'; import type { Product } from '../types/product.types'; const ListPage: FC = () => { - const { data: products, isLoading, error } = useProducts(); + const { t } = useTranslation(); + const { data: products, isLoading, error, refetch } = useProducts(); const deleteMutation = useDeleteProduct(); const handleEdit = (product: Product) => { @@ -268,17 +273,28 @@ const ListPage: FC = () => { }; const handleDelete = (id: string) => { - if (confirm('Are you sure?')) { + if (confirm(t(($) => $.products.dialogs.confirmDelete))) { deleteMutation.mutate(id); } }; if (isLoading) return ; - if (error) return
{getDomainErrorMessage(error)}
; + if (error) { + return ( + $.products.errors.couldNotLoad)} + variant="page" + onRetry={() => void refetch()} + /> + ); + } return (
-

Products

+

+ {t(($) => $.products.labels.products)} +

()`, `useController`, `useWatch`, `reset`, and `handleSubmit`; +- own default values, edit-mode hydration, submit DTO mapping, mutation state, + navigation, and query invalidation; +- map backend `ValidationIssue.path` values to concrete form fields; +- merge client and service validation messages before rendering. + +Keep reusable feature form components mostly presentational. Pass field values, +change handlers, pending/disabled state, and already translated validation +messages as props. Do not make presentational form components import platform +services, React Query mutations, route navigation, or backend validation types. + +## Schema And Message Pattern + +Use schema factories when validation messages need localization: + +```ts +const createProductFormSchema = (t: TFunction) => + z.object({ + description: z.string(), + name: z.string().trim().min(1, t(($) => $.forms.validation.required, { + field: t(($) => $.products.fields.name), + })), + }) + +type ProductFormValues = z.infer> + +const form = useForm({ + defaultValues: { description: '', name: '' }, + resolver: zodResolver(createProductFormSchema(t), undefined, { mode: 'sync' }), +}) +``` + +Prefer shared helpers for common validation rules and message conversion only +when multiple forms already repeat the same behavior. Keep field-specific label +mapping local to the owning form/page unless the same form module owns every +caller. + +Trim and normalize values at clear boundaries. Schema rules may trim to validate +user intent, but submit handlers should still send explicit DTO values, such as +`values.name.trim()`. Avoid mutating visible input text on every keystroke unless +the product intentionally formats that field while editing. + +## Client And Server Validation + +Show field-level errors only when the owning form can map the validation path to +a concrete input or field group the user can correct. Unknown paths, missing +paths, conflicts, permission errors, offline failures, timeouts, and unavailable +services should render as form-level or action-level errors near the submitting +surface. + +Client-side Zod errors and backend validation issues should feed the same +presentational validation-message props. Merge them in the page/container so the +form component does not need to know where a message came from. + +Clear stale mutation errors when the user edits a field that can fix the +failure. Clear client field errors when the new value obviously satisfies the +rule, or let React Hook Form validation update them during submit/change +according to the project's existing validation mode. + +## Accessibility And Rendering + +Each invalid input or field group should expose the error accessibly: + +- set `aria-invalid` when messages exist for that field; +- connect messages with `aria-describedby`; +- render visible message text near the owned field or field group; +- use a form/action error surface for failures that do not belong to one field. + +## Testing + +Add colocated tests for required and conditional validation rules, edit-mode +hydration and reset behavior, submit DTO trimming/mapping, mutation failures +preserving draft values, backend validation paths attaching to the expected +fields, and accessible invalid states/descriptions. Add Storybook states for +visually meaningful form conditions such as default, filled/editing, validation +errors, mutation error, dense/long content, and disabled or pending submit. diff --git a/skills/react-vite-structure/references/i18n.md b/skills/react-vite-structure/references/i18n.md new file mode 100644 index 0000000..b3a1442 --- /dev/null +++ b/skills/react-vite-structure/references/i18n.md @@ -0,0 +1,174 @@ +# I18n + +Use this reference when adding or changing localization in a React + Vite app. + +## Core Pattern + +When the project does not already have a coherent localization stack, prefer +`i18next + react-i18next` with typed selector resources. Keep the app-wide +runtime setup under `src/core/i18n`: + +```text +src/core/i18n/ + I18nProvider.tsx + I18nProvider.test.tsx + i18n.ts + i18n.test.ts + i18next.d.ts + index.ts + locales.ts + resources/ + en-US.ts + [locale].ts + index.ts +``` + +Use TypeScript resource modules instead of JSON when selector typing or shared +resource types are needed. Treat the default locale resource as the canonical +key tree; every public locale should match its keys and interpolation +placeholders. + +## Responsibilities + +- `i18n.ts` creates the app i18next instance, registers `initReactI18next`, sets the default namespace, enables selector keys, disables React suspense unless the app intentionally uses async translation loading, and sets fallback/supported locales. +- `i18next.d.ts` augments `i18next` with the default namespace, selector mode, resource type, and non-null return behavior. +- `locales.ts` owns `defaultLocale`, `publicLocales`, `PublicLocale`, locale validation, fallback-to-default behavior, and text direction metadata for RTL locales. +- `I18nProvider.tsx` wraps `I18nextProvider` and syncs `document.documentElement.lang` and `dir` with the resolved language. +- `resources/index.ts` collects locale resources under the default namespace and exports the resource type used by `i18next.d.ts`. +- `index.ts` exports only the public i18n API needed by app providers, tests, settings, and Storybook harnesses. + +## Usage Boundaries + +Feature pages and components should call `useTranslation()` and use selector +keys: + +```tsx +const { t } = useTranslation() + +return

{t(($) => $.settings.labels.settings)}

+``` + +Keep resource paths semantic and UI-oriented, such as `common.actions.save`, +`settings.labels.language`, or `decks.errors.deckCouldNotLoad`. Do not make +feature modules own i18n initialization, locale lists, fallback behavior, or +document metadata. + +Put shared translated helper boundaries near the shared behavior they represent: + +- `src/shared/errors/translation.ts` for generic `DomainError` summary messages on shared error surfaces. +- `src/shared/lib/translated-date-format.ts` for user-facing date, relative-time, duration, and status labels. + +Shared error surfaces such as load states, bottom status banners, and confirm +dialogs do not know field labels or validation rules. Use a generic summary +translator there. These translators should receive full `DomainError` values; +API/Tauri boundary mappers normalize partial transport payloads before errors +reach UI components. + +```ts +export const translateDomainErrorSummary = (t: TFunction, error: DomainError) => { + switch (error.type) { + case DomainErrorType.Validation: + return t(($) => $.errors.byType.validation) + case DomainErrorType.Unauthorized: + return t(($) => $.errors.byType.unauthorized) + case DomainErrorType.Forbidden: + return t(($) => $.errors.byType.forbidden) + case DomainErrorType.NotFound: + return t(($) => $.errors.byType.notFound) + case DomainErrorType.Conflict: + return t(($) => $.errors.byType.conflict) + case DomainErrorType.RateLimited: + return t(($) => $.errors.byType.rateLimited) + case DomainErrorType.Network: + switch (error.reason) { + case 'offline': + return t(($) => $.errors.byType.offline) + case 'timeout': + return t(($) => $.errors.byType.timeout) + case 'unavailable': + return t(($) => $.errors.byType.unavailable) + } + case DomainErrorType.Unexpected: + return error.message + } +} +``` + +Translate field-level validation only after the owning form or page maps the +data path to a translated UI label. Keep the path-to-label mapper local to the +form/page, or feature-local when the same form module owns every caller: + +```ts +const getFieldLabel = ( + t: TFunction, + issue: ValidationIssue, +): string | null => { + switch (issue.path?.join('.')) { + case 'name': + return t(($) => $.productForm.fields.name) + case 'stock': + return t(($) => $.productForm.fields.stock) + default: + return null + } +} + +const translateFieldValidationIssue = ( + t: TFunction, + issue: ValidationIssue, + fieldLabel: string, +) => { + switch (issue.code) { + case 'required': + return t(($) => $.forms.validation.required, { field: fieldLabel }) + case 'min': + return t(($) => $.forms.validation.min, { + field: fieldLabel, + min: issue.params?.min, + }) + default: + return t(($) => $.forms.validation.invalid, { field: fieldLabel }) + } +} + +const fieldLabel = getFieldLabel(t, issue) +const message = fieldLabel + ? translateFieldValidationIssue(t, issue, fieldLabel) + : t(($) => $.productForm.validation.invalid) +``` + +`issue.path` is a data/schema path, not a user-facing label. Use it to +associate issues with nested fields or array items, then let the owning UI map +that path to a concrete input and label. Issues without `path`, or with a path +unknown to the current form, should become form/page-level validation messages +instead of field-level messages. Keep server-provided display strings out of +new validation contracts. + +Keep platform services, generated API code, backend contracts, route paths, +fixture seeds, and internal test-only strings language-agnostic unless they are +visible UI. + +## Providers and Storybook + +Wrap the main app provider tree with the app i18n provider. Storybook preview +decorators and shared Storybook app harnesses should use the same provider once +stories render translated copy; avoid per-story ad hoc i18n setup. + +If a language setting exists, store the selected public locale in settings and +call `i18n.changeLanguage(getDocumentLocale(value))` at the UI boundary. Keep +settings service contracts generic, usually as a string locale value, unless the +backend contract is intentionally restricted to public locales. + +## Testing + +Add or update focused tests for: + +- default locale creation and fallback for unsupported locales; +- `publicLocales`, locale validation, and RTL document direction metadata; +- resource key and interpolation-placeholder parity against the default locale; +- plural-sensitive translations for representative locales when count-based strings exist; +- `I18nProvider` document `lang` and `dir` synchronization; +- branchy translated helper modules, colocated beside the helper. + +Use fixed timers for relative-date helpers so expectations assert exact +user-facing strings deterministically. diff --git a/skills/react-vite-structure/references/structure.md b/skills/react-vite-structure/references/structure.md index 94a1d8c..d72179b 100644 --- a/skills/react-vite-structure/references/structure.md +++ b/skills/react-vite-structure/references/structure.md @@ -104,6 +104,7 @@ project-root/ | | | |-- feedback/ # User feedback | | | | |-- Toast.tsx | | | | |-- LoadingSpinner.tsx +| | | | |-- LoadErrorState.tsx | | | | |-- ErrorBoundary.tsx | | | | |-- Skeleton.tsx | | | | `-- index.ts @@ -141,12 +142,12 @@ project-root/ | | | |-- constants.ts # Global constants | | | `-- index.ts | | | -| | |-- errors/ # Typed Result and DomainError model +| | |-- errors/ # Typed Result, DomainError, and translated summaries | | | |-- result.ts | | | |-- domain-error.ts | | | |-- domain-error.test.ts | | | |-- domain-result.ts -| | | |-- messages.ts +| | | |-- translation.ts | | | `-- index.ts | | | | | |-- services/ # Core services @@ -210,10 +211,14 @@ project-root/ | | | `-- index.ts | | | | | `-- i18n/ # Internationalization (optional) -| | |-- locales/ -| | | |-- en.json -| | | `-- id.json -| | |-- i18n.config.ts +| | |-- I18nProvider.tsx # App i18n provider + document metadata +| | |-- i18n.ts # i18next init and app instance +| | |-- i18next.d.ts # Typed selector resource augmentation +| | |-- locales.ts # Public locales, fallback, direction metadata +| | |-- resources/ +| | | |-- en-US.ts # Canonical default-locale key tree +| | | |-- [locale].ts +| | | `-- index.ts | | `-- index.ts | | | |-- platform/ # Runtime/backend-specific implementations @@ -502,7 +507,7 @@ shared/errors/ |-- result.ts |-- domain-error.ts |-- domain-result.ts -|-- messages.ts +|-- translation.ts `-- index.ts shared/services/api/ diff --git a/skills/react-vite-structure/references/ui-error-states.md b/skills/react-vite-structure/references/ui-error-states.md index fd3dada..529f2ba 100644 --- a/skills/react-vite-structure/references/ui-error-states.md +++ b/skills/react-vite-structure/references/ui-error-states.md @@ -108,7 +108,7 @@ Rules: - Do not leave `mutation.isError` without a visible, accessible error surface on the owning page, form, dialog, or action area. - Do not navigate away, close a dialog, clear a draft, or remove the target item on mutation failure. - Keep form values, selection, active tab, scroll context, and dialog state intact. -- Use field-level errors for validation when `DomainError.fieldErrors` identifies fields the user can correct. +- Use field-level errors for validation only when the owning form can map `ValidationIssue.path` to a concrete input the user can correct. - Use a form-level or action-level error for non-field failures such as conflict, permission, offline, timeout, or unavailable service. - Make failed user actions visibly distinct from non-blocking background warnings, using the app's existing severity/tone system. - On success, invalidate or update relevant React Query cache, then close/navigate only after the mutation has actually succeeded. @@ -140,4 +140,4 @@ Use: For visual Storybook coverage of these states, read `references/storybook.md`. -Keep unit/integration tests focused on branching behavior that can regress: no-data query errors replace the owning surface, stale-data query errors keep content visible, mutation errors preserve drafts, field errors attach to the correct inputs, and retry callbacks are wired only when expected. +Keep unit/integration tests focused on branching behavior that can regress: no-data query errors replace the owning surface, stale-data query errors keep content visible, mutation errors preserve drafts, validation issues attach to the correct inputs, and retry callbacks are wired only when expected. diff --git a/ui/package.json b/ui/package.json index 1919367..b668a9b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -19,6 +19,7 @@ "build-storybook": "storybook build" }, "dependencies": { + "@hookform/resolvers": "5.4.0", "@local/mock-server": "workspace:*", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -36,6 +37,7 @@ "lucide-react": "^1.17.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-hook-form": "7.77.0", "react-i18next": "^17.0.8", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", diff --git a/ui/src/core/i18n/resources/ar.ts b/ui/src/core/i18n/resources/ar.ts index 338d39a..cc7110a 100644 --- a/ui/src/core/i18n/resources/ar.ts +++ b/ui/src/core/i18n/resources/ar.ts @@ -198,7 +198,6 @@ export const ar = { }, descriptions: { emptyDeck: 'أضف ملاحظة حتى تحتوي هذه المجموعة على مادة للمراجعة.', - editorDefault: 'مجموعة دراسة مركزة.', editorVisual: 'اختر رمزا لغلاف هذه المجموعة.', notesSearchPlaceholder: 'ابحث في الملاحظات…', }, @@ -230,8 +229,6 @@ export const ar = { descriptionLabel: 'وصف المجموعة', descriptionPlaceholder: 'ما الذي ستساعدك هذه المجموعة على مراجعته؟', namePlaceholder: 'اسم المجموعة', - untitledDeck: 'مجموعة بلا عنوان', - untitledDeckLower: 'مجموعة بلا عنوان', }, labels: { createDeckTitle: 'إنشاء مجموعة', @@ -261,20 +258,31 @@ export const ar = { timeout: 'استغرق ذلك وقتا طويلا. حاول مرة أخرى.', unauthorized: 'سجل الدخول للمتابعة.', unavailable: 'الخدمة غير متاحة مؤقتا.', + validation: 'تحقق من الحقول المميزة ثم حاول مرة أخرى.', }, fallback: { unexpected: 'خطأ غير متوقع', }, }, + forms: { + validation: { + invalid: '{{field}} غير صالح.', + invalidEnum: 'اختر قيمة صالحة لـ {{field}}.', + invalidFormat: 'أدخل قيمة صالحة لـ {{field}}.', + maxLength: 'يجب ألا يتجاوز {{field}} {{max}} أحرف.', + maximum: 'يجب ألا يزيد {{field}} عن {{max}}.', + minLength: 'يجب أن يحتوي {{field}} على {{min}} أحرف على الأقل.', + minimum: 'يجب ألا يقل {{field}} عن {{min}}.', + required: '{{field}} مطلوب.', + }, + }, + folders: { actions: { createFolder: 'إنشاء مجلد', deleteFolder: 'حذف المجلد', editFolder: 'تعديل المجلد', }, - descriptions: { - editorDefault: 'مجلد للمجموعات ذات الصلة.', - }, dialogs: { deleteFolderDescription: 'سيتم نقل "{{name}}" إلى المهملات. يمكنك استعادته لاحقا.', deleteFolderFallbackDescription: 'سيتم نقل هذا المجلد إلى المهملات. يمكنك استعادته لاحقا.', @@ -296,7 +304,6 @@ export const ar = { descriptionLabel: 'وصف المجلد', descriptionPlaceholder: 'ما الذي ينتمي إلى هذا المجلد؟', namePlaceholder: 'اسم المجلد', - untitledFolder: 'مجلد بلا عنوان', }, labels: { createFolderTitle: 'إنشاء مجلد', @@ -638,7 +645,6 @@ export const ar = { openingWorkspace: 'جار فتح {{title}}', }, descriptions: { - editorDefault: 'سياق دراسة.', editorVisual: 'اختر مرجعا بصريا لمساحة العمل هذه.', emptyList: 'افصل المجموعات والملاحظات وقوائم المراجعة حسب سياق الدراسة.', }, @@ -663,7 +669,6 @@ export const ar = { descriptionLabel: 'وصف مساحة العمل', descriptionPlaceholder: 'ما الذي ينتمي إلى مساحة العمل هذه؟', namePlaceholder: 'اسم مساحة العمل', - untitledWorkspace: 'مساحة عمل بلا عنوان', }, labels: { createWorkspaceTitle: 'إنشاء مساحة عمل', diff --git a/ui/src/core/i18n/resources/bg.ts b/ui/src/core/i18n/resources/bg.ts index 3fc4855..83a8440 100644 --- a/ui/src/core/i18n/resources/bg.ts +++ b/ui/src/core/i18n/resources/bg.ts @@ -158,7 +158,6 @@ export const bg = { }, descriptions: { emptyDeck: 'Добавете бележка, за да има тази колода материал за преговор.', - editorDefault: 'Фокусирана колода за учене.', editorVisual: 'Изберете символ за корицата на тази колода.', notesSearchPlaceholder: 'Търсене на бележки…', }, @@ -190,8 +189,6 @@ export const bg = { descriptionLabel: 'Описание на колодата', descriptionPlaceholder: 'Какво ще ви помогне да преговаряте тази колода?', namePlaceholder: 'Име на колодата', - untitledDeck: 'Колода без заглавие', - untitledDeckLower: 'колода без заглавие', }, labels: { createDeckTitle: 'Създай колода', @@ -221,20 +218,31 @@ export const bg = { timeout: 'Това отне твърде дълго. Опитайте отново.', unauthorized: 'Влезте, за да продължите.', unavailable: 'Услугата временно не е налична.', + validation: 'Проверете маркираните полета и опитайте отново.', }, fallback: { unexpected: 'Неочаквана грешка', }, }, + forms: { + validation: { + invalid: '{{field}} е невалидно.', + invalidEnum: 'Изберете валидна стойност за {{field}}.', + invalidFormat: 'Въведете валидна стойност за {{field}}.', + maxLength: '{{field}} трябва да е най-много {{max}} знака.', + maximum: '{{field}} трябва да е най-много {{max}}.', + minLength: '{{field}} трябва да е поне {{min}} знака.', + minimum: '{{field}} трябва да е поне {{min}}.', + required: '{{field}} е задължително.', + }, + }, + folders: { actions: { createFolder: 'Създай папка', deleteFolder: 'Изтрий папка', editFolder: 'Редактирай папка', }, - descriptions: { - editorDefault: 'Папка за свързани колоди.', - }, dialogs: { deleteFolderDescription: 'Това премества "{{name}}" в Кошчето. Можете да я възстановите по-късно.', deleteFolderFallbackDescription: 'Това премества тази папка в Кошчето. Можете да я възстановите по-късно.', @@ -256,7 +264,6 @@ export const bg = { descriptionLabel: 'Описание на папката', descriptionPlaceholder: 'Какво принадлежи в тази папка?', namePlaceholder: 'Име на папката', - untitledFolder: 'Папка без заглавие', }, labels: { createFolderTitle: 'Създай папка', @@ -586,7 +593,6 @@ export const bg = { openingWorkspace: 'Отваряне на {{title}}', }, descriptions: { - editorDefault: 'Контекст за учене.', editorVisual: 'Изберете визуална опора за това работно пространство.', emptyList: 'Разделете колоди, бележки и опашки за преговор по контекст на учене.', }, @@ -611,7 +617,6 @@ export const bg = { descriptionLabel: 'Описание на работното пространство', descriptionPlaceholder: 'Какво принадлежи в това работно пространство?', namePlaceholder: 'Име на работното пространство', - untitledWorkspace: 'Работно пространство без заглавие', }, labels: { createWorkspaceTitle: 'Създай работно пространство', diff --git a/ui/src/core/i18n/resources/bs.ts b/ui/src/core/i18n/resources/bs.ts index 25aeb28..84b3692 100644 --- a/ui/src/core/i18n/resources/bs.ts +++ b/ui/src/core/i18n/resources/bs.ts @@ -168,7 +168,6 @@ export const bs = { }, descriptions: { emptyDeck: 'Dodajte bilješku da bi ovaj špil imao materijal za ponavljanje.', - editorDefault: 'Fokusiran špil za učenje.', editorVisual: 'Odaberite simbol naslovnice za ovaj špil.', notesSearchPlaceholder: 'Pretraži bilješke…', }, @@ -200,8 +199,6 @@ export const bs = { descriptionLabel: 'Opis špila', descriptionPlaceholder: 'Šta će vam ovaj špil pomoći da ponavljate?', namePlaceholder: 'Naziv špila', - untitledDeck: 'Špil bez naslova', - untitledDeckLower: 'špil bez naslova', }, labels: { createDeckTitle: 'Kreiraj špil', @@ -231,20 +228,31 @@ export const bs = { timeout: 'Ovo je trajalo predugo. Pokušajte ponovo.', unauthorized: 'Prijavite se da nastavite.', unavailable: 'Usluga je privremeno nedostupna.', + validation: 'Provjerite označena polja i pokušajte ponovo.', }, fallback: { unexpected: 'Neočekivana greška', }, }, + forms: { + validation: { + invalid: '{{field}} nije važeće.', + invalidEnum: 'Odaberite važeću vrijednost za {{field}}.', + invalidFormat: 'Unesite važeću vrijednost za {{field}}.', + maxLength: '{{field}} može imati najviše {{max}} znakova.', + maximum: '{{field}} mora biti najviše {{max}}.', + minLength: '{{field}} mora imati najmanje {{min}} znakova.', + minimum: '{{field}} mora biti najmanje {{min}}.', + required: '{{field}} je obavezno.', + }, + }, + folders: { actions: { createFolder: 'Kreiraj mapu', deleteFolder: 'Izbriši mapu', editFolder: 'Uredi mapu', }, - descriptions: { - editorDefault: 'Mapa za povezane špilove.', - }, dialogs: { deleteFolderDescription: 'Ovo premješta "{{name}}" u Smeće. Možete je vratiti kasnije.', deleteFolderFallbackDescription: 'Ovo premješta ovu mapu u Smeće. Možete je vratiti kasnije.', @@ -266,7 +274,6 @@ export const bs = { descriptionLabel: 'Opis mape', descriptionPlaceholder: 'Šta pripada ovoj mapi?', namePlaceholder: 'Naziv mape', - untitledFolder: 'Mapa bez naslova', }, labels: { createFolderTitle: 'Kreiraj mapu', @@ -599,7 +606,6 @@ export const bs = { openingWorkspace: 'Otvaranje {{title}}', }, descriptions: { - editorDefault: 'Kontekst učenja.', editorVisual: 'Odaberite vizuelno sidro za ovaj radni prostor.', emptyList: 'Odvojite špilove, bilješke i redove za ponavljanje po kontekstu učenja.', }, @@ -624,7 +630,6 @@ export const bs = { descriptionLabel: 'Opis radnog prostora', descriptionPlaceholder: 'Šta pripada ovom radnom prostoru?', namePlaceholder: 'Naziv radnog prostora', - untitledWorkspace: 'Radni prostor bez naslova', }, labels: { createWorkspaceTitle: 'Kreiraj radni prostor', diff --git a/ui/src/core/i18n/resources/ca.ts b/ui/src/core/i18n/resources/ca.ts index 4479b1c..dba66f4 100644 --- a/ui/src/core/i18n/resources/ca.ts +++ b/ui/src/core/i18n/resources/ca.ts @@ -168,7 +168,6 @@ export const ca = { }, descriptions: { emptyDeck: 'Afegeix una nota perquè aquesta baralla tingui material per repassar.', - editorDefault: 'Baralla d\'estudi enfocada.', editorVisual: 'Tria un símbol de portada per a aquesta baralla.', notesSearchPlaceholder: 'Cerca notes…', }, @@ -200,8 +199,6 @@ export const ca = { descriptionLabel: 'Descripció de la baralla', descriptionPlaceholder: 'Què t\'ajudarà a repassar aquesta baralla?', namePlaceholder: 'Nom de la baralla', - untitledDeck: 'Baralla sense títol', - untitledDeckLower: 'baralla sense títol', }, labels: { createDeckTitle: 'Crea una baralla', @@ -231,20 +228,31 @@ export const ca = { timeout: 'Això ha trigat massa. Torna-ho a provar.', unauthorized: 'Inicia la sessió per continuar.', unavailable: 'El servei no està disponible temporalment.', + validation: 'Revisa els camps ressaltats i torna-ho a provar.', }, fallback: { unexpected: 'Error inesperat', }, }, + forms: { + validation: { + invalid: '{{field}} no és vàlid.', + invalidEnum: 'Tria un valor vàlid per a {{field}}.', + invalidFormat: 'Introdueix un valor vàlid per a {{field}}.', + maxLength: '{{field}} ha de tenir com a màxim {{max}} caràcters.', + maximum: '{{field}} ha de ser com a màxim {{max}}.', + minLength: '{{field}} ha de tenir com a mínim {{min}} caràcters.', + minimum: '{{field}} ha de ser com a mínim {{min}}.', + required: '{{field}} és obligatori.', + }, + }, + folders: { actions: { createFolder: 'Crea una carpeta', deleteFolder: 'Suprimeix la carpeta', editFolder: 'Edita la carpeta', }, - descriptions: { - editorDefault: 'Carpeta per a baralles relacionades.', - }, dialogs: { deleteFolderDescription: 'Això mou "{{name}}" a la Paperera. Pots restaurar-la més tard.', deleteFolderFallbackDescription: 'Això mou aquesta carpeta a la Paperera. Pots restaurar-la més tard.', @@ -266,7 +274,6 @@ export const ca = { descriptionLabel: 'Descripció de la carpeta', descriptionPlaceholder: 'Què pertany a aquesta carpeta?', namePlaceholder: 'Nom de la carpeta', - untitledFolder: 'Carpeta sense títol', }, labels: { createFolderTitle: 'Crea una carpeta', @@ -599,7 +606,6 @@ export const ca = { openingWorkspace: 'S\'està obrint {{title}}', }, descriptions: { - editorDefault: 'Context d\'estudi.', editorVisual: 'Tria un ancoratge visual per a aquest espai de treball.', emptyList: 'Separa baralles, notes i cues de repàs per context d\'estudi.', }, @@ -624,7 +630,6 @@ export const ca = { descriptionLabel: 'Descripció de l\'espai de treball', descriptionPlaceholder: 'Què pertany a aquest espai de treball?', namePlaceholder: 'Nom de l\'espai de treball', - untitledWorkspace: 'Espai de treball sense títol', }, labels: { createWorkspaceTitle: 'Crea un espai de treball', diff --git a/ui/src/core/i18n/resources/cs.ts b/ui/src/core/i18n/resources/cs.ts index 8fe8568..f8a7f5e 100644 --- a/ui/src/core/i18n/resources/cs.ts +++ b/ui/src/core/i18n/resources/cs.ts @@ -178,7 +178,6 @@ export const cs = { }, descriptions: { emptyDeck: 'Přidejte poznámku, aby měl tento balíček materiál k opakování.', - editorDefault: 'Zaměřený studijní balíček.', editorVisual: 'Vyberte krycí symbol pro tento balíček.', notesSearchPlaceholder: 'Hledat poznámky…', }, @@ -210,8 +209,6 @@ export const cs = { descriptionLabel: 'Popis balíčku', descriptionPlaceholder: 'Co vám tento balíček pomůže opakovat?', namePlaceholder: 'Název balíčku', - untitledDeck: 'Balíček bez názvu', - untitledDeckLower: 'balíček bez názvu', }, labels: { createDeckTitle: 'Vytvořit balíček', @@ -241,20 +238,31 @@ export const cs = { timeout: 'Trvá to příliš dlouho. Zkuste to znovu.', unauthorized: 'Pro pokračování se přihlaste.', unavailable: 'Služba je dočasně nedostupná.', + validation: 'Zkontrolujte zvýrazněná pole a zkuste to znovu.', }, fallback: { unexpected: 'Neočekávaná chyba', }, }, + forms: { + validation: { + invalid: '{{field}} není platné.', + invalidEnum: 'Vyberte platnou hodnotu pro {{field}}.', + invalidFormat: 'Zadejte platnou hodnotu pro {{field}}.', + maxLength: '{{field}} může mít nejvýše {{max}} znaků.', + maximum: '{{field}} musí být nejvýše {{max}}.', + minLength: '{{field}} musí mít alespoň {{min}} znaků.', + minimum: '{{field}} musí být alespoň {{min}}.', + required: '{{field}} je povinné.', + }, + }, + folders: { actions: { createFolder: 'Vytvořit složku', deleteFolder: 'Smazat složku', editFolder: 'Upravit složku', }, - descriptions: { - editorDefault: 'Složka pro související balíčky.', - }, dialogs: { deleteFolderDescription: 'Tím se "{{name}}" přesune do Koše. Později ji můžete obnovit.', deleteFolderFallbackDescription: 'Tím se tato složka přesune do Koše. Později ji můžete obnovit.', @@ -276,7 +284,6 @@ export const cs = { descriptionLabel: 'Popis složky', descriptionPlaceholder: 'Co patří do této složky?', namePlaceholder: 'Název složky', - untitledFolder: 'Složka bez názvu', }, labels: { createFolderTitle: 'Vytvořit složku', @@ -612,7 +619,6 @@ export const cs = { openingWorkspace: 'Otevírání {{title}}', }, descriptions: { - editorDefault: 'Studijní kontext.', editorVisual: 'Vyberte vizuální kotvu pro tento pracovní prostor.', emptyList: 'Oddělte balíčky, poznámky a fronty opakování podle studijního kontextu.', }, @@ -637,7 +643,6 @@ export const cs = { descriptionLabel: 'Popis pracovního prostoru', descriptionPlaceholder: 'Co patří do tohoto pracovního prostoru?', namePlaceholder: 'Název pracovního prostoru', - untitledWorkspace: 'Pracovní prostor bez názvu', }, labels: { createWorkspaceTitle: 'Vytvořit pracovní prostor', diff --git a/ui/src/core/i18n/resources/da.ts b/ui/src/core/i18n/resources/da.ts index 75cd673..6861ef5 100644 --- a/ui/src/core/i18n/resources/da.ts +++ b/ui/src/core/i18n/resources/da.ts @@ -158,7 +158,6 @@ export const da = { }, descriptions: { emptyDeck: 'Tilføj en note, så dette kortsæt har materiale til gentagelse.', - editorDefault: 'Fokuseret studiekortsæt.', editorVisual: 'Vælg et omslagssymbol til dette kortsæt.', notesSearchPlaceholder: 'Søg i noter…', }, @@ -190,8 +189,6 @@ export const da = { descriptionLabel: 'Beskrivelse af kortsæt', descriptionPlaceholder: 'Hvad skal dette kortsæt hjælpe dig med at gentage?', namePlaceholder: 'Navn på kortsæt', - untitledDeck: 'Kortsæt uden titel', - untitledDeckLower: 'kortsæt uden titel', }, labels: { createDeckTitle: 'Opret kortsæt', @@ -221,20 +218,31 @@ export const da = { timeout: 'Dette tog for lang tid. Prøv igen.', unauthorized: 'Log ind for at fortsætte.', unavailable: 'Tjenesten er midlertidigt utilgængelig.', + validation: 'Kontrollér de markerede felter, og prøv igen.', }, fallback: { unexpected: 'Uventet fejl', }, }, + forms: { + validation: { + invalid: '{{field}} er ugyldigt.', + invalidEnum: 'Vælg en gyldig værdi for {{field}}.', + invalidFormat: 'Indtast en gyldig værdi for {{field}}.', + maxLength: '{{field}} må højst være {{max}} tegn.', + maximum: '{{field}} må højst være {{max}}.', + minLength: '{{field}} skal være mindst {{min}} tegn.', + minimum: '{{field}} skal være mindst {{min}}.', + required: '{{field}} er påkrævet.', + }, + }, + folders: { actions: { createFolder: 'Opret mappe', deleteFolder: 'Slet mappe', editFolder: 'Rediger mappe', }, - descriptions: { - editorDefault: 'Mappe til relaterede kortsæt.', - }, dialogs: { deleteFolderDescription: 'Dette flytter "{{name}}" til Papirkurv. Du kan gendanne den senere.', deleteFolderFallbackDescription: 'Dette flytter denne mappe til Papirkurv. Du kan gendanne den senere.', @@ -256,7 +264,6 @@ export const da = { descriptionLabel: 'Mappebeskrivelse', descriptionPlaceholder: 'Hvad hører hjemme i denne mappe?', namePlaceholder: 'Mappenavn', - untitledFolder: 'Mappe uden titel', }, labels: { createFolderTitle: 'Opret mappe', @@ -586,7 +593,6 @@ export const da = { openingWorkspace: 'Åbner {{title}}', }, descriptions: { - editorDefault: 'Studiekontekst.', editorVisual: 'Vælg et visuelt anker til dette arbejdsområde.', emptyList: 'Adskil kortsæt, noter og gentagelseskøer efter studiekontekst.', }, @@ -611,7 +617,6 @@ export const da = { descriptionLabel: 'Beskrivelse af arbejdsområde', descriptionPlaceholder: 'Hvad hører hjemme i dette arbejdsområde?', namePlaceholder: 'Navn på arbejdsområde', - untitledWorkspace: 'Arbejdsområde uden titel', }, labels: { createWorkspaceTitle: 'Opret arbejdsområde', diff --git a/ui/src/core/i18n/resources/de.ts b/ui/src/core/i18n/resources/de.ts index 41d3aeb..9ce179a 100644 --- a/ui/src/core/i18n/resources/de.ts +++ b/ui/src/core/i18n/resources/de.ts @@ -158,7 +158,6 @@ export const de = { }, descriptions: { emptyDeck: 'Füge eine Notiz hinzu, damit dieser Stapel Wiederholungsstoff hat.', - editorDefault: 'Fokussierter Lernstapel.', editorVisual: 'Wähle ein Titelzeichen für diesen Stapel.', notesSearchPlaceholder: 'Notizen suchen…', }, @@ -190,8 +189,6 @@ export const de = { descriptionLabel: 'Stapelbeschreibung', descriptionPlaceholder: 'Was hilft dir dieser Stapel zu wiederholen?', namePlaceholder: 'Stapelname', - untitledDeck: 'Unbenannter Stapel', - untitledDeckLower: 'unbenannter Stapel', }, labels: { createDeckTitle: 'Stapel erstellen', @@ -221,20 +218,31 @@ export const de = { timeout: 'Das hat zu lange gedauert. Versuche es erneut.', unauthorized: 'Melde dich an, um fortzufahren.', unavailable: 'Der Dienst ist vorübergehend nicht verfügbar.', + validation: 'Prüfe die markierten Felder und versuche es erneut.', }, fallback: { unexpected: 'Unerwarteter Fehler', }, }, + forms: { + validation: { + invalid: '{{field}} ist ungültig.', + invalidEnum: 'Wähle einen gültigen Wert für {{field}}.', + invalidFormat: 'Gib einen gültigen Wert für {{field}} ein.', + maxLength: '{{field}} darf höchstens {{max}} Zeichen lang sein.', + maximum: '{{field}} darf höchstens {{max}} sein.', + minLength: '{{field}} muss mindestens {{min}} Zeichen lang sein.', + minimum: '{{field}} muss mindestens {{min}} sein.', + required: '{{field}} ist erforderlich.', + }, + }, + folders: { actions: { createFolder: 'Ordner erstellen', deleteFolder: 'Ordner löschen', editFolder: 'Ordner bearbeiten', }, - descriptions: { - editorDefault: 'Ordner für verwandte Stapel.', - }, dialogs: { deleteFolderDescription: 'Dadurch wird "{{name}}" in den Papierkorb verschoben. Du kannst ihn später wiederherstellen.', deleteFolderFallbackDescription: 'Dadurch wird dieser Ordner in den Papierkorb verschoben. Du kannst ihn später wiederherstellen.', @@ -256,7 +264,6 @@ export const de = { descriptionLabel: 'Ordnerbeschreibung', descriptionPlaceholder: 'Was gehört in diesen Ordner?', namePlaceholder: 'Ordnername', - untitledFolder: 'Unbenannter Ordner', }, labels: { createFolderTitle: 'Ordner erstellen', @@ -586,7 +593,6 @@ export const de = { openingWorkspace: '{{title}} wird geöffnet', }, descriptions: { - editorDefault: 'Lernkontext.', editorVisual: 'Wähle einen visuellen Anker für diesen Arbeitsbereich.', emptyList: 'Trenne Stapel, Notizen und Wiederholungslisten nach Lernkontext.', }, @@ -611,7 +617,6 @@ export const de = { descriptionLabel: 'Arbeitsbereichsbeschreibung', descriptionPlaceholder: 'Was gehört in diesen Arbeitsbereich?', namePlaceholder: 'Name des Arbeitsbereichs', - untitledWorkspace: 'Unbenannter Arbeitsbereich', }, labels: { createWorkspaceTitle: 'Arbeitsbereich erstellen', diff --git a/ui/src/core/i18n/resources/el.ts b/ui/src/core/i18n/resources/el.ts index 57c87c5..e85d975 100644 --- a/ui/src/core/i18n/resources/el.ts +++ b/ui/src/core/i18n/resources/el.ts @@ -158,7 +158,6 @@ export const el = { }, descriptions: { emptyDeck: 'Προσθέστε μια σημείωση ώστε αυτή η τράπουλα να έχει υλικό για επανάληψη.', - editorDefault: 'Εστιασμένη τράπουλα μελέτης.', editorVisual: 'Επιλέξτε ένα σύμβολο εξωφύλλου για αυτήν την τράπουλα.', notesSearchPlaceholder: 'Αναζήτηση σημειώσεων…', }, @@ -190,8 +189,6 @@ export const el = { descriptionLabel: 'Περιγραφή τράπουλας', descriptionPlaceholder: 'Τι θα σας βοηθήσει να επαναλάβετε αυτή η τράπουλα;', namePlaceholder: 'Όνομα τράπουλας', - untitledDeck: 'Τράπουλα χωρίς τίτλο', - untitledDeckLower: 'τράπουλα χωρίς τίτλο', }, labels: { createDeckTitle: 'Δημιουργία τράπουλας', @@ -221,20 +218,31 @@ export const el = { timeout: 'Αυτό πήρε πολύ χρόνο. Δοκιμάστε ξανά.', unauthorized: 'Συνδεθείτε για να συνεχίσετε.', unavailable: 'Η υπηρεσία δεν είναι προσωρινά διαθέσιμη.', + validation: 'Ελέγξτε τα επισημασμένα πεδία και δοκιμάστε ξανά.', }, fallback: { unexpected: 'Απροσδόκητο σφάλμα', }, }, + forms: { + validation: { + invalid: 'Το {{field}} δεν είναι έγκυρο.', + invalidEnum: 'Επιλέξτε έγκυρη τιμή για το {{field}}.', + invalidFormat: 'Εισαγάγετε έγκυρη τιμή για το {{field}}.', + maxLength: 'Το {{field}} πρέπει να έχει το πολύ {{max}} χαρακτήρες.', + maximum: 'Το {{field}} πρέπει να είναι το πολύ {{max}}.', + minLength: 'Το {{field}} πρέπει να έχει τουλάχιστον {{min}} χαρακτήρες.', + minimum: 'Το {{field}} πρέπει να είναι τουλάχιστον {{min}}.', + required: 'Το {{field}} είναι υποχρεωτικό.', + }, + }, + folders: { actions: { createFolder: 'Δημιουργία φακέλου', deleteFolder: 'Διαγραφή φακέλου', editFolder: 'Επεξεργασία φακέλου', }, - descriptions: { - editorDefault: 'Φάκελος για σχετικές τράπουλες.', - }, dialogs: { deleteFolderDescription: 'Αυτό μετακινεί το "{{name}}" στον Κάδο. Μπορείτε να το επαναφέρετε αργότερα.', deleteFolderFallbackDescription: 'Αυτό μετακινεί αυτόν τον φάκελο στον Κάδο. Μπορείτε να τον επαναφέρετε αργότερα.', @@ -256,7 +264,6 @@ export const el = { descriptionLabel: 'Περιγραφή φακέλου', descriptionPlaceholder: 'Τι ανήκει σε αυτόν τον φάκελο;', namePlaceholder: 'Όνομα φακέλου', - untitledFolder: 'Φάκελος χωρίς τίτλο', }, labels: { createFolderTitle: 'Δημιουργία φακέλου', @@ -586,7 +593,6 @@ export const el = { openingWorkspace: 'Άνοιγμα {{title}}', }, descriptions: { - editorDefault: 'Πλαίσιο μελέτης.', editorVisual: 'Επιλέξτε μια οπτική άγκυρα για αυτόν τον χώρο εργασίας.', emptyList: 'Διαχωρίστε τράπουλες, σημειώσεις και ουρές επανάληψης ανά πλαίσιο μελέτης.', }, @@ -611,7 +617,6 @@ export const el = { descriptionLabel: 'Περιγραφή χώρου εργασίας', descriptionPlaceholder: 'Τι ανήκει σε αυτόν τον χώρο εργασίας;', namePlaceholder: 'Όνομα χώρου εργασίας', - untitledWorkspace: 'Χώρος εργασίας χωρίς τίτλο', }, labels: { createWorkspaceTitle: 'Δημιουργία χώρου εργασίας', diff --git a/ui/src/core/i18n/resources/en-US.ts b/ui/src/core/i18n/resources/en-US.ts index 9add2ab..e96fcda 100644 --- a/ui/src/core/i18n/resources/en-US.ts +++ b/ui/src/core/i18n/resources/en-US.ts @@ -158,7 +158,6 @@ export const enUS = { }, descriptions: { emptyDeck: 'Add a note so this deck has material to review.', - editorDefault: 'Focused study deck.', editorVisual: 'Choose a cover glyph for this deck.', notesSearchPlaceholder: 'Search notes…', }, @@ -190,8 +189,6 @@ export const enUS = { descriptionLabel: 'Deck description', descriptionPlaceholder: 'What will this deck help you review?', namePlaceholder: 'Deck name', - untitledDeck: 'Untitled Deck', - untitledDeckLower: 'Untitled deck', }, labels: { createDeckTitle: 'Create Deck', @@ -221,20 +218,31 @@ export const enUS = { timeout: 'This took too long. Try again.', unauthorized: 'Sign in to continue.', unavailable: 'The service is temporarily unavailable.', + validation: 'Check the highlighted fields and try again.', }, fallback: { unexpected: 'Unexpected error', }, }, + forms: { + validation: { + invalid: '{{field}} is invalid.', + invalidEnum: 'Choose a valid {{field}}.', + invalidFormat: 'Enter a valid {{field}}.', + maxLength: '{{field}} must be at most {{max}} characters.', + maximum: '{{field}} must be at most {{max}}.', + minLength: '{{field}} must be at least {{min}} characters.', + minimum: '{{field}} must be at least {{min}}.', + required: '{{field}} is required.', + }, + }, + folders: { actions: { createFolder: 'Create folder', deleteFolder: 'Delete folder', editFolder: 'Edit Folder', }, - descriptions: { - editorDefault: 'Folder for related decks.', - }, dialogs: { deleteFolderDescription: 'This moves "{{name}}" to Trash. You can restore it later.', deleteFolderFallbackDescription: 'This moves this folder to Trash. You can restore it later.', @@ -256,7 +264,6 @@ export const enUS = { descriptionLabel: 'Folder description', descriptionPlaceholder: 'What belongs in this folder?', namePlaceholder: 'Folder name', - untitledFolder: 'Untitled Folder', }, labels: { createFolderTitle: 'Create Folder', @@ -586,7 +593,6 @@ export const enUS = { openingWorkspace: 'Opening {{title}}', }, descriptions: { - editorDefault: 'Study context.', editorVisual: 'Choose a visual anchor for this workspace.', emptyList: 'Separate decks, notes, and review queues by study context.', }, @@ -611,7 +617,6 @@ export const enUS = { descriptionLabel: 'Workspace description', descriptionPlaceholder: 'What belongs in this workspace?', namePlaceholder: 'Workspace name', - untitledWorkspace: 'Untitled Workspace', }, labels: { createWorkspaceTitle: 'Create Workspace', diff --git a/ui/src/core/i18n/resources/es.ts b/ui/src/core/i18n/resources/es.ts index 3333ab9..699a036 100644 --- a/ui/src/core/i18n/resources/es.ts +++ b/ui/src/core/i18n/resources/es.ts @@ -158,7 +158,6 @@ export const es = { }, descriptions: { emptyDeck: 'Añade una nota para que este mazo tenga material de repaso.', - editorDefault: 'Mazo de estudio enfocado.', editorVisual: 'Elige un glifo de portada para este mazo.', notesSearchPlaceholder: 'Buscar notas…', }, @@ -190,8 +189,6 @@ export const es = { descriptionLabel: 'Descripción del mazo', descriptionPlaceholder: '¿Qué te ayudará a repasar este mazo?', namePlaceholder: 'Nombre del mazo', - untitledDeck: 'Mazo sin título', - untitledDeckLower: 'mazo sin título', }, labels: { createDeckTitle: 'Crear mazo', @@ -221,20 +218,31 @@ export const es = { timeout: 'Esto tardó demasiado. Inténtalo de nuevo.', unauthorized: 'Inicia sesión para continuar.', unavailable: 'El servicio no está disponible temporalmente.', + validation: 'Revisa los campos resaltados e inténtalo de nuevo.', }, fallback: { unexpected: 'Error inesperado', }, }, + forms: { + validation: { + invalid: '{{field}} no es válido.', + invalidEnum: 'Elige un {{field}} válido.', + invalidFormat: 'Introduce un {{field}} válido.', + maxLength: '{{field}} debe tener como máximo {{max}} caracteres.', + maximum: '{{field}} debe ser como máximo {{max}}.', + minLength: '{{field}} debe tener al menos {{min}} caracteres.', + minimum: '{{field}} debe ser al menos {{min}}.', + required: '{{field}} es obligatorio.', + }, + }, + folders: { actions: { createFolder: 'Crear carpeta', deleteFolder: 'Eliminar carpeta', editFolder: 'Editar carpeta', }, - descriptions: { - editorDefault: 'Carpeta para mazos relacionados.', - }, dialogs: { deleteFolderDescription: 'Esto mueve "{{name}}" a la Papelera. Puedes restaurarla más tarde.', deleteFolderFallbackDescription: 'Esto mueve esta carpeta a la Papelera. Puedes restaurarla más tarde.', @@ -256,7 +264,6 @@ export const es = { descriptionLabel: 'Descripción de la carpeta', descriptionPlaceholder: '¿Qué pertenece a esta carpeta?', namePlaceholder: 'Nombre de la carpeta', - untitledFolder: 'Carpeta sin título', }, labels: { createFolderTitle: 'Crear carpeta', @@ -586,7 +593,6 @@ export const es = { openingWorkspace: 'Abriendo {{title}}', }, descriptions: { - editorDefault: 'Contexto de estudio.', editorVisual: 'Elige un ancla visual para este espacio de trabajo.', emptyList: 'Separa mazos, notas y colas de repaso por contexto de estudio.', }, @@ -611,7 +617,6 @@ export const es = { descriptionLabel: 'Descripción del espacio de trabajo', descriptionPlaceholder: '¿Qué pertenece a este espacio de trabajo?', namePlaceholder: 'Nombre del espacio de trabajo', - untitledWorkspace: 'Espacio de trabajo sin título', }, labels: { createWorkspaceTitle: 'Crear espacio de trabajo', diff --git a/ui/src/core/i18n/resources/et.ts b/ui/src/core/i18n/resources/et.ts index 0a71d5c..2a050a4 100644 --- a/ui/src/core/i18n/resources/et.ts +++ b/ui/src/core/i18n/resources/et.ts @@ -158,7 +158,6 @@ export const et = { }, descriptions: { emptyDeck: 'Lisa märge, et sellel pakil oleks kordamiseks materjali.', - editorDefault: 'Fookustatud õppepakk.', editorVisual: 'Vali selle paki kaanesümbol.', notesSearchPlaceholder: 'Otsi märkmeid…', }, @@ -190,8 +189,6 @@ export const et = { descriptionLabel: 'Paki kirjeldus', descriptionPlaceholder: 'Mida see pakk aitab sul korrata?', namePlaceholder: 'Paki nimi', - untitledDeck: 'Pealkirjata pakk', - untitledDeckLower: 'pealkirjata pakk', }, labels: { createDeckTitle: 'Loo pakk', @@ -221,20 +218,31 @@ export const et = { timeout: 'See võttis liiga kaua aega. Proovi uuesti.', unauthorized: 'Jätkamiseks logi sisse.', unavailable: 'Teenus pole ajutiselt saadaval.', + validation: 'Kontrolli esiletõstetud välju ja proovi uuesti.', }, fallback: { unexpected: 'Ootamatu tõrge', }, }, + forms: { + validation: { + invalid: '{{field}} on vigane.', + invalidEnum: 'Vali kehtiv väärtus väljale {{field}}.', + invalidFormat: 'Sisesta kehtiv väärtus väljale {{field}}.', + maxLength: '{{field}} võib olla kuni {{max}} märki.', + maximum: '{{field}} võib olla kuni {{max}}.', + minLength: '{{field}} peab olema vähemalt {{min}} märki.', + minimum: '{{field}} peab olema vähemalt {{min}}.', + required: '{{field}} on kohustuslik.', + }, + }, + folders: { actions: { createFolder: 'Loo kaust', deleteFolder: 'Kustuta kaust', editFolder: 'Muuda kausta', }, - descriptions: { - editorDefault: 'Kaust seotud pakkide jaoks.', - }, dialogs: { deleteFolderDescription: 'See teisaldab "{{name}}" prügikasti. Saad selle hiljem taastada.', deleteFolderFallbackDescription: 'See teisaldab selle kausta prügikasti. Saad selle hiljem taastada.', @@ -256,7 +264,6 @@ export const et = { descriptionLabel: 'Kausta kirjeldus', descriptionPlaceholder: 'Mis sellesse kausta kuulub?', namePlaceholder: 'Kausta nimi', - untitledFolder: 'Pealkirjata kaust', }, labels: { createFolderTitle: 'Loo kaust', @@ -586,7 +593,6 @@ export const et = { openingWorkspace: '{{title}} avamine', }, descriptions: { - editorDefault: 'Õppekontekst.', editorVisual: 'Vali selle tööruumi visuaalne ankur.', emptyList: 'Eralda pakid, märkmed ja kordamisjärjekorrad õppekonteksti järgi.', }, @@ -611,7 +617,6 @@ export const et = { descriptionLabel: 'Tööruumi kirjeldus', descriptionPlaceholder: 'Mis sellesse tööruumi kuulub?', namePlaceholder: 'Tööruumi nimi', - untitledWorkspace: 'Pealkirjata tööruum', }, labels: { createWorkspaceTitle: 'Loo tööruum', diff --git a/ui/src/core/i18n/resources/fa.ts b/ui/src/core/i18n/resources/fa.ts index 667267b..6c2d81d 100644 --- a/ui/src/core/i18n/resources/fa.ts +++ b/ui/src/core/i18n/resources/fa.ts @@ -158,7 +158,6 @@ export const fa = { }, descriptions: { emptyDeck: 'یک یادداشت اضافه کنید تا این دسته محتوایی برای مرور داشته باشد.', - editorDefault: 'دسته مطالعه متمرکز.', editorVisual: 'یک نماد جلد برای این دسته انتخاب کنید.', notesSearchPlaceholder: 'جستجو در یادداشت‌ها…', }, @@ -190,8 +189,6 @@ export const fa = { descriptionLabel: 'توضیحات دسته', descriptionPlaceholder: 'این دسته به مرور چه چیزی کمک می‌کند؟', namePlaceholder: 'نام دسته', - untitledDeck: 'دسته بدون عنوان', - untitledDeckLower: 'دسته بدون عنوان', }, labels: { createDeckTitle: 'ایجاد دسته', @@ -221,20 +218,31 @@ export const fa = { timeout: 'این کار بیش از حد طول کشید. دوباره تلاش کنید.', unauthorized: 'برای ادامه وارد شوید.', unavailable: 'سرویس موقتا در دسترس نیست.', + validation: 'فیلدهای برجسته‌شده را بررسی کنید و دوباره تلاش کنید.', }, fallback: { unexpected: 'خطای غیرمنتظره', }, }, + forms: { + validation: { + invalid: '{{field}} نامعتبر است.', + invalidEnum: 'یک مقدار معتبر برای {{field}} انتخاب کنید.', + invalidFormat: 'یک مقدار معتبر برای {{field}} وارد کنید.', + maxLength: '{{field}} باید حداکثر {{max}} نویسه باشد.', + maximum: '{{field}} باید حداکثر {{max}} باشد.', + minLength: '{{field}} باید حداقل {{min}} نویسه باشد.', + minimum: '{{field}} باید حداقل {{min}} باشد.', + required: '{{field}} الزامی است.', + }, + }, + folders: { actions: { createFolder: 'ایجاد پوشه', deleteFolder: 'حذف پوشه', editFolder: 'ویرایش پوشه', }, - descriptions: { - editorDefault: 'پوشه‌ای برای دسته‌های مرتبط.', - }, dialogs: { deleteFolderDescription: '"{{name}}" به سطل زباله منتقل می‌شود. بعدا می‌توانید آن را بازیابی کنید.', deleteFolderFallbackDescription: 'این پوشه به سطل زباله منتقل می‌شود. بعدا می‌توانید آن را بازیابی کنید.', @@ -256,7 +264,6 @@ export const fa = { descriptionLabel: 'توضیحات پوشه', descriptionPlaceholder: 'چه چیزی در این پوشه قرار می‌گیرد؟', namePlaceholder: 'نام پوشه', - untitledFolder: 'پوشه بدون عنوان', }, labels: { createFolderTitle: 'ایجاد پوشه', @@ -586,7 +593,6 @@ export const fa = { openingWorkspace: 'در حال باز کردن {{title}}', }, descriptions: { - editorDefault: 'زمینه مطالعه.', editorVisual: 'یک نشانه بصری برای این فضای کاری انتخاب کنید.', emptyList: 'دسته‌ها، یادداشت‌ها و صف‌های مرور را بر اساس زمینه مطالعه جدا کنید.', }, @@ -611,7 +617,6 @@ export const fa = { descriptionLabel: 'توضیحات فضای کاری', descriptionPlaceholder: 'چه چیزی در این فضای کاری قرار می‌گیرد؟', namePlaceholder: 'نام فضای کاری', - untitledWorkspace: 'فضای کاری بدون عنوان', }, labels: { createWorkspaceTitle: 'ایجاد فضای کاری', diff --git a/ui/src/core/i18n/resources/fi.ts b/ui/src/core/i18n/resources/fi.ts index 540de32..2f52ab6 100644 --- a/ui/src/core/i18n/resources/fi.ts +++ b/ui/src/core/i18n/resources/fi.ts @@ -158,7 +158,6 @@ export const fi = { }, descriptions: { emptyDeck: 'Lisää muistiinpano, jotta tässä pakassa on kerrattavaa.', - editorDefault: 'Keskittynyt opiskelupakka.', editorVisual: 'Valitse tälle pakalle kansikuvake.', notesSearchPlaceholder: 'Hae muistiinpanoja…', }, @@ -190,8 +189,6 @@ export const fi = { descriptionLabel: 'Pakan kuvaus', descriptionPlaceholder: 'Mitä tämä pakka auttaa sinua kertaamaan?', namePlaceholder: 'Pakan nimi', - untitledDeck: 'Nimetön pakka', - untitledDeckLower: 'nimetön pakka', }, labels: { createDeckTitle: 'Luo pakka', @@ -221,20 +218,31 @@ export const fi = { timeout: 'Tässä kesti liian kauan. Yritä uudelleen.', unauthorized: 'Kirjaudu sisään jatkaaksesi.', unavailable: 'Palvelu ei ole tilapäisesti saatavilla.', + validation: 'Tarkista korostetut kentät ja yritä uudelleen.', }, fallback: { unexpected: 'Odottamaton virhe', }, }, + forms: { + validation: { + invalid: '{{field}} ei ole kelvollinen.', + invalidEnum: 'Valitse kelvollinen arvo kentälle {{field}}.', + invalidFormat: 'Syötä kelvollinen arvo kentälle {{field}}.', + maxLength: '{{field}} saa olla enintään {{max}} merkkiä.', + maximum: '{{field}} saa olla enintään {{max}}.', + minLength: '{{field}} on oltava vähintään {{min}} merkkiä.', + minimum: '{{field}} on oltava vähintään {{min}}.', + required: '{{field}} on pakollinen.', + }, + }, + folders: { actions: { createFolder: 'Luo kansio', deleteFolder: 'Poista kansio', editFolder: 'Muokkaa kansiota', }, - descriptions: { - editorDefault: 'Kansio toisiinsa liittyville pakoille.', - }, dialogs: { deleteFolderDescription: 'Tämä siirtää kohteen "{{name}}" roskakoriin. Voit palauttaa sen myöhemmin.', deleteFolderFallbackDescription: 'Tämä siirtää tämän kansion roskakoriin. Voit palauttaa sen myöhemmin.', @@ -256,7 +264,6 @@ export const fi = { descriptionLabel: 'Kansion kuvaus', descriptionPlaceholder: 'Mitä tähän kansioon kuuluu?', namePlaceholder: 'Kansion nimi', - untitledFolder: 'Nimetön kansio', }, labels: { createFolderTitle: 'Luo kansio', @@ -586,7 +593,6 @@ export const fi = { openingWorkspace: 'Avataan {{title}}', }, descriptions: { - editorDefault: 'Opiskelukonteksti.', editorVisual: 'Valitse tälle työtilalle visuaalinen ankkuri.', emptyList: 'Erota pakat, muistiinpanot ja kertausjonot opiskelukontekstin mukaan.', }, @@ -611,7 +617,6 @@ export const fi = { descriptionLabel: 'Työtilan kuvaus', descriptionPlaceholder: 'Mitä tähän työtilaan kuuluu?', namePlaceholder: 'Työtilan nimi', - untitledWorkspace: 'Nimetön työtila', }, labels: { createWorkspaceTitle: 'Luo työtila', diff --git a/ui/src/core/i18n/resources/fr.ts b/ui/src/core/i18n/resources/fr.ts index bb780f1..230eb8d 100644 --- a/ui/src/core/i18n/resources/fr.ts +++ b/ui/src/core/i18n/resources/fr.ts @@ -158,7 +158,6 @@ export const fr = { }, descriptions: { emptyDeck: 'Ajoutez une note pour donner du contenu à réviser à ce paquet.', - editorDefault: 'Paquet d’étude ciblé.', editorVisual: 'Choisissez un glyphe de couverture pour ce paquet.', notesSearchPlaceholder: 'Rechercher des notes…', }, @@ -190,8 +189,6 @@ export const fr = { descriptionLabel: 'Description du paquet', descriptionPlaceholder: 'Que ce paquet vous aidera-t-il à réviser ?', namePlaceholder: 'Nom du paquet', - untitledDeck: 'Paquet sans titre', - untitledDeckLower: 'paquet sans titre', }, labels: { createDeckTitle: 'Créer un paquet', @@ -221,20 +218,31 @@ export const fr = { timeout: 'Cela a pris trop de temps. Réessayez.', unauthorized: 'Connectez-vous pour continuer.', unavailable: 'Le service est temporairement indisponible.', + validation: 'Vérifiez les champs en surbrillance puis réessayez.', }, fallback: { unexpected: 'Erreur inattendue', }, }, + forms: { + validation: { + invalid: '{{field}} est invalide.', + invalidEnum: 'Choisissez une valeur valide pour {{field}}.', + invalidFormat: 'Saisissez une valeur valide pour {{field}}.', + maxLength: '{{field}} doit contenir au plus {{max}} caractères.', + maximum: '{{field}} doit être au maximum {{max}}.', + minLength: '{{field}} doit contenir au moins {{min}} caractères.', + minimum: '{{field}} doit être au moins {{min}}.', + required: '{{field}} est obligatoire.', + }, + }, + folders: { actions: { createFolder: 'Créer un dossier', deleteFolder: 'Supprimer le dossier', editFolder: 'Modifier le dossier', }, - descriptions: { - editorDefault: 'Dossier pour paquets associés.', - }, dialogs: { deleteFolderDescription: 'Cela déplace "{{name}}" vers la corbeille. Vous pourrez le restaurer plus tard.', deleteFolderFallbackDescription: 'Cela déplace ce dossier vers la corbeille. Vous pourrez le restaurer plus tard.', @@ -256,7 +264,6 @@ export const fr = { descriptionLabel: 'Description du dossier', descriptionPlaceholder: 'Qu’est-ce qui appartient à ce dossier ?', namePlaceholder: 'Nom du dossier', - untitledFolder: 'Dossier sans titre', }, labels: { createFolderTitle: 'Créer un dossier', @@ -586,7 +593,6 @@ export const fr = { openingWorkspace: 'Ouverture de {{title}}', }, descriptions: { - editorDefault: 'Contexte d’étude.', editorVisual: 'Choisissez un repère visuel pour cet espace de travail.', emptyList: 'Séparez les paquets, les notes et les files de révision par contexte d’étude.', }, @@ -611,7 +617,6 @@ export const fr = { descriptionLabel: 'Description de l’espace de travail', descriptionPlaceholder: 'Qu’est-ce qui appartient à cet espace de travail ?', namePlaceholder: 'Nom de l’espace de travail', - untitledWorkspace: 'Espace de travail sans titre', }, labels: { createWorkspaceTitle: 'Créer un espace de travail', diff --git a/ui/src/core/i18n/resources/he.ts b/ui/src/core/i18n/resources/he.ts index e90b8f9..d2321dd 100644 --- a/ui/src/core/i18n/resources/he.ts +++ b/ui/src/core/i18n/resources/he.ts @@ -168,7 +168,6 @@ export const he = { }, descriptions: { emptyDeck: 'הוסיפו הערה כדי שלחפיסה הזו יהיה חומר לסקירה.', - editorDefault: 'חפיסת לימוד ממוקדת.', editorVisual: 'בחרו סמל כיסוי לחפיסה הזו.', notesSearchPlaceholder: 'חיפוש הערות…', }, @@ -200,8 +199,6 @@ export const he = { descriptionLabel: 'תיאור החפיסה', descriptionPlaceholder: 'מה החפיסה הזו תעזור לכם לסקור?', namePlaceholder: 'שם החפיסה', - untitledDeck: 'חפיסה ללא שם', - untitledDeckLower: 'חפיסה ללא שם', }, labels: { createDeckTitle: 'יצירת חפיסה', @@ -231,20 +228,31 @@ export const he = { timeout: 'זה נמשך יותר מדי זמן. נסו שוב.', unauthorized: 'התחברו כדי להמשיך.', unavailable: 'השירות אינו זמין זמנית.', + validation: 'בדוק את השדות המסומנים ונסה שוב.', }, fallback: { unexpected: 'שגיאה בלתי צפויה', }, }, + forms: { + validation: { + invalid: '{{field}} אינו תקין.', + invalidEnum: 'בחר ערך תקין עבור {{field}}.', + invalidFormat: 'הזן ערך תקין עבור {{field}}.', + maxLength: '{{field}} חייב להכיל לכל היותר {{max}} תווים.', + maximum: '{{field}} חייב להיות לכל היותר {{max}}.', + minLength: '{{field}} חייב להכיל לפחות {{min}} תווים.', + minimum: '{{field}} חייב להיות לפחות {{min}}.', + required: '{{field}} הוא שדה חובה.', + }, + }, + folders: { actions: { createFolder: 'יצירת תיקייה', deleteFolder: 'מחיקת תיקייה', editFolder: 'עריכת תיקייה', }, - descriptions: { - editorDefault: 'תיקייה לחפיסות קשורות.', - }, dialogs: { deleteFolderDescription: '"{{name}}" תועבר לאשפה. ניתן לשחזר אותה מאוחר יותר.', deleteFolderFallbackDescription: 'התיקייה הזו תועבר לאשפה. ניתן לשחזר אותה מאוחר יותר.', @@ -266,7 +274,6 @@ export const he = { descriptionLabel: 'תיאור התיקייה', descriptionPlaceholder: 'מה שייך לתיקייה הזו?', namePlaceholder: 'שם התיקייה', - untitledFolder: 'תיקייה ללא שם', }, labels: { createFolderTitle: 'יצירת תיקייה', @@ -599,7 +606,6 @@ export const he = { openingWorkspace: '{{title}} נפתח', }, descriptions: { - editorDefault: 'הקשר לימוד.', editorVisual: 'בחרו עוגן חזותי למרחב העבודה הזה.', emptyList: 'הפרידו חפיסות, הערות ותורי סקירה לפי הקשר לימוד.', }, @@ -624,7 +630,6 @@ export const he = { descriptionLabel: 'תיאור מרחב העבודה', descriptionPlaceholder: 'מה שייך למרחב העבודה הזה?', namePlaceholder: 'שם מרחב העבודה', - untitledWorkspace: 'מרחב עבודה ללא שם', }, labels: { createWorkspaceTitle: 'יצירת מרחב עבודה', diff --git a/ui/src/core/i18n/resources/hr.ts b/ui/src/core/i18n/resources/hr.ts index debad50..6b17495 100644 --- a/ui/src/core/i18n/resources/hr.ts +++ b/ui/src/core/i18n/resources/hr.ts @@ -168,7 +168,6 @@ export const hr = { }, descriptions: { emptyDeck: 'Dodajte bilješku kako bi ovaj špil imao materijal za ponavljanje.', - editorDefault: 'Fokusirani špil za učenje.', editorVisual: 'Odaberite simbol naslovnice za ovaj špil.', notesSearchPlaceholder: 'Pretraži bilješke…', }, @@ -200,8 +199,6 @@ export const hr = { descriptionLabel: 'Opis špila', descriptionPlaceholder: 'Što će vam ovaj špil pomoći ponavljati?', namePlaceholder: 'Naziv špila', - untitledDeck: 'Špil bez naslova', - untitledDeckLower: 'špil bez naslova', }, labels: { createDeckTitle: 'Stvori špil', @@ -231,20 +228,31 @@ export const hr = { timeout: 'Ovo je trajalo predugo. Pokušajte ponovno.', unauthorized: 'Prijavite se za nastavak.', unavailable: 'Usluga je privremeno nedostupna.', + validation: 'Provjerite označena polja i pokušajte ponovno.', }, fallback: { unexpected: 'Neočekivana pogreška', }, }, + forms: { + validation: { + invalid: '{{field}} nije valjano.', + invalidEnum: 'Odaberite valjanu vrijednost za {{field}}.', + invalidFormat: 'Unesite valjanu vrijednost za {{field}}.', + maxLength: '{{field}} smije imati najviše {{max}} znakova.', + maximum: '{{field}} mora biti najviše {{max}}.', + minLength: '{{field}} mora imati najmanje {{min}} znakova.', + minimum: '{{field}} mora biti najmanje {{min}}.', + required: '{{field}} je obavezno.', + }, + }, + folders: { actions: { createFolder: 'Stvori mapu', deleteFolder: 'Izbriši mapu', editFolder: 'Uredi mapu', }, - descriptions: { - editorDefault: 'Mapa za povezane špilove.', - }, dialogs: { deleteFolderDescription: 'Ovo premješta "{{name}}" u Smeće. Možete je vratiti kasnije.', deleteFolderFallbackDescription: 'Ovo premješta ovu mapu u Smeće. Možete je vratiti kasnije.', @@ -266,7 +274,6 @@ export const hr = { descriptionLabel: 'Opis mape', descriptionPlaceholder: 'Što pripada u ovu mapu?', namePlaceholder: 'Naziv mape', - untitledFolder: 'Mapa bez naslova', }, labels: { createFolderTitle: 'Stvori mapu', @@ -599,7 +606,6 @@ export const hr = { openingWorkspace: 'Otvaranje {{title}}', }, descriptions: { - editorDefault: 'Kontekst učenja.', editorVisual: 'Odaberite vizualno sidro za ovaj radni prostor.', emptyList: 'Odvojite špilove, bilješke i redove za ponavljanje prema kontekstu učenja.', }, @@ -624,7 +630,6 @@ export const hr = { descriptionLabel: 'Opis radnog prostora', descriptionPlaceholder: 'Što pripada u ovaj radni prostor?', namePlaceholder: 'Naziv radnog prostora', - untitledWorkspace: 'Radni prostor bez naslova', }, labels: { createWorkspaceTitle: 'Stvori radni prostor', diff --git a/ui/src/core/i18n/resources/hu.ts b/ui/src/core/i18n/resources/hu.ts index 90aa9c9..127f1af 100644 --- a/ui/src/core/i18n/resources/hu.ts +++ b/ui/src/core/i18n/resources/hu.ts @@ -158,7 +158,6 @@ export const hu = { }, descriptions: { emptyDeck: 'Adjon hozzá jegyzetet, hogy legyen mit ismételni ebben a pakliban.', - editorDefault: 'Fókuszált tanulási pakli.', editorVisual: 'Válasszon borítójelet ehhez a paklihoz.', notesSearchPlaceholder: 'Jegyzetek keresése…', }, @@ -190,8 +189,6 @@ export const hu = { descriptionLabel: 'Pakli leírása', descriptionPlaceholder: 'Mit segít ismételni ez a pakli?', namePlaceholder: 'Pakli neve', - untitledDeck: 'Névtelen pakli', - untitledDeckLower: 'névtelen pakli', }, labels: { createDeckTitle: 'Pakli létrehozása', @@ -221,20 +218,31 @@ export const hu = { timeout: 'Ez túl sokáig tartott. Próbálja újra.', unauthorized: 'A folytatáshoz jelentkezzen be.', unavailable: 'A szolgáltatás átmenetileg nem érhető el.', + validation: 'Ellenőrizd a kiemelt mezőket, majd próbáld újra.', }, fallback: { unexpected: 'Váratlan hiba', }, }, + forms: { + validation: { + invalid: '{{field}} érvénytelen.', + invalidEnum: 'Válassz érvényes értéket ehhez: {{field}}.', + invalidFormat: 'Adj meg érvényes értéket ehhez: {{field}}.', + maxLength: '{{field}} legfeljebb {{max}} karakter lehet.', + maximum: '{{field}} legfeljebb {{max}} lehet.', + minLength: '{{field}} legalább {{min}} karakter legyen.', + minimum: '{{field}} legalább {{min}} legyen.', + required: '{{field}} kötelező.', + }, + }, + folders: { actions: { createFolder: 'Mappa létrehozása', deleteFolder: 'Mappa törlése', editFolder: 'Mappa szerkesztése', }, - descriptions: { - editorDefault: 'Mappa kapcsolódó paklikhoz.', - }, dialogs: { deleteFolderDescription: 'Ez áthelyezi a következőt a Kukába: "{{name}}". Később visszaállítható.', deleteFolderFallbackDescription: 'Ez áthelyezi ezt a mappát a Kukába. Később visszaállítható.', @@ -256,7 +264,6 @@ export const hu = { descriptionLabel: 'Mappa leírása', descriptionPlaceholder: 'Mi tartozik ebbe a mappába?', namePlaceholder: 'Mappa neve', - untitledFolder: 'Névtelen mappa', }, labels: { createFolderTitle: 'Mappa létrehozása', @@ -586,7 +593,6 @@ export const hu = { openingWorkspace: '{{title}} megnyitása', }, descriptions: { - editorDefault: 'Tanulási kontextus.', editorVisual: 'Válasszon vizuális horgonyt ehhez a munkaterülethez.', emptyList: 'Válassza szét a paklikat, jegyzeteket és ismétlési sorokat tanulási kontextus szerint.', }, @@ -611,7 +617,6 @@ export const hu = { descriptionLabel: 'Munkaterület leírása', descriptionPlaceholder: 'Mi tartozik ebbe a munkaterületbe?', namePlaceholder: 'Munkaterület neve', - untitledWorkspace: 'Névtelen munkaterület', }, labels: { createWorkspaceTitle: 'Munkaterület létrehozása', diff --git a/ui/src/core/i18n/resources/id.ts b/ui/src/core/i18n/resources/id.ts index 3b488b7..9a08d42 100644 --- a/ui/src/core/i18n/resources/id.ts +++ b/ui/src/core/i18n/resources/id.ts @@ -158,7 +158,6 @@ export const id = { }, descriptions: { emptyDeck: 'Tambahkan catatan agar deck ini memiliki materi untuk diulas.', - editorDefault: 'Deck belajar terfokus.', editorVisual: 'Pilih glif sampul untuk deck ini.', notesSearchPlaceholder: 'Cari catatan…', }, @@ -190,8 +189,6 @@ export const id = { descriptionLabel: 'Deskripsi deck', descriptionPlaceholder: 'Apa yang akan dibantu deck ini untuk Anda ulas?', namePlaceholder: 'Nama deck', - untitledDeck: 'Deck tanpa judul', - untitledDeckLower: 'deck tanpa judul', }, labels: { createDeckTitle: 'Buat deck', @@ -221,20 +218,31 @@ export const id = { timeout: 'Ini terlalu lama. Coba lagi.', unauthorized: 'Masuk untuk melanjutkan.', unavailable: 'Layanan sementara tidak tersedia.', + validation: 'Periksa bidang yang disorot lalu coba lagi.', }, fallback: { unexpected: 'Error tidak terduga', }, }, + forms: { + validation: { + invalid: '{{field}} tidak valid.', + invalidEnum: 'Pilih {{field}} yang valid.', + invalidFormat: 'Masukkan {{field}} yang valid.', + maxLength: '{{field}} maksimal {{max}} karakter.', + maximum: '{{field}} maksimal {{max}}.', + minLength: '{{field}} minimal {{min}} karakter.', + minimum: '{{field}} minimal {{min}}.', + required: '{{field}} wajib diisi.', + }, + }, + folders: { actions: { createFolder: 'Buat folder', deleteFolder: 'Hapus folder', editFolder: 'Edit folder', }, - descriptions: { - editorDefault: 'Folder untuk deck terkait.', - }, dialogs: { deleteFolderDescription: 'Ini memindahkan "{{name}}" ke Sampah. Anda dapat memulihkannya nanti.', deleteFolderFallbackDescription: 'Ini memindahkan folder ini ke Sampah. Anda dapat memulihkannya nanti.', @@ -256,7 +264,6 @@ export const id = { descriptionLabel: 'Deskripsi folder', descriptionPlaceholder: 'Apa yang termasuk dalam folder ini?', namePlaceholder: 'Nama folder', - untitledFolder: 'Folder tanpa judul', }, labels: { createFolderTitle: 'Buat folder', @@ -586,7 +593,6 @@ export const id = { openingWorkspace: 'Membuka {{title}}', }, descriptions: { - editorDefault: 'Konteks belajar.', editorVisual: 'Pilih jangkar visual untuk workspace ini.', emptyList: 'Pisahkan deck, catatan, dan antrean ulasan berdasarkan konteks belajar.', }, @@ -611,7 +617,6 @@ export const id = { descriptionLabel: 'Deskripsi workspace', descriptionPlaceholder: 'Apa yang termasuk dalam workspace ini?', namePlaceholder: 'Nama workspace', - untitledWorkspace: 'Workspace tanpa judul', }, labels: { createWorkspaceTitle: 'Buat workspace', diff --git a/ui/src/core/i18n/resources/it.ts b/ui/src/core/i18n/resources/it.ts index cbec46c..6ec92e7 100644 --- a/ui/src/core/i18n/resources/it.ts +++ b/ui/src/core/i18n/resources/it.ts @@ -168,7 +168,6 @@ export const it = { }, descriptions: { emptyDeck: 'Aggiungi una nota per dare a questo mazzo materiale da ripassare.', - editorDefault: 'Mazzo di studio focalizzato.', editorVisual: 'Scegli un glifo di copertina per questo mazzo.', notesSearchPlaceholder: 'Cerca note…', }, @@ -200,8 +199,6 @@ export const it = { descriptionLabel: 'Descrizione del mazzo', descriptionPlaceholder: 'Cosa ti aiuterà a ripassare questo mazzo?', namePlaceholder: 'Nome del mazzo', - untitledDeck: 'Mazzo senza titolo', - untitledDeckLower: 'mazzo senza titolo', }, labels: { createDeckTitle: 'Crea mazzo', @@ -231,20 +228,31 @@ export const it = { timeout: 'Ci sta mettendo troppo tempo. Riprova.', unauthorized: 'Accedi per continuare.', unavailable: 'Il servizio è temporaneamente non disponibile.', + validation: 'Controlla i campi evidenziati e riprova.', }, fallback: { unexpected: 'Errore imprevisto', }, }, + forms: { + validation: { + invalid: '{{field}} non è valido.', + invalidEnum: 'Scegli un valore valido per {{field}}.', + invalidFormat: 'Inserisci un valore valido per {{field}}.', + maxLength: '{{field}} deve contenere al massimo {{max}} caratteri.', + maximum: '{{field}} deve essere al massimo {{max}}.', + minLength: '{{field}} deve contenere almeno {{min}} caratteri.', + minimum: '{{field}} deve essere almeno {{min}}.', + required: '{{field}} è obbligatorio.', + }, + }, + folders: { actions: { createFolder: 'Crea cartella', deleteFolder: 'Elimina cartella', editFolder: 'Modifica cartella', }, - descriptions: { - editorDefault: 'Cartella per mazzi correlati.', - }, dialogs: { deleteFolderDescription: 'Questo sposta "{{name}}" nel Cestino. Puoi ripristinarla più tardi.', deleteFolderFallbackDescription: 'Questo sposta questa cartella nel Cestino. Puoi ripristinarla più tardi.', @@ -266,7 +274,6 @@ export const it = { descriptionLabel: 'Descrizione della cartella', descriptionPlaceholder: 'Cosa appartiene a questa cartella?', namePlaceholder: 'Nome della cartella', - untitledFolder: 'Cartella senza titolo', }, labels: { createFolderTitle: 'Crea cartella', @@ -599,7 +606,6 @@ export const it = { openingWorkspace: 'Apertura di {{title}}', }, descriptions: { - editorDefault: 'Contesto di studio.', editorVisual: 'Scegli un riferimento visivo per questo spazio di lavoro.', emptyList: 'Separa mazzi, note e code di ripasso per contesto di studio.', }, @@ -624,7 +630,6 @@ export const it = { descriptionLabel: 'Descrizione dello spazio di lavoro', descriptionPlaceholder: 'Cosa appartiene a questo spazio di lavoro?', namePlaceholder: 'Nome dello spazio di lavoro', - untitledWorkspace: 'Spazio di lavoro senza titolo', }, labels: { createWorkspaceTitle: 'Crea spazio di lavoro', diff --git a/ui/src/core/i18n/resources/ja.ts b/ui/src/core/i18n/resources/ja.ts index f889300..56f2e90 100644 --- a/ui/src/core/i18n/resources/ja.ts +++ b/ui/src/core/i18n/resources/ja.ts @@ -158,7 +158,6 @@ export const ja = { }, descriptions: { emptyDeck: '復習する内容を作るためにノートを追加してください。', - editorDefault: '集中学習用のデッキ。', editorVisual: 'このデッキのカバーグリフを選びます。', notesSearchPlaceholder: 'ノートを検索…', }, @@ -190,8 +189,6 @@ export const ja = { descriptionLabel: 'デッキの説明', descriptionPlaceholder: 'このデッキで何を復習しますか?', namePlaceholder: 'デッキ名', - untitledDeck: '無題のデッキ', - untitledDeckLower: '無題のデッキ', }, labels: { createDeckTitle: 'デッキを作成', @@ -221,20 +218,31 @@ export const ja = { timeout: '時間がかかりすぎました。もう一度お試しください。', unauthorized: '続行するにはサインインしてください。', unavailable: 'サービスは一時的に利用できません。', + validation: '強調表示された項目を確認して、もう一度試してください。', }, fallback: { unexpected: '予期しないエラー', }, }, + forms: { + validation: { + invalid: '{{field}}が無効です。', + invalidEnum: '有効な{{field}}を選択してください。', + invalidFormat: '有効な{{field}}を入力してください。', + maxLength: '{{field}}は{{max}}文字以内にしてください。', + maximum: '{{field}}は{{max}}以下にしてください。', + minLength: '{{field}}は{{min}}文字以上にしてください。', + minimum: '{{field}}は{{min}}以上にしてください。', + required: '{{field}}は必須です。', + }, + }, + folders: { actions: { createFolder: 'フォルダーを作成', deleteFolder: 'フォルダーを削除', editFolder: 'フォルダーを編集', }, - descriptions: { - editorDefault: '関連するデッキ用のフォルダー。', - }, dialogs: { deleteFolderDescription: '"{{name}}" をゴミ箱に移動します。後で復元できます。', deleteFolderFallbackDescription: 'このフォルダーをゴミ箱に移動します。後で復元できます。', @@ -256,7 +264,6 @@ export const ja = { descriptionLabel: 'フォルダーの説明', descriptionPlaceholder: 'このフォルダーには何を入れますか?', namePlaceholder: 'フォルダー名', - untitledFolder: '無題のフォルダー', }, labels: { createFolderTitle: 'フォルダーを作成', @@ -586,7 +593,6 @@ export const ja = { openingWorkspace: '{{title}} を開いています', }, descriptions: { - editorDefault: '学習コンテキスト。', editorVisual: 'このワークスペースの視覚的な目印を選びます。', emptyList: 'デッキ、ノート、復習キューを学習コンテキストごとに分けます。', }, @@ -611,7 +617,6 @@ export const ja = { descriptionLabel: 'ワークスペースの説明', descriptionPlaceholder: 'このワークスペースには何を入れますか?', namePlaceholder: 'ワークスペース名', - untitledWorkspace: '無題のワークスペース', }, labels: { createWorkspaceTitle: 'ワークスペースを作成', diff --git a/ui/src/core/i18n/resources/ko.ts b/ui/src/core/i18n/resources/ko.ts index 1a3580f..361e803 100644 --- a/ui/src/core/i18n/resources/ko.ts +++ b/ui/src/core/i18n/resources/ko.ts @@ -158,7 +158,6 @@ export const ko = { }, descriptions: { emptyDeck: '이 덱에 복습할 자료가 생기도록 노트를 추가하세요.', - editorDefault: '집중 학습 덱.', editorVisual: '이 덱의 표지 글리프를 선택하세요.', notesSearchPlaceholder: '노트 검색…', }, @@ -190,8 +189,6 @@ export const ko = { descriptionLabel: '덱 설명', descriptionPlaceholder: '이 덱은 무엇을 복습하는 데 도움이 되나요?', namePlaceholder: '덱 이름', - untitledDeck: '제목 없는 덱', - untitledDeckLower: '제목 없는 덱', }, labels: { createDeckTitle: '덱 만들기', @@ -221,20 +218,31 @@ export const ko = { timeout: '너무 오래 걸렸습니다. 다시 시도하세요.', unauthorized: '계속하려면 로그인하세요.', unavailable: '서비스를 일시적으로 사용할 수 없습니다.', + validation: '강조 표시된 필드를 확인한 후 다시 시도하세요.', }, fallback: { unexpected: '예상치 못한 오류', }, }, + forms: { + validation: { + invalid: '{{field}}이(가) 올바르지 않습니다.', + invalidEnum: '유효한 {{field}}을(를) 선택하세요.', + invalidFormat: '유효한 {{field}}을(를) 입력하세요.', + maxLength: '{{field}}은(는) 최대 {{max}}자여야 합니다.', + maximum: '{{field}}은(는) {{max}} 이하여야 합니다.', + minLength: '{{field}}은(는) 최소 {{min}}자여야 합니다.', + minimum: '{{field}}은(는) {{min}} 이상이어야 합니다.', + required: '{{field}}은(는) 필수입니다.', + }, + }, + folders: { actions: { createFolder: '폴더 만들기', deleteFolder: '폴더 삭제', editFolder: '폴더 편집', }, - descriptions: { - editorDefault: '관련 덱을 위한 폴더.', - }, dialogs: { deleteFolderDescription: '"{{name}}"을 휴지통으로 이동합니다. 나중에 복원할 수 있습니다.', deleteFolderFallbackDescription: '이 폴더를 휴지통으로 이동합니다. 나중에 복원할 수 있습니다.', @@ -256,7 +264,6 @@ export const ko = { descriptionLabel: '폴더 설명', descriptionPlaceholder: '이 폴더에는 무엇이 들어가나요?', namePlaceholder: '폴더 이름', - untitledFolder: '제목 없는 폴더', }, labels: { createFolderTitle: '폴더 만들기', @@ -586,7 +593,6 @@ export const ko = { openingWorkspace: '{{title}} 여는 중', }, descriptions: { - editorDefault: '학습 컨텍스트.', editorVisual: '이 워크스페이스의 시각적 기준을 선택하세요.', emptyList: '덱, 노트, 복습 대기열을 학습 컨텍스트별로 나누세요.', }, @@ -611,7 +617,6 @@ export const ko = { descriptionLabel: '워크스페이스 설명', descriptionPlaceholder: '이 워크스페이스에는 무엇이 들어가나요?', namePlaceholder: '워크스페이스 이름', - untitledWorkspace: '제목 없는 워크스페이스', }, labels: { createWorkspaceTitle: '워크스페이스 만들기', diff --git a/ui/src/core/i18n/resources/lt.ts b/ui/src/core/i18n/resources/lt.ts index 35e5954..ef6ba50 100644 --- a/ui/src/core/i18n/resources/lt.ts +++ b/ui/src/core/i18n/resources/lt.ts @@ -178,7 +178,6 @@ export const lt = { }, descriptions: { emptyDeck: 'Pridėkite pastabą, kad ši kaladė turėtų medžiagos kartojimui.', - editorDefault: 'Sutelktam mokymuisi skirta kaladė.', editorVisual: 'Pasirinkite šios kaladės viršelio simbolį.', notesSearchPlaceholder: 'Ieškoti pastabų…', }, @@ -210,8 +209,6 @@ export const lt = { descriptionLabel: 'Kaladės aprašas', descriptionPlaceholder: 'Ką ši kaladė padės jums kartoti?', namePlaceholder: 'Kaladės pavadinimas', - untitledDeck: 'Kaladė be pavadinimo', - untitledDeckLower: 'kaladė be pavadinimo', }, labels: { createDeckTitle: 'Sukurti kaladę', @@ -241,20 +238,31 @@ export const lt = { timeout: 'Tai užtruko per ilgai. Bandykite dar kartą.', unauthorized: 'Prisijunkite, kad tęstumėte.', unavailable: 'Paslauga laikinai nepasiekiama.', + validation: 'Patikrinkite pažymėtus laukus ir bandykite dar kartą.', }, fallback: { unexpected: 'Netikėta klaida', }, }, + forms: { + validation: { + invalid: '{{field}} yra netinkamas.', + invalidEnum: 'Pasirinkite tinkamą reikšmę laukui {{field}}.', + invalidFormat: 'Įveskite tinkamą reikšmę laukui {{field}}.', + maxLength: '{{field}} gali būti ne ilgesnis kaip {{max}} simbolių.', + maximum: '{{field}} turi būti ne daugiau kaip {{max}}.', + minLength: '{{field}} turi būti bent {{min}} simbolių.', + minimum: '{{field}} turi būti bent {{min}}.', + required: '{{field}} yra privalomas.', + }, + }, + folders: { actions: { createFolder: 'Sukurti aplanką', deleteFolder: 'Ištrinti aplanką', editFolder: 'Redaguoti aplanką', }, - descriptions: { - editorDefault: 'Aplankas susijusioms kaladėms.', - }, dialogs: { deleteFolderDescription: 'Tai perkels "{{name}}" į Šiukšlinę. Galėsite atkurti vėliau.', deleteFolderFallbackDescription: 'Tai perkels šį aplanką į Šiukšlinę. Galėsite atkurti vėliau.', @@ -276,7 +284,6 @@ export const lt = { descriptionLabel: 'Aplanko aprašas', descriptionPlaceholder: 'Kas priklauso šiam aplankui?', namePlaceholder: 'Aplanko pavadinimas', - untitledFolder: 'Aplankas be pavadinimo', }, labels: { createFolderTitle: 'Sukurti aplanką', @@ -612,7 +619,6 @@ export const lt = { openingWorkspace: 'Atidaroma {{title}}', }, descriptions: { - editorDefault: 'Mokymosi kontekstas.', editorVisual: 'Pasirinkite šios darbo srities vizualinį orientyrą.', emptyList: 'Atskirkite kalades, pastabas ir kartojimo eiles pagal mokymosi kontekstą.', }, @@ -637,7 +643,6 @@ export const lt = { descriptionLabel: 'Darbo srities aprašas', descriptionPlaceholder: 'Kas priklauso šiai darbo sričiai?', namePlaceholder: 'Darbo srities pavadinimas', - untitledWorkspace: 'Darbo sritis be pavadinimo', }, labels: { createWorkspaceTitle: 'Sukurti darbo sritį', diff --git a/ui/src/core/i18n/resources/lv.ts b/ui/src/core/i18n/resources/lv.ts index 17bddfa..2b70aa8 100644 --- a/ui/src/core/i18n/resources/lv.ts +++ b/ui/src/core/i18n/resources/lv.ts @@ -168,7 +168,6 @@ export const lv = { }, descriptions: { emptyDeck: 'Pievienojiet piezīmi, lai šai kavai būtu materiāls atkārtošanai.', - editorDefault: 'Mērķēta mācību kava.', editorVisual: 'Izvēlieties vāka simbolu šai kavai.', notesSearchPlaceholder: 'Meklēt piezīmes…', }, @@ -200,8 +199,6 @@ export const lv = { descriptionLabel: 'Kavas apraksts', descriptionPlaceholder: 'Ko šī kava palīdzēs atkārtot?', namePlaceholder: 'Kavas nosaukums', - untitledDeck: 'Kava bez nosaukuma', - untitledDeckLower: 'kava bez nosaukuma', }, labels: { createDeckTitle: 'Izveidot kavu', @@ -231,20 +228,31 @@ export const lv = { timeout: 'Tas aizņēma pārāk ilgu laiku. Mēģiniet vēlreiz.', unauthorized: 'Pierakstieties, lai turpinātu.', unavailable: 'Pakalpojums īslaicīgi nav pieejams.', + validation: 'Pārbaudiet iezīmētos laukus un mēģiniet vēlreiz.', }, fallback: { unexpected: 'Negaidīta kļūda', }, }, + forms: { + validation: { + invalid: '{{field}} nav derīgs.', + invalidEnum: 'Izvēlieties derīgu vērtību laukam {{field}}.', + invalidFormat: 'Ievadiet derīgu vērtību laukam {{field}}.', + maxLength: '{{field}} drīkst būt ne vairāk kā {{max}} rakstzīmes.', + maximum: '{{field}} jābūt ne vairāk kā {{max}}.', + minLength: '{{field}} jābūt vismaz {{min}} rakstzīmēm.', + minimum: '{{field}} jābūt vismaz {{min}}.', + required: '{{field}} ir obligāts.', + }, + }, + folders: { actions: { createFolder: 'Izveidot mapi', deleteFolder: 'Dzēst mapi', editFolder: 'Rediģēt mapi', }, - descriptions: { - editorDefault: 'Mape saistītām kavām.', - }, dialogs: { deleteFolderDescription: 'Tas pārvieto "{{name}}" uz Atkritni. Vēlāk to varēsiet atjaunot.', deleteFolderFallbackDescription: 'Tas pārvieto šo mapi uz Atkritni. Vēlāk to varēsiet atjaunot.', @@ -266,7 +274,6 @@ export const lv = { descriptionLabel: 'Mapes apraksts', descriptionPlaceholder: 'Kas pieder šai mapei?', namePlaceholder: 'Mapes nosaukums', - untitledFolder: 'Mape bez nosaukuma', }, labels: { createFolderTitle: 'Izveidot mapi', @@ -599,7 +606,6 @@ export const lv = { openingWorkspace: 'Atver {{title}}', }, descriptions: { - editorDefault: 'Mācību konteksts.', editorVisual: 'Izvēlieties vizuālo enkuru šai darbvietai.', emptyList: 'Atdaliet kavas, piezīmes un atkārtošanas rindas pēc mācību konteksta.', }, @@ -624,7 +630,6 @@ export const lv = { descriptionLabel: 'Darbvietas apraksts', descriptionPlaceholder: 'Kas pieder šai darbvietai?', namePlaceholder: 'Darbvietas nosaukums', - untitledWorkspace: 'Darbvieta bez nosaukuma', }, labels: { createWorkspaceTitle: 'Izveidot darbvietu', diff --git a/ui/src/core/i18n/resources/nb.ts b/ui/src/core/i18n/resources/nb.ts index a4d7d01..4eae091 100644 --- a/ui/src/core/i18n/resources/nb.ts +++ b/ui/src/core/i18n/resources/nb.ts @@ -158,7 +158,6 @@ export const nb = { }, descriptions: { emptyDeck: 'Legg til et notat, så denne kortstokken har materiale å repetere.', - editorDefault: 'Fokusert studiekortstokk.', editorVisual: 'Velg et omslagssymbol for denne kortstokken.', notesSearchPlaceholder: 'Søk i notater…', }, @@ -190,8 +189,6 @@ export const nb = { descriptionLabel: 'Kortstokkbeskrivelse', descriptionPlaceholder: 'Hva skal denne kortstokken hjelpe deg med å repetere?', namePlaceholder: 'Navn på kortstokk', - untitledDeck: 'Kortstokk uten tittel', - untitledDeckLower: 'kortstokk uten tittel', }, labels: { createDeckTitle: 'Opprett kortstokk', @@ -221,20 +218,31 @@ export const nb = { timeout: 'Dette tok for lang tid. Prøv igjen.', unauthorized: 'Logg inn for å fortsette.', unavailable: 'Tjenesten er midlertidig utilgjengelig.', + validation: 'Kontroller de markerte feltene og prøv igjen.', }, fallback: { unexpected: 'Uventet feil', }, }, + forms: { + validation: { + invalid: '{{field}} er ugyldig.', + invalidEnum: 'Velg en gyldig verdi for {{field}}.', + invalidFormat: 'Skriv inn en gyldig verdi for {{field}}.', + maxLength: '{{field}} kan være maks {{max}} tegn.', + maximum: '{{field}} kan være maks {{max}}.', + minLength: '{{field}} må være minst {{min}} tegn.', + minimum: '{{field}} må være minst {{min}}.', + required: '{{field}} er påkrevd.', + }, + }, + folders: { actions: { createFolder: 'Opprett mappe', deleteFolder: 'Slett mappe', editFolder: 'Rediger mappe', }, - descriptions: { - editorDefault: 'Mappe for relaterte kortstokker.', - }, dialogs: { deleteFolderDescription: 'Dette flytter "{{name}}" til Papirkurv. Du kan gjenopprette den senere.', deleteFolderFallbackDescription: 'Dette flytter denne mappen til Papirkurv. Du kan gjenopprette den senere.', @@ -256,7 +264,6 @@ export const nb = { descriptionLabel: 'Mappebeskrivelse', descriptionPlaceholder: 'Hva hører hjemme i denne mappen?', namePlaceholder: 'Mappenavn', - untitledFolder: 'Mappe uten tittel', }, labels: { createFolderTitle: 'Opprett mappe', @@ -586,7 +593,6 @@ export const nb = { openingWorkspace: 'Åpner {{title}}', }, descriptions: { - editorDefault: 'Studiekontekst.', editorVisual: 'Velg et visuelt anker for dette arbeidsområdet.', emptyList: 'Skill kortstokker, notater og repetisjonskøer etter studiekontekst.', }, @@ -611,7 +617,6 @@ export const nb = { descriptionLabel: 'Arbeidsområdebeskrivelse', descriptionPlaceholder: 'Hva hører hjemme i dette arbeidsområdet?', namePlaceholder: 'Navn på arbeidsområde', - untitledWorkspace: 'Arbeidsområde uten tittel', }, labels: { createWorkspaceTitle: 'Opprett arbeidsområde', diff --git a/ui/src/core/i18n/resources/nl.ts b/ui/src/core/i18n/resources/nl.ts index f8c9c82..392daeb 100644 --- a/ui/src/core/i18n/resources/nl.ts +++ b/ui/src/core/i18n/resources/nl.ts @@ -158,7 +158,6 @@ export const nl = { }, descriptions: { emptyDeck: 'Voeg een notitie toe zodat deze kaartenset materiaal heeft om te herhalen.', - editorDefault: 'Gerichte studiekaartenset.', editorVisual: 'Kies een omslagicoon voor deze kaartenset.', notesSearchPlaceholder: 'Notities zoeken…', }, @@ -190,8 +189,6 @@ export const nl = { descriptionLabel: 'Beschrijving van kaartenset', descriptionPlaceholder: 'Wat helpt deze kaartenset je te herhalen?', namePlaceholder: 'Naam van kaartenset', - untitledDeck: 'Naamloze kaartenset', - untitledDeckLower: 'naamloze kaartenset', }, labels: { createDeckTitle: 'Kaartenset maken', @@ -221,20 +218,31 @@ export const nl = { timeout: 'Dit duurt te lang. Probeer opnieuw.', unauthorized: 'Log in om door te gaan.', unavailable: 'De service is tijdelijk niet beschikbaar.', + validation: 'Controleer de gemarkeerde velden en probeer het opnieuw.', }, fallback: { unexpected: 'Onverwachte fout', }, }, + forms: { + validation: { + invalid: '{{field}} is ongeldig.', + invalidEnum: 'Kies een geldige waarde voor {{field}}.', + invalidFormat: 'Voer een geldige waarde in voor {{field}}.', + maxLength: '{{field}} mag maximaal {{max}} tekens bevatten.', + maximum: '{{field}} mag maximaal {{max}} zijn.', + minLength: '{{field}} moet minimaal {{min}} tekens bevatten.', + minimum: '{{field}} moet minimaal {{min}} zijn.', + required: '{{field}} is verplicht.', + }, + }, + folders: { actions: { createFolder: 'Map maken', deleteFolder: 'Map verwijderen', editFolder: 'Map bewerken', }, - descriptions: { - editorDefault: 'Map voor verwante kaartensets.', - }, dialogs: { deleteFolderDescription: 'Dit verplaatst "{{name}}" naar de Prullenbak. Je kunt hem later herstellen.', deleteFolderFallbackDescription: 'Dit verplaatst deze map naar de Prullenbak. Je kunt hem later herstellen.', @@ -256,7 +264,6 @@ export const nl = { descriptionLabel: 'Mapbeschrijving', descriptionPlaceholder: 'Wat hoort in deze map?', namePlaceholder: 'Mapnaam', - untitledFolder: 'Naamloze map', }, labels: { createFolderTitle: 'Map maken', @@ -586,7 +593,6 @@ export const nl = { openingWorkspace: '{{title}} openen', }, descriptions: { - editorDefault: 'Studiecontext.', editorVisual: 'Kies een visueel anker voor deze werkruimte.', emptyList: 'Scheid kaartensets, notities en herhaalwachtrijen per studiecontext.', }, @@ -611,7 +617,6 @@ export const nl = { descriptionLabel: 'Beschrijving van werkruimte', descriptionPlaceholder: 'Wat hoort in deze werkruimte?', namePlaceholder: 'Naam van werkruimte', - untitledWorkspace: 'Naamloze werkruimte', }, labels: { createWorkspaceTitle: 'Werkruimte maken', diff --git a/ui/src/core/i18n/resources/pl.ts b/ui/src/core/i18n/resources/pl.ts index 161052d..d0e3f43 100644 --- a/ui/src/core/i18n/resources/pl.ts +++ b/ui/src/core/i18n/resources/pl.ts @@ -178,7 +178,6 @@ export const pl = { }, descriptions: { emptyDeck: 'Dodaj notatkę, aby ten zestaw miał materiał do powtórek.', - editorDefault: 'Skupiony zestaw do nauki.', editorVisual: 'Wybierz glif okładki dla tego zestawu.', notesSearchPlaceholder: 'Szukaj notatek…', }, @@ -210,8 +209,6 @@ export const pl = { descriptionLabel: 'Opis zestawu', descriptionPlaceholder: 'Co ten zestaw pomoże Ci powtórzyć?', namePlaceholder: 'Nazwa zestawu', - untitledDeck: 'Zestaw bez tytułu', - untitledDeckLower: 'zestaw bez tytułu', }, labels: { createDeckTitle: 'Utwórz zestaw', @@ -241,20 +238,31 @@ export const pl = { timeout: 'To trwa zbyt długo. Spróbuj ponownie.', unauthorized: 'Zaloguj się, aby kontynuować.', unavailable: 'Usługa jest tymczasowo niedostępna.', + validation: 'Sprawdź wyróżnione pola i spróbuj ponownie.', }, fallback: { unexpected: 'Nieoczekiwany błąd', }, }, + forms: { + validation: { + invalid: '{{field}} jest nieprawidłowe.', + invalidEnum: 'Wybierz prawidłową wartość dla {{field}}.', + invalidFormat: 'Wpisz prawidłową wartość dla {{field}}.', + maxLength: '{{field}} może mieć najwyżej {{max}} znaków.', + maximum: '{{field}} musi wynosić najwyżej {{max}}.', + minLength: '{{field}} musi mieć co najmniej {{min}} znaków.', + minimum: '{{field}} musi wynosić co najmniej {{min}}.', + required: '{{field}} jest wymagane.', + }, + }, + folders: { actions: { createFolder: 'Utwórz folder', deleteFolder: 'Usuń folder', editFolder: 'Edytuj folder', }, - descriptions: { - editorDefault: 'Folder na powiązane zestawy.', - }, dialogs: { deleteFolderDescription: 'To przeniesie "{{name}}" do Kosza. Możesz przywrócić go później.', deleteFolderFallbackDescription: 'To przeniesie ten folder do Kosza. Możesz przywrócić go później.', @@ -276,7 +284,6 @@ export const pl = { descriptionLabel: 'Opis folderu', descriptionPlaceholder: 'Co należy do tego folderu?', namePlaceholder: 'Nazwa folderu', - untitledFolder: 'Folder bez tytułu', }, labels: { createFolderTitle: 'Utwórz folder', @@ -612,7 +619,6 @@ export const pl = { openingWorkspace: 'Otwieranie {{title}}', }, descriptions: { - editorDefault: 'Kontekst nauki.', editorVisual: 'Wybierz wizualny punkt odniesienia dla tego obszaru roboczego.', emptyList: 'Oddziel zestawy, notatki i kolejki powtórek według kontekstu nauki.', }, @@ -637,7 +643,6 @@ export const pl = { descriptionLabel: 'Opis obszaru roboczego', descriptionPlaceholder: 'Co należy do tego obszaru roboczego?', namePlaceholder: 'Nazwa obszaru roboczego', - untitledWorkspace: 'Obszar roboczy bez tytułu', }, labels: { createWorkspaceTitle: 'Utwórz obszar roboczy', diff --git a/ui/src/core/i18n/resources/pt-BR.ts b/ui/src/core/i18n/resources/pt-BR.ts index 6899753..feebcc5 100644 --- a/ui/src/core/i18n/resources/pt-BR.ts +++ b/ui/src/core/i18n/resources/pt-BR.ts @@ -158,7 +158,6 @@ export const ptBR = { }, descriptions: { emptyDeck: 'Adicione uma nota para que este baralho tenha material para revisão.', - editorDefault: 'Baralho de estudo focado.', editorVisual: 'Escolha um glifo de capa para este baralho.', notesSearchPlaceholder: 'Pesquisar notas…', }, @@ -190,8 +189,6 @@ export const ptBR = { descriptionLabel: 'Descrição do baralho', descriptionPlaceholder: 'O que este baralho vai ajudar você a revisar?', namePlaceholder: 'Nome do baralho', - untitledDeck: 'Baralho sem título', - untitledDeckLower: 'baralho sem título', }, labels: { createDeckTitle: 'Criar baralho', @@ -221,20 +218,31 @@ export const ptBR = { timeout: 'Isso demorou demais. Tente novamente.', unauthorized: 'Entre para continuar.', unavailable: 'O serviço está temporariamente indisponível.', + validation: 'Verifique os campos destacados e tente novamente.', }, fallback: { unexpected: 'Erro inesperado', }, }, + forms: { + validation: { + invalid: '{{field}} é inválido.', + invalidEnum: 'Escolha um {{field}} válido.', + invalidFormat: 'Digite um {{field}} válido.', + maxLength: '{{field}} deve ter no máximo {{max}} caracteres.', + maximum: '{{field}} deve ser no máximo {{max}}.', + minLength: '{{field}} deve ter pelo menos {{min}} caracteres.', + minimum: '{{field}} deve ser pelo menos {{min}}.', + required: '{{field}} é obrigatório.', + }, + }, + folders: { actions: { createFolder: 'Criar pasta', deleteFolder: 'Excluir pasta', editFolder: 'Editar pasta', }, - descriptions: { - editorDefault: 'Pasta para baralhos relacionados.', - }, dialogs: { deleteFolderDescription: 'Isso move "{{name}}" para a Lixeira. Você pode restaurá-la depois.', deleteFolderFallbackDescription: 'Isso move esta pasta para a Lixeira. Você pode restaurá-la depois.', @@ -256,7 +264,6 @@ export const ptBR = { descriptionLabel: 'Descrição da pasta', descriptionPlaceholder: 'O que pertence a esta pasta?', namePlaceholder: 'Nome da pasta', - untitledFolder: 'Pasta sem título', }, labels: { createFolderTitle: 'Criar pasta', @@ -586,7 +593,6 @@ export const ptBR = { openingWorkspace: 'Abrindo {{title}}', }, descriptions: { - editorDefault: 'Contexto de estudo.', editorVisual: 'Escolha uma âncora visual para este espaço de trabalho.', emptyList: 'Separe baralhos, notas e filas de revisão por contexto de estudo.', }, @@ -611,7 +617,6 @@ export const ptBR = { descriptionLabel: 'Descrição do espaço de trabalho', descriptionPlaceholder: 'O que pertence a este espaço de trabalho?', namePlaceholder: 'Nome do espaço de trabalho', - untitledWorkspace: 'Espaço de trabalho sem título', }, labels: { createWorkspaceTitle: 'Criar espaço de trabalho', diff --git a/ui/src/core/i18n/resources/ro.ts b/ui/src/core/i18n/resources/ro.ts index a485893..248cb29 100644 --- a/ui/src/core/i18n/resources/ro.ts +++ b/ui/src/core/i18n/resources/ro.ts @@ -168,7 +168,6 @@ export const ro = { }, descriptions: { emptyDeck: 'Adaugă o notiță pentru ca acest pachet să aibă material de recapitulat.', - editorDefault: 'Pachet de studiu concentrat.', editorVisual: 'Alege un simbol de copertă pentru acest pachet.', notesSearchPlaceholder: 'Caută notițe…', }, @@ -200,8 +199,6 @@ export const ro = { descriptionLabel: 'Descrierea pachetului', descriptionPlaceholder: 'Ce te va ajuta să recapitulezi acest pachet?', namePlaceholder: 'Numele pachetului', - untitledDeck: 'Pachet fără titlu', - untitledDeckLower: 'pachet fără titlu', }, labels: { createDeckTitle: 'Creează pachet', @@ -231,20 +228,31 @@ export const ro = { timeout: 'A durat prea mult. Încearcă din nou.', unauthorized: 'Conectează-te pentru a continua.', unavailable: 'Serviciul este temporar indisponibil.', + validation: 'Verifică câmpurile evidențiate și încearcă din nou.', }, fallback: { unexpected: 'Eroare neașteptată', }, }, + forms: { + validation: { + invalid: '{{field}} nu este valid.', + invalidEnum: 'Alege o valoare validă pentru {{field}}.', + invalidFormat: 'Introdu o valoare validă pentru {{field}}.', + maxLength: '{{field}} trebuie să aibă cel mult {{max}} caractere.', + maximum: '{{field}} trebuie să fie cel mult {{max}}.', + minLength: '{{field}} trebuie să aibă cel puțin {{min}} caractere.', + minimum: '{{field}} trebuie să fie cel puțin {{min}}.', + required: '{{field}} este obligatoriu.', + }, + }, + folders: { actions: { createFolder: 'Creează folder', deleteFolder: 'Șterge folderul', editFolder: 'Editează folderul', }, - descriptions: { - editorDefault: 'Folder pentru pachete asociate.', - }, dialogs: { deleteFolderDescription: 'Aceasta mută "{{name}}" în Coș. Îl poți restaura mai târziu.', deleteFolderFallbackDescription: 'Aceasta mută acest folder în Coș. Îl poți restaura mai târziu.', @@ -266,7 +274,6 @@ export const ro = { descriptionLabel: 'Descrierea folderului', descriptionPlaceholder: 'Ce aparține acestui folder?', namePlaceholder: 'Numele folderului', - untitledFolder: 'Folder fără titlu', }, labels: { createFolderTitle: 'Creează folder', @@ -599,7 +606,6 @@ export const ro = { openingWorkspace: 'Se deschide {{title}}', }, descriptions: { - editorDefault: 'Context de studiu.', editorVisual: 'Alege o ancoră vizuală pentru acest spațiu de lucru.', emptyList: 'Separă pachetele, notițele și cozile de recapitulare după contextul de studiu.', }, @@ -624,7 +630,6 @@ export const ro = { descriptionLabel: 'Descrierea spațiului de lucru', descriptionPlaceholder: 'Ce aparține acestui spațiu de lucru?', namePlaceholder: 'Numele spațiului de lucru', - untitledWorkspace: 'Spațiu de lucru fără titlu', }, labels: { createWorkspaceTitle: 'Creează spațiu de lucru', diff --git a/ui/src/core/i18n/resources/ru.ts b/ui/src/core/i18n/resources/ru.ts index 01942ed..1770d35 100644 --- a/ui/src/core/i18n/resources/ru.ts +++ b/ui/src/core/i18n/resources/ru.ts @@ -178,7 +178,6 @@ export const ru = { }, descriptions: { emptyDeck: 'Добавьте заметку, чтобы в этой колоде был материал для повторения.', - editorDefault: 'Колода для сфокусированного обучения.', editorVisual: 'Выберите символ обложки для этой колоды.', notesSearchPlaceholder: 'Искать заметки…', }, @@ -210,8 +209,6 @@ export const ru = { descriptionLabel: 'Описание колоды', descriptionPlaceholder: 'Что эта колода поможет вам повторять?', namePlaceholder: 'Название колоды', - untitledDeck: 'Колода без названия', - untitledDeckLower: 'колода без названия', }, labels: { createDeckTitle: 'Создать колоду', @@ -241,20 +238,31 @@ export const ru = { timeout: 'Это заняло слишком много времени. Попробуйте снова.', unauthorized: 'Войдите, чтобы продолжить.', unavailable: 'Сервис временно недоступен.', + validation: 'Проверьте выделенные поля и попробуйте снова.', }, fallback: { unexpected: 'Неожиданная ошибка', }, }, + forms: { + validation: { + invalid: '{{field}} заполнено неверно.', + invalidEnum: 'Выберите допустимое значение для {{field}}.', + invalidFormat: 'Введите допустимое значение для {{field}}.', + maxLength: '{{field}} должно быть не длиннее {{max}} символов.', + maximum: '{{field}} должно быть не больше {{max}}.', + minLength: '{{field}} должно быть не короче {{min}} символов.', + minimum: '{{field}} должно быть не меньше {{min}}.', + required: '{{field}} обязательно.', + }, + }, + folders: { actions: { createFolder: 'Создать папку', deleteFolder: 'Удалить папку', editFolder: 'Изменить папку', }, - descriptions: { - editorDefault: 'Папка для связанных колод.', - }, dialogs: { deleteFolderDescription: 'Это переместит "{{name}}" в Корзину. Вы сможете восстановить ее позже.', deleteFolderFallbackDescription: 'Это переместит эту папку в Корзину. Вы сможете восстановить ее позже.', @@ -276,7 +284,6 @@ export const ru = { descriptionLabel: 'Описание папки', descriptionPlaceholder: 'Что относится к этой папке?', namePlaceholder: 'Название папки', - untitledFolder: 'Папка без названия', }, labels: { createFolderTitle: 'Создать папку', @@ -612,7 +619,6 @@ export const ru = { openingWorkspace: 'Открытие {{title}}', }, descriptions: { - editorDefault: 'Учебный контекст.', editorVisual: 'Выберите визуальный якорь для этого пространства.', emptyList: 'Разделяйте колоды, заметки и очереди повторения по учебному контексту.', }, @@ -637,7 +643,6 @@ export const ru = { descriptionLabel: 'Описание рабочего пространства', descriptionPlaceholder: 'Что относится к этому рабочему пространству?', namePlaceholder: 'Название рабочего пространства', - untitledWorkspace: 'Рабочее пространство без названия', }, labels: { createWorkspaceTitle: 'Создать рабочее пространство', diff --git a/ui/src/core/i18n/resources/sk.ts b/ui/src/core/i18n/resources/sk.ts index cb9e183..dfbbda0 100644 --- a/ui/src/core/i18n/resources/sk.ts +++ b/ui/src/core/i18n/resources/sk.ts @@ -178,7 +178,6 @@ export const sk = { }, descriptions: { emptyDeck: 'Pridajte poznámku, aby mal tento balíček materiál na opakovanie.', - editorDefault: 'Sústredený študijný balíček.', editorVisual: 'Vyberte symbol obalu pre tento balíček.', notesSearchPlaceholder: 'Hľadať poznámky…', }, @@ -210,8 +209,6 @@ export const sk = { descriptionLabel: 'Popis balíčka', descriptionPlaceholder: 'Čo vám tento balíček pomôže opakovať?', namePlaceholder: 'Názov balíčka', - untitledDeck: 'Balíček bez názvu', - untitledDeckLower: 'balíček bez názvu', }, labels: { createDeckTitle: 'Vytvoriť balíček', @@ -241,20 +238,31 @@ export const sk = { timeout: 'Trvalo to príliš dlho. Skúste to znova.', unauthorized: 'Ak chcete pokračovať, prihláste sa.', unavailable: 'Služba je dočasne nedostupná.', + validation: 'Skontrolujte zvýraznené polia a skúste to znova.', }, fallback: { unexpected: 'Neočakávaná chyba', }, }, + forms: { + validation: { + invalid: '{{field}} nie je platné.', + invalidEnum: 'Vyberte platnú hodnotu pre {{field}}.', + invalidFormat: 'Zadajte platnú hodnotu pre {{field}}.', + maxLength: '{{field}} môže mať najviac {{max}} znakov.', + maximum: '{{field}} musí byť najviac {{max}}.', + minLength: '{{field}} musí mať aspoň {{min}} znakov.', + minimum: '{{field}} musí byť aspoň {{min}}.', + required: '{{field}} je povinné.', + }, + }, + folders: { actions: { createFolder: 'Vytvoriť priečinok', deleteFolder: 'Odstrániť priečinok', editFolder: 'Upraviť priečinok', }, - descriptions: { - editorDefault: 'Priečinok pre súvisiace balíčky.', - }, dialogs: { deleteFolderDescription: 'Toto presunie "{{name}}" do Koša. Neskôr ho môžete obnoviť.', deleteFolderFallbackDescription: 'Toto presunie tento priečinok do Koša. Neskôr ho môžete obnoviť.', @@ -276,7 +284,6 @@ export const sk = { descriptionLabel: 'Popis priečinka', descriptionPlaceholder: 'Čo patrí do tohto priečinka?', namePlaceholder: 'Názov priečinka', - untitledFolder: 'Priečinok bez názvu', }, labels: { createFolderTitle: 'Vytvoriť priečinok', @@ -612,7 +619,6 @@ export const sk = { openingWorkspace: 'Otvára sa {{title}}', }, descriptions: { - editorDefault: 'Študijný kontext.', editorVisual: 'Vyberte vizuálnu kotvu pre tento pracovný priestor.', emptyList: 'Oddeľte balíčky, poznámky a fronty opakovania podľa študijného kontextu.', }, @@ -637,7 +643,6 @@ export const sk = { descriptionLabel: 'Popis pracovného priestoru', descriptionPlaceholder: 'Čo patrí do tohto pracovného priestoru?', namePlaceholder: 'Názov pracovného priestoru', - untitledWorkspace: 'Pracovný priestor bez názvu', }, labels: { createWorkspaceTitle: 'Vytvoriť pracovný priestor', diff --git a/ui/src/core/i18n/resources/sl.ts b/ui/src/core/i18n/resources/sl.ts index 9ed9767..b7dbc29 100644 --- a/ui/src/core/i18n/resources/sl.ts +++ b/ui/src/core/i18n/resources/sl.ts @@ -178,7 +178,6 @@ export const sl = { }, descriptions: { emptyDeck: 'Dodajte zapisek, da bo imel ta komplet gradivo za ponavljanje.', - editorDefault: 'Osredotočen učni komplet.', editorVisual: 'Izberite simbol naslovnice za ta komplet.', notesSearchPlaceholder: 'Išči zapiske…', }, @@ -210,8 +209,6 @@ export const sl = { descriptionLabel: 'Opis kompleta', descriptionPlaceholder: 'Kaj vam bo ta komplet pomagal ponavljati?', namePlaceholder: 'Ime kompleta', - untitledDeck: 'Komplet brez naslova', - untitledDeckLower: 'komplet brez naslova', }, labels: { createDeckTitle: 'Ustvari komplet', @@ -241,20 +238,31 @@ export const sl = { timeout: 'To je trajalo predolgo. Poskusite znova.', unauthorized: 'Za nadaljevanje se prijavite.', unavailable: 'Storitev začasno ni na voljo.', + validation: 'Preverite označena polja in poskusite znova.', }, fallback: { unexpected: 'Nepričakovana napaka', }, }, + forms: { + validation: { + invalid: '{{field}} ni veljavno.', + invalidEnum: 'Izberite veljavno vrednost za {{field}}.', + invalidFormat: 'Vnesite veljavno vrednost za {{field}}.', + maxLength: '{{field}} ima lahko največ {{max}} znakov.', + maximum: '{{field}} mora biti največ {{max}}.', + minLength: '{{field}} mora imeti vsaj {{min}} znakov.', + minimum: '{{field}} mora biti vsaj {{min}}.', + required: '{{field}} je obvezno.', + }, + }, + folders: { actions: { createFolder: 'Ustvari mapo', deleteFolder: 'Izbriši mapo', editFolder: 'Uredi mapo', }, - descriptions: { - editorDefault: 'Mapa za povezane komplete.', - }, dialogs: { deleteFolderDescription: 'To premakne "{{name}}" v Koš. Pozneje jo lahko obnovite.', deleteFolderFallbackDescription: 'To premakne to mapo v Koš. Pozneje jo lahko obnovite.', @@ -276,7 +284,6 @@ export const sl = { descriptionLabel: 'Opis mape', descriptionPlaceholder: 'Kaj spada v to mapo?', namePlaceholder: 'Ime mape', - untitledFolder: 'Mapa brez naslova', }, labels: { createFolderTitle: 'Ustvari mapo', @@ -612,7 +619,6 @@ export const sl = { openingWorkspace: 'Odpiranje {{title}}', }, descriptions: { - editorDefault: 'Učni kontekst.', editorVisual: 'Izberite vizualno sidro za ta delovni prostor.', emptyList: 'Ločite komplete, zapiske in čakalne vrste za ponavljanje po učnem kontekstu.', }, @@ -637,7 +643,6 @@ export const sl = { descriptionLabel: 'Opis delovnega prostora', descriptionPlaceholder: 'Kaj spada v ta delovni prostor?', namePlaceholder: 'Ime delovnega prostora', - untitledWorkspace: 'Delovni prostor brez naslova', }, labels: { createWorkspaceTitle: 'Ustvari delovni prostor', diff --git a/ui/src/core/i18n/resources/sr-Latn.ts b/ui/src/core/i18n/resources/sr-Latn.ts index cff69bf..265443b 100644 --- a/ui/src/core/i18n/resources/sr-Latn.ts +++ b/ui/src/core/i18n/resources/sr-Latn.ts @@ -168,7 +168,6 @@ export const srLatn = { }, descriptions: { emptyDeck: 'Dodajte belešku da bi ovaj špil imao materijal za ponavljanje.', - editorDefault: 'Fokusiran špil za učenje.', editorVisual: 'Izaberite simbol naslovnice za ovaj špil.', notesSearchPlaceholder: 'Pretraži beleške…', }, @@ -200,8 +199,6 @@ export const srLatn = { descriptionLabel: 'Opis špila', descriptionPlaceholder: 'Šta će vam ovaj špil pomoći da ponavljate?', namePlaceholder: 'Naziv špila', - untitledDeck: 'Špil bez naslova', - untitledDeckLower: 'špil bez naslova', }, labels: { createDeckTitle: 'Napravi špil', @@ -231,20 +228,31 @@ export const srLatn = { timeout: 'Ovo je trajalo predugo. Pokušajte ponovo.', unauthorized: 'Prijavite se da nastavite.', unavailable: 'Usluga je privremeno nedostupna.', + validation: 'Proverite označena polja i pokušajte ponovo.', }, fallback: { unexpected: 'Neočekivana greška', }, }, + forms: { + validation: { + invalid: '{{field}} nije važeće.', + invalidEnum: 'Izaberite važeću vrednost za {{field}}.', + invalidFormat: 'Unesite važeću vrednost za {{field}}.', + maxLength: '{{field}} može imati najviše {{max}} znakova.', + maximum: '{{field}} mora biti najviše {{max}}.', + minLength: '{{field}} mora imati najmanje {{min}} znakova.', + minimum: '{{field}} mora biti najmanje {{min}}.', + required: '{{field}} je obavezno.', + }, + }, + folders: { actions: { createFolder: 'Napravi fasciklu', deleteFolder: 'Obriši fasciklu', editFolder: 'Uredi fasciklu', }, - descriptions: { - editorDefault: 'Fascikla za povezane špilove.', - }, dialogs: { deleteFolderDescription: 'Ovo premešta "{{name}}" u Smeće. Možete je vratiti kasnije.', deleteFolderFallbackDescription: 'Ovo premešta ovu fasciklu u Smeće. Možete je vratiti kasnije.', @@ -266,7 +274,6 @@ export const srLatn = { descriptionLabel: 'Opis fascikle', descriptionPlaceholder: 'Šta pripada ovoj fascikli?', namePlaceholder: 'Naziv fascikle', - untitledFolder: 'Fascikla bez naslova', }, labels: { createFolderTitle: 'Napravi fasciklu', @@ -599,7 +606,6 @@ export const srLatn = { openingWorkspace: 'Otvaranje {{title}}', }, descriptions: { - editorDefault: 'Kontekst učenja.', editorVisual: 'Izaberite vizuelno sidro za ovaj radni prostor.', emptyList: 'Odvojite špilove, beleške i redove za ponavljanje po kontekstu učenja.', }, @@ -624,7 +630,6 @@ export const srLatn = { descriptionLabel: 'Opis radnog prostora', descriptionPlaceholder: 'Šta pripada ovom radnom prostoru?', namePlaceholder: 'Naziv radnog prostora', - untitledWorkspace: 'Radni prostor bez naslova', }, labels: { createWorkspaceTitle: 'Napravi radni prostor', diff --git a/ui/src/core/i18n/resources/sv.ts b/ui/src/core/i18n/resources/sv.ts index 0204bd0..3074ce4 100644 --- a/ui/src/core/i18n/resources/sv.ts +++ b/ui/src/core/i18n/resources/sv.ts @@ -158,7 +158,6 @@ export const sv = { }, descriptions: { emptyDeck: 'Lägg till en anteckning så att kortleken har material att repetera.', - editorDefault: 'Fokuserad studiekortlek.', editorVisual: 'Välj en omslagsikon för den här kortleken.', notesSearchPlaceholder: 'Sök anteckningar…', }, @@ -190,8 +189,6 @@ export const sv = { descriptionLabel: 'Kortleksbeskrivning', descriptionPlaceholder: 'Vad hjälper den här kortleken dig att repetera?', namePlaceholder: 'Kortleksnamn', - untitledDeck: 'Namnlös kortlek', - untitledDeckLower: 'namnlös kortlek', }, labels: { createDeckTitle: 'Skapa kortlek', @@ -221,20 +218,31 @@ export const sv = { timeout: 'Detta tog för lång tid. Försök igen.', unauthorized: 'Logga in för att fortsätta.', unavailable: 'Tjänsten är tillfälligt otillgänglig.', + validation: 'Kontrollera de markerade fälten och försök igen.', }, fallback: { unexpected: 'Oväntat fel', }, }, + forms: { + validation: { + invalid: '{{field}} är ogiltigt.', + invalidEnum: 'Välj ett giltigt värde för {{field}}.', + invalidFormat: 'Ange ett giltigt värde för {{field}}.', + maxLength: '{{field}} får vara högst {{max}} tecken.', + maximum: '{{field}} får vara högst {{max}}.', + minLength: '{{field}} måste vara minst {{min}} tecken.', + minimum: '{{field}} måste vara minst {{min}}.', + required: '{{field}} krävs.', + }, + }, + folders: { actions: { createFolder: 'Skapa mapp', deleteFolder: 'Ta bort mapp', editFolder: 'Redigera mapp', }, - descriptions: { - editorDefault: 'Mapp för relaterade kortlekar.', - }, dialogs: { deleteFolderDescription: 'Detta flyttar "{{name}}" till Papperskorgen. Du kan återställa den senare.', deleteFolderFallbackDescription: 'Detta flyttar den här mappen till Papperskorgen. Du kan återställa den senare.', @@ -256,7 +264,6 @@ export const sv = { descriptionLabel: 'Mappbeskrivning', descriptionPlaceholder: 'Vad hör hemma i den här mappen?', namePlaceholder: 'Mappnamn', - untitledFolder: 'Namnlös mapp', }, labels: { createFolderTitle: 'Skapa mapp', @@ -586,7 +593,6 @@ export const sv = { openingWorkspace: 'Öppnar {{title}}', }, descriptions: { - editorDefault: 'Studiekontext.', editorVisual: 'Välj ett visuellt ankare för den här arbetsytan.', emptyList: 'Separera kortlekar, anteckningar och repetitionsköer efter studiekontext.', }, @@ -611,7 +617,6 @@ export const sv = { descriptionLabel: 'Beskrivning av arbetsyta', descriptionPlaceholder: 'Vad hör hemma i den här arbetsytan?', namePlaceholder: 'Namn på arbetsyta', - untitledWorkspace: 'Namnlös arbetsyta', }, labels: { createWorkspaceTitle: 'Skapa arbetsyta', diff --git a/ui/src/core/i18n/resources/th.ts b/ui/src/core/i18n/resources/th.ts index 477695a..8ef5806 100644 --- a/ui/src/core/i18n/resources/th.ts +++ b/ui/src/core/i18n/resources/th.ts @@ -158,7 +158,6 @@ export const th = { }, descriptions: { emptyDeck: 'เพิ่มโน้ตเพื่อให้สำรับนี้มีเนื้อหาสำหรับทบทวน', - editorDefault: 'สำรับเรียนแบบโฟกัส', editorVisual: 'เลือกสัญลักษณ์หน้าปกสำหรับสำรับนี้', notesSearchPlaceholder: 'ค้นหาโน้ต…', }, @@ -190,8 +189,6 @@ export const th = { descriptionLabel: 'คำอธิบายสำรับ', descriptionPlaceholder: 'สำรับนี้จะช่วยคุณทบทวนอะไร', namePlaceholder: 'ชื่อสำรับ', - untitledDeck: 'สำรับไม่มีชื่อ', - untitledDeckLower: 'สำรับไม่มีชื่อ', }, labels: { createDeckTitle: 'สร้างสำรับ', @@ -221,20 +218,31 @@ export const th = { timeout: 'ใช้เวลานานเกินไป ลองอีกครั้ง', unauthorized: 'ลงชื่อเข้าใช้เพื่อดำเนินการต่อ', unavailable: 'บริการไม่พร้อมใช้งานชั่วคราว', + validation: 'ตรวจสอบช่องที่ไฮไลต์แล้วลองอีกครั้ง', }, fallback: { unexpected: 'ข้อผิดพลาดที่ไม่คาดคิด', }, }, + forms: { + validation: { + invalid: '{{field}} ไม่ถูกต้อง', + invalidEnum: 'เลือก {{field}} ที่ถูกต้อง', + invalidFormat: 'ป้อน {{field}} ที่ถูกต้อง', + maxLength: '{{field}} ต้องไม่เกิน {{max}} อักขระ', + maximum: '{{field}} ต้องไม่เกิน {{max}}', + minLength: '{{field}} ต้องมีอย่างน้อย {{min}} อักขระ', + minimum: '{{field}} ต้องมีค่าอย่างน้อย {{min}}', + required: 'ต้องระบุ {{field}}', + }, + }, + folders: { actions: { createFolder: 'สร้างโฟลเดอร์', deleteFolder: 'ลบโฟลเดอร์', editFolder: 'แก้ไขโฟลเดอร์', }, - descriptions: { - editorDefault: 'โฟลเดอร์สำหรับสำรับที่เกี่ยวข้องกัน', - }, dialogs: { deleteFolderDescription: 'การทำงานนี้จะย้าย "{{name}}" ไปที่ถังขยะ คุณสามารถกู้คืนได้ภายหลัง', deleteFolderFallbackDescription: 'การทำงานนี้จะย้ายโฟลเดอร์นี้ไปที่ถังขยะ คุณสามารถกู้คืนได้ภายหลัง', @@ -256,7 +264,6 @@ export const th = { descriptionLabel: 'คำอธิบายโฟลเดอร์', descriptionPlaceholder: 'อะไรอยู่ในโฟลเดอร์นี้', namePlaceholder: 'ชื่อโฟลเดอร์', - untitledFolder: 'โฟลเดอร์ไม่มีชื่อ', }, labels: { createFolderTitle: 'สร้างโฟลเดอร์', @@ -586,7 +593,6 @@ export const th = { openingWorkspace: 'กำลังเปิด {{title}}', }, descriptions: { - editorDefault: 'บริบทการเรียน', editorVisual: 'เลือกจุดยึดภาพสำหรับพื้นที่ทำงานนี้', emptyList: 'แยกสำรับ โน้ต และคิวทบทวนตามบริบทการเรียน', }, @@ -611,7 +617,6 @@ export const th = { descriptionLabel: 'คำอธิบายพื้นที่ทำงาน', descriptionPlaceholder: 'อะไรอยู่ในพื้นที่ทำงานนี้', namePlaceholder: 'ชื่อพื้นที่ทำงาน', - untitledWorkspace: 'พื้นที่ทำงานไม่มีชื่อ', }, labels: { createWorkspaceTitle: 'สร้างพื้นที่ทำงาน', diff --git a/ui/src/core/i18n/resources/tr.ts b/ui/src/core/i18n/resources/tr.ts index 2d8c7d7..9c52af4 100644 --- a/ui/src/core/i18n/resources/tr.ts +++ b/ui/src/core/i18n/resources/tr.ts @@ -158,7 +158,6 @@ export const tr = { }, descriptions: { emptyDeck: 'Bu destede tekrar edilecek materyal olması için bir not ekleyin.', - editorDefault: 'Odaklı çalışma destesi.', editorVisual: 'Bu deste için bir kapak glifi seçin.', notesSearchPlaceholder: 'Not ara…', }, @@ -190,8 +189,6 @@ export const tr = { descriptionLabel: 'Deste açıklaması', descriptionPlaceholder: 'Bu deste neyi tekrar etmenize yardımcı olacak?', namePlaceholder: 'Deste adı', - untitledDeck: 'Adsız deste', - untitledDeckLower: 'adsız deste', }, labels: { createDeckTitle: 'Deste oluştur', @@ -221,20 +218,31 @@ export const tr = { timeout: 'Bu işlem çok uzun sürdü. Tekrar deneyin.', unauthorized: 'Devam etmek için oturum açın.', unavailable: 'Hizmet geçici olarak kullanılamıyor.', + validation: 'Vurgulanan alanları kontrol edin ve tekrar deneyin.', }, fallback: { unexpected: 'Beklenmeyen hata', }, }, + forms: { + validation: { + invalid: '{{field}} geçersiz.', + invalidEnum: 'Geçerli bir {{field}} seçin.', + invalidFormat: 'Geçerli bir {{field}} girin.', + maxLength: '{{field}} en fazla {{max}} karakter olmalıdır.', + maximum: '{{field}} en fazla {{max}} olmalıdır.', + minLength: '{{field}} en az {{min}} karakter olmalıdır.', + minimum: '{{field}} en az {{min}} olmalıdır.', + required: '{{field}} zorunludur.', + }, + }, + folders: { actions: { createFolder: 'Klasör oluştur', deleteFolder: 'Klasörü sil', editFolder: 'Klasörü düzenle', }, - descriptions: { - editorDefault: 'İlgili desteler için klasör.', - }, dialogs: { deleteFolderDescription: 'Bu, "{{name}}" öğesini Çöp Kutusu\'na taşır. Daha sonra geri yükleyebilirsiniz.', deleteFolderFallbackDescription: 'Bu, bu klasörü Çöp Kutusu\'na taşır. Daha sonra geri yükleyebilirsiniz.', @@ -256,7 +264,6 @@ export const tr = { descriptionLabel: 'Klasör açıklaması', descriptionPlaceholder: 'Bu klasöre neler ait?', namePlaceholder: 'Klasör adı', - untitledFolder: 'Adsız klasör', }, labels: { createFolderTitle: 'Klasör oluştur', @@ -586,7 +593,6 @@ export const tr = { openingWorkspace: '{{title}} açılıyor', }, descriptions: { - editorDefault: 'Çalışma bağlamı.', editorVisual: 'Bu çalışma alanı için görsel bir işaret seçin.', emptyList: 'Desteleri, notları ve tekrar sıralarını çalışma bağlamına göre ayırın.', }, @@ -611,7 +617,6 @@ export const tr = { descriptionLabel: 'Çalışma alanı açıklaması', descriptionPlaceholder: 'Bu çalışma alanına neler ait?', namePlaceholder: 'Çalışma alanı adı', - untitledWorkspace: 'Adsız çalışma alanı', }, labels: { createWorkspaceTitle: 'Çalışma alanı oluştur', diff --git a/ui/src/core/i18n/resources/uk.ts b/ui/src/core/i18n/resources/uk.ts index f6d25ac..fcc995e 100644 --- a/ui/src/core/i18n/resources/uk.ts +++ b/ui/src/core/i18n/resources/uk.ts @@ -178,7 +178,6 @@ export const uk = { }, descriptions: { emptyDeck: 'Додайте нотатку, щоб у цій колоді був матеріал для повторення.', - editorDefault: 'Сфокусована навчальна колода.', editorVisual: 'Виберіть символ обкладинки для цієї колоди.', notesSearchPlaceholder: 'Шукати нотатки…', }, @@ -210,8 +209,6 @@ export const uk = { descriptionLabel: 'Опис колоди', descriptionPlaceholder: 'Що ця колода допоможе вам повторювати?', namePlaceholder: 'Назва колоди', - untitledDeck: 'Колода без назви', - untitledDeckLower: 'колода без назви', }, labels: { createDeckTitle: 'Створити колоду', @@ -241,20 +238,31 @@ export const uk = { timeout: 'Це зайняло забагато часу. Спробуйте ще раз.', unauthorized: 'Увійдіть, щоб продовжити.', unavailable: 'Сервіс тимчасово недоступний.', + validation: 'Перевірте виділені поля й спробуйте знову.', }, fallback: { unexpected: 'Неочікувана помилка', }, }, + forms: { + validation: { + invalid: '{{field}} заповнено неправильно.', + invalidEnum: 'Виберіть припустиме значення для {{field}}.', + invalidFormat: 'Введіть припустиме значення для {{field}}.', + maxLength: '{{field}} має містити щонайбільше {{max}} символів.', + maximum: '{{field}} має бути не більше {{max}}.', + minLength: '{{field}} має містити щонайменше {{min}} символів.', + minimum: '{{field}} має бути не менше {{min}}.', + required: '{{field}} обов’язкове.', + }, + }, + folders: { actions: { createFolder: 'Створити папку', deleteFolder: 'Видалити папку', editFolder: 'Редагувати папку', }, - descriptions: { - editorDefault: 'Папка для пов’язаних колод.', - }, dialogs: { deleteFolderDescription: 'Це перемістить "{{name}}" до Кошика. Ви зможете відновити її пізніше.', deleteFolderFallbackDescription: 'Це перемістить цю папку до Кошика. Ви зможете відновити її пізніше.', @@ -276,7 +284,6 @@ export const uk = { descriptionLabel: 'Опис папки', descriptionPlaceholder: 'Що належить до цієї папки?', namePlaceholder: 'Назва папки', - untitledFolder: 'Папка без назви', }, labels: { createFolderTitle: 'Створити папку', @@ -612,7 +619,6 @@ export const uk = { openingWorkspace: 'Відкриття {{title}}', }, descriptions: { - editorDefault: 'Навчальний контекст.', editorVisual: 'Виберіть візуальний орієнтир для цього робочого простору.', emptyList: 'Розділяйте колоди, нотатки й черги повторення за навчальним контекстом.', }, @@ -637,7 +643,6 @@ export const uk = { descriptionLabel: 'Опис робочого простору', descriptionPlaceholder: 'Що належить до цього робочого простору?', namePlaceholder: 'Назва робочого простору', - untitledWorkspace: 'Робочий простір без назви', }, labels: { createWorkspaceTitle: 'Створити робочий простір', diff --git a/ui/src/core/i18n/resources/vi.ts b/ui/src/core/i18n/resources/vi.ts index a0a7069..3df162f 100644 --- a/ui/src/core/i18n/resources/vi.ts +++ b/ui/src/core/i18n/resources/vi.ts @@ -158,7 +158,6 @@ export const vi = { }, descriptions: { emptyDeck: 'Thêm ghi chú để bộ thẻ này có nội dung ôn tập.', - editorDefault: 'Bộ thẻ học tập tập trung.', editorVisual: 'Chọn một glyph bìa cho bộ thẻ này.', notesSearchPlaceholder: 'Tìm ghi chú…', }, @@ -190,8 +189,6 @@ export const vi = { descriptionLabel: 'Mô tả bộ thẻ', descriptionPlaceholder: 'Bộ thẻ này giúp bạn ôn tập điều gì?', namePlaceholder: 'Tên bộ thẻ', - untitledDeck: 'Bộ thẻ chưa đặt tên', - untitledDeckLower: 'bộ thẻ chưa đặt tên', }, labels: { createDeckTitle: 'Tạo bộ thẻ', @@ -221,20 +218,31 @@ export const vi = { timeout: 'Thao tác này mất quá lâu. Hãy thử lại.', unauthorized: 'Đăng nhập để tiếp tục.', unavailable: 'Dịch vụ tạm thời không khả dụng.', + validation: 'Kiểm tra các trường được đánh dấu rồi thử lại.', }, fallback: { unexpected: 'Lỗi không mong muốn', }, }, + forms: { + validation: { + invalid: '{{field}} không hợp lệ.', + invalidEnum: 'Chọn {{field}} hợp lệ.', + invalidFormat: 'Nhập {{field}} hợp lệ.', + maxLength: '{{field}} tối đa {{max}} ký tự.', + maximum: '{{field}} tối đa là {{max}}.', + minLength: '{{field}} tối thiểu {{min}} ký tự.', + minimum: '{{field}} tối thiểu là {{min}}.', + required: '{{field}} là bắt buộc.', + }, + }, + folders: { actions: { createFolder: 'Tạo thư mục', deleteFolder: 'Xóa thư mục', editFolder: 'Chỉnh sửa thư mục', }, - descriptions: { - editorDefault: 'Thư mục cho các bộ thẻ liên quan.', - }, dialogs: { deleteFolderDescription: 'Thao tác này chuyển "{{name}}" vào Thùng rác. Bạn có thể khôi phục sau.', deleteFolderFallbackDescription: 'Thao tác này chuyển thư mục này vào Thùng rác. Bạn có thể khôi phục sau.', @@ -256,7 +264,6 @@ export const vi = { descriptionLabel: 'Mô tả thư mục', descriptionPlaceholder: 'Những gì thuộc về thư mục này?', namePlaceholder: 'Tên thư mục', - untitledFolder: 'Thư mục chưa đặt tên', }, labels: { createFolderTitle: 'Tạo thư mục', @@ -586,7 +593,6 @@ export const vi = { openingWorkspace: 'Đang mở {{title}}', }, descriptions: { - editorDefault: 'Ngữ cảnh học tập.', editorVisual: 'Chọn điểm neo hình ảnh cho không gian làm việc này.', emptyList: 'Tách bộ thẻ, ghi chú và hàng đợi ôn tập theo ngữ cảnh học tập.', }, @@ -611,7 +617,6 @@ export const vi = { descriptionLabel: 'Mô tả không gian làm việc', descriptionPlaceholder: 'Những gì thuộc về không gian làm việc này?', namePlaceholder: 'Tên không gian làm việc', - untitledWorkspace: 'Không gian làm việc chưa đặt tên', }, labels: { createWorkspaceTitle: 'Tạo không gian làm việc', diff --git a/ui/src/core/i18n/resources/zh-Hans.ts b/ui/src/core/i18n/resources/zh-Hans.ts index 002ad43..18af59c 100644 --- a/ui/src/core/i18n/resources/zh-Hans.ts +++ b/ui/src/core/i18n/resources/zh-Hans.ts @@ -158,7 +158,6 @@ export const zhHans = { }, descriptions: { emptyDeck: '添加一条笔记,让这个牌组有可复习的内容。', - editorDefault: '专注学习牌组。', editorVisual: '为这个牌组选择封面符号。', notesSearchPlaceholder: '搜索笔记…', }, @@ -190,8 +189,6 @@ export const zhHans = { descriptionLabel: '牌组描述', descriptionPlaceholder: '这个牌组会帮助你复习什么?', namePlaceholder: '牌组名称', - untitledDeck: '未命名牌组', - untitledDeckLower: '未命名牌组', }, labels: { createDeckTitle: '创建牌组', @@ -221,20 +218,31 @@ export const zhHans = { timeout: '耗时过长。请重试。', unauthorized: '请登录以继续。', unavailable: '服务暂时不可用。', + validation: '请检查高亮字段,然后重试。', }, fallback: { unexpected: '意外错误', }, }, + forms: { + validation: { + invalid: '{{field}}无效。', + invalidEnum: '请选择有效的{{field}}。', + invalidFormat: '请输入有效的{{field}}。', + maxLength: '{{field}}最多 {{max}} 个字符。', + maximum: '{{field}}必须小于或等于 {{max}}。', + minLength: '{{field}}至少 {{min}} 个字符。', + minimum: '{{field}}必须大于或等于 {{min}}。', + required: '{{field}}为必填项。', + }, + }, + folders: { actions: { createFolder: '创建文件夹', deleteFolder: '删除文件夹', editFolder: '编辑文件夹', }, - descriptions: { - editorDefault: '用于相关牌组的文件夹。', - }, dialogs: { deleteFolderDescription: '这会将 "{{name}}" 移至回收站。你稍后可以恢复它。', deleteFolderFallbackDescription: '这会将此文件夹移至回收站。你稍后可以恢复它。', @@ -256,7 +264,6 @@ export const zhHans = { descriptionLabel: '文件夹描述', descriptionPlaceholder: '这个文件夹里放什么?', namePlaceholder: '文件夹名称', - untitledFolder: '未命名文件夹', }, labels: { createFolderTitle: '创建文件夹', @@ -586,7 +593,6 @@ export const zhHans = { openingWorkspace: '正在打开 {{title}}', }, descriptions: { - editorDefault: '学习上下文。', editorVisual: '为此工作区选择视觉锚点。', emptyList: '按学习上下文分隔牌组、笔记和复习队列。', }, @@ -611,7 +617,6 @@ export const zhHans = { descriptionLabel: '工作区描述', descriptionPlaceholder: '这个工作区里放什么?', namePlaceholder: '工作区名称', - untitledWorkspace: '未命名工作区', }, labels: { createWorkspaceTitle: '创建工作区', diff --git a/ui/src/core/i18n/resources/zh-Hant.ts b/ui/src/core/i18n/resources/zh-Hant.ts index 8672755..f74c105 100644 --- a/ui/src/core/i18n/resources/zh-Hant.ts +++ b/ui/src/core/i18n/resources/zh-Hant.ts @@ -158,7 +158,6 @@ export const zhHant = { }, descriptions: { emptyDeck: '新增一則筆記,让這個牌組有可複習的內容。', - editorDefault: '专注學習牌組。', editorVisual: '為這個牌組選擇封面符號。', notesSearchPlaceholder: '搜尋筆記…', }, @@ -190,8 +189,6 @@ export const zhHant = { descriptionLabel: '牌組描述', descriptionPlaceholder: '這個牌組會帮助你複習什么?', namePlaceholder: '牌組名稱', - untitledDeck: '未命名牌組', - untitledDeckLower: '未命名牌組', }, labels: { createDeckTitle: '建立牌組', @@ -221,20 +218,31 @@ export const zhHant = { timeout: '耗時過长。請重試。', unauthorized: '請登入以继续。', unavailable: '服務暫時不可用。', + validation: '請檢查醒目標示的欄位,然後再試一次。', }, fallback: { unexpected: '意外錯誤', }, }, + forms: { + validation: { + invalid: '{{field}}無效。', + invalidEnum: '請選擇有效的{{field}}。', + invalidFormat: '請輸入有效的{{field}}。', + maxLength: '{{field}}最多 {{max}} 個字元。', + maximum: '{{field}}必須小於或等於 {{max}}。', + minLength: '{{field}}至少 {{min}} 個字元。', + minimum: '{{field}}必須大於或等於 {{min}}。', + required: '{{field}}為必填欄位。', + }, + }, + folders: { actions: { createFolder: '建立資料夾', deleteFolder: '刪除資料夾', editFolder: '編輯資料夾', }, - descriptions: { - editorDefault: '用于相關牌組的資料夾。', - }, dialogs: { deleteFolderDescription: '這會將 "{{name}}" 移至回收站。你稍後可以還原它。', deleteFolderFallbackDescription: '這會將此資料夾移至回收站。你稍後可以還原它。', @@ -256,7 +264,6 @@ export const zhHant = { descriptionLabel: '資料夾描述', descriptionPlaceholder: '這個資料夾裡放什么?', namePlaceholder: '資料夾名稱', - untitledFolder: '未命名資料夾', }, labels: { createFolderTitle: '建立資料夾', @@ -586,7 +593,6 @@ export const zhHant = { openingWorkspace: '正在打開 {{title}}', }, descriptions: { - editorDefault: '學習脈絡。', editorVisual: '為此工作區選擇視覺锚点。', emptyList: '按學習脈絡分隔牌組、筆記和複習佇列。', }, @@ -611,7 +617,6 @@ export const zhHant = { descriptionLabel: '工作區描述', descriptionPlaceholder: '這個工作區裡放什么?', namePlaceholder: '工作區名稱', - untitledWorkspace: '未命名工作區', }, labels: { createWorkspaceTitle: '建立工作區', diff --git a/ui/src/features/decks/components/DeckCard.stories.tsx b/ui/src/features/decks/components/DeckCard.stories.tsx index 880d39a..edb2078 100644 --- a/ui/src/features/decks/components/DeckCard.stories.tsx +++ b/ui/src/features/decks/components/DeckCard.stories.tsx @@ -210,15 +210,6 @@ export const LongInvalidDate: Story = { }, } -export const EmptyTitle: Story = { - args: { - deck: createDeck({ - id: 'empty-title', - title: '', - }), - }, -} - export const LongTitle: Story = { args: { deck: createDeck({ diff --git a/ui/src/features/decks/components/DeckCard.tsx b/ui/src/features/decks/components/DeckCard.tsx index d46013b..5eaf49f 100644 --- a/ui/src/features/decks/components/DeckCard.tsx +++ b/ui/src/features/decks/components/DeckCard.tsx @@ -46,7 +46,7 @@ export const DeckCard = ({ }: DeckCardProps) => { const { t } = useTranslation() const { formatRelativeDate } = useDateFormatters() - const deckTitle = deck.title.trim() || t(($) => $.decks.fields.untitledDeckLower) + const deckTitle = deck.title.trim() const dueToday = normalizeNonNegativeInteger(deck.dueToday) const dueTodayLabel = formatNonNegativeInteger(deck.dueToday) const hasDueToday = dueToday > 0 diff --git a/ui/src/features/decks/components/DeckEditorForm.stories.tsx b/ui/src/features/decks/components/DeckEditorForm.stories.tsx index 7405929..f0a19ce 100644 --- a/ui/src/features/decks/components/DeckEditorForm.stories.tsx +++ b/ui/src/features/decks/components/DeckEditorForm.stories.tsx @@ -73,6 +73,16 @@ export const WithLocation: Story = { }, } +export const Validation: Story = { + args: { + validationMessages: { + description: ['Description is invalid.'], + icon: ['Visual is invalid.'], + title: ['Name is required.'], + }, + }, +} + export const LongLocation: Story = { args: { description: 'Layout stress case for a deeply nested deck path.', diff --git a/ui/src/features/decks/components/DeckEditorForm.test.tsx b/ui/src/features/decks/components/DeckEditorForm.test.tsx index 1eced5a..42026d8 100644 --- a/ui/src/features/decks/components/DeckEditorForm.test.tsx +++ b/ui/src/features/decks/components/DeckEditorForm.test.tsx @@ -79,4 +79,31 @@ describe('DeckEditorForm', () => { expect(onDescriptionChange).toHaveBeenLastCalledWith('Daily review cards') expect(onIconChange).toHaveBeenLastCalledWith('flask-conical') }) + + it('renders field validation messages beside owned deck fields', () => { + render( + undefined} + onIconChange={() => undefined} + onTitleChange={() => undefined} + />, + ) + + const name = screen.getByLabelText('Deck name') + const description = screen.getByLabelText('Deck description') + + expect(name).toHaveAttribute('aria-invalid', 'true') + expect(name).toHaveAccessibleDescription('Name is required.') + expect(description).toHaveAttribute('aria-invalid', 'true') + expect(description).toHaveAccessibleDescription('Description is invalid.') + expect(screen.getByText('Visual is invalid.')).toBeInTheDocument() + }) }) diff --git a/ui/src/features/decks/components/DeckEditorForm.tsx b/ui/src/features/decks/components/DeckEditorForm.tsx index 70685bd..b2b1e7e 100644 --- a/ui/src/features/decks/components/DeckEditorForm.tsx +++ b/ui/src/features/decks/components/DeckEditorForm.tsx @@ -2,6 +2,7 @@ import { Folder } from 'lucide-react' import { useTranslation } from 'react-i18next' import type { VisualIconName } from '@shared/components/icons/IconGlyph' +import { FieldValidationMessages } from '@shared/components/forms/FieldValidationMessages' import { VisualPicker } from '@shared/components/forms/VisualPicker' import { SectionHeading } from '@shared/components/layout/Screen' import { Card } from '@shared/components/ui/card' @@ -14,6 +15,12 @@ import { cn } from '@shared/lib/utils' import { deckPresetVisualOptions } from '../constants/visuals' +export type DeckEditorValidationMessages = { + description?: string[] + icon?: string[] + title?: string[] +} + export const DeckEditorForm = ({ description, icon, @@ -22,6 +29,7 @@ export const DeckEditorForm = ({ onIconChange, onTitleChange, title, + validationMessages, }: { description: string icon: VisualIconName @@ -30,12 +38,22 @@ export const DeckEditorForm = ({ onIconChange: (value: VisualIconName) => void onTitleChange: (value: string) => void title: string + validationMessages?: DeckEditorValidationMessages }) => { const { t } = useTranslation() const locationLabel = locationPath ? formatLocationPathLabel(locationPath) : undefined const compactLocationLabel = locationPath ? formatCompactLocationPath(locationPath) : undefined + const titleErrorId = validationMessages?.title?.length + ? 'deck-name-error' + : undefined + const descriptionErrorId = validationMessages?.description?.length + ? 'deck-description-error' + : undefined + const iconErrorId = validationMessages?.icon?.length + ? 'deck-visual-error' + : undefined return ( @@ -60,6 +78,8 @@ export const DeckEditorForm = ({