feat(upload): Implement comprehensive file upload and management system#91
Conversation
- Add new file upload components for evidence and task attachments - Create hooks for file upload, preview, delete, and URL management - Implement server actions for file operations with AWS S3 integration - Remove UploadThing dependencies and replace with custom S3 upload solution - Update evidence and task pages to use new file management components - Add support for different upload types (evidence and risk tasks) - Enhance file handling with preview, delete, and URL management features
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
|
Warning Rate limit exceeded@carhartlewis has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 3 minutes and 3 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (3)
WalkthroughThis pull request introduces modifications to the application’s file management system. New server-side actions have been added for uploading, deleting, retrieving signed URLs from AWS S3, and updating evidence URLs. Client-side functionalities are extended with new React components and custom hooks for file uploads, previews, deletion, and URL management. The changes also include refactoring updates in dashboard pages, layout adjustments, removal of obsolete UploadThing integrations, and elimination of deprecated environment variables, along with various formatting improvements. Changes
Sequence Diagram(s)sequenceDiagram
participant U as User
participant FU as FileUpload Component
participant H as useFileUpload Hook
participant API as uploadFile Action
participant S3 as AWS S3
U->>FU: Select file for upload
FU->>H: Trigger file upload
H->>API: Request presigned URL and upload parameters
API->>S3: Generate signed URL for file upload
S3-->>API: Return signed URL
API-->>H: Return upload details (upload URL, file URL)
H->>S3: Upload file using PUT request
S3-->>H: Confirm file upload
H->>FU: Invoke onSuccess callback
FU-->>U: Display uploaded file confirmation
sequenceDiagram
participant U as User
participant FC as FileCard/Deletion UI
participant H as useFileDelete Hook
participant API as deleteFile Action
participant DB as Database
U->>FC: Initiate file deletion
FC->>H: Trigger delete action with file URL
H->>API: Call deleteFile with necessary parameters
API->>DB: Remove file reference from evidence or task record
DB-->>API: Confirm deletion
API-->>H: Return success response
H->>FC: Execute onSuccess callback
FC-->>U: Confirm file deletion
Possibly related PRs
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (32)
apps/app/src/components/upload/FileIcon.tsx (2)
1-19: Clean component implementation with room for minor optimizations.The
FileIconcomponent is well-structured and follows good React practices. It effectively maps file extensions to appropriate icons based on file type categories.Consider these optional improvements:
- Extract the repeated styling to avoid duplication:
- <FileImage className="h-12 w-12 text-muted-foreground" /> - <FileText className="h-12 w-12 text-muted-foreground" /> - <File className="h-12 w-12 text-muted-foreground" /> + const iconClassName = "h-12 w-12 text-muted-foreground"; + ... + <FileImage className={iconClassName} /> + <FileText className={iconClassName} /> + <File className={iconClassName} />
- For future extensibility, consider using a mapping object for file types:
+ const ICON_MAPPING = { + image: ["jpg", "jpeg", "png", "gif", "webp"], + document: ["pdf", "doc", "docx", "txt"], + // Add more categories as needed (e.g., video, audio, spreadsheet) + }; + + export function FileIcon({ fileName }: FileIconProps) { + const extension = fileName.split(".").pop()?.toLowerCase(); + const iconClassName = "h-12 w-12 text-muted-foreground"; + + if (extension && ICON_MAPPING.image.includes(extension)) { + return <FileImage className={iconClassName} />; + } + if (extension && ICON_MAPPING.document.includes(extension)) { + return <FileText className={iconClassName} />; + } + return <File className={iconClassName} />; + }
- Consider adding support for additional common file types like video, audio, spreadsheets, etc.
10-10: Improve handling of filenames without extensions.The current implementation might have issues with filenames that don't contain periods.
Consider adding a null check before accessing the extension:
- const extension = fileName.split(".").pop()?.toLowerCase(); + const parts = fileName.split("."); + const extension = parts.length > 1 ? parts.pop()?.toLowerCase() : "";This ensures that for filenames without dots, you'll get an empty string instead of potentially the entire filename converted to lowercase.
apps/web/src/app/layout.tsx (1)
22-27: Good conditional initialization of analytics.The code appropriately checks for the presence of both required environment variables before initializing the analytics server. This prevents runtime errors that could occur if the variables were undefined.
However, it might be beneficial to add error handling in case the initialization fails:
if (env.NEXT_PUBLIC_POSTHOG_KEY && env.NEXT_PUBLIC_POSTHOG_HOST) { + try { initializeServer({ apiKey: env.NEXT_PUBLIC_POSTHOG_KEY, apiHost: env.NEXT_PUBLIC_POSTHOG_HOST, }); + } catch (error) { + console.error("Failed to initialize analytics:", error); + } }apps/app/src/actions/files/update-urls.ts (1)
32-45: Consider optimizing database operations.The current implementation makes two separate database calls: first checking if the evidence exists, then updating it. This creates an unnecessary database roundtrip.
Consider combining these operations into a single update call with proper error handling:
-try { - const evidence = await db.organizationEvidence.findFirst({ - where: { - id: evidenceId, - organizationId: user.organizationId, - }, - }); - - if (!evidence) { - return { - success: false, - error: "Evidence not found", - } as const; - } - - const updatedEvidence = await db.organizationEvidence.update({ - where: { id: evidenceId }, +try { + const updatedEvidence = await db.organizationEvidence.update({ + where: { + id: evidenceId, + organizationId: user.organizationId + }, data: { additionalUrls: urls, }, include: { evidence: true, }, }); return { success: true, data: updatedEvidence, } as const; } catch (error) { + // Check if this is a "not found" error from Prisma + if (error.code === 'P2025') { + return { + success: false, + error: "Evidence not found", + } as const; + } console.error("Error updating evidence URLs:", error); return { success: false, error: "Failed to update evidence URLs", } as const; }apps/app/src/hooks/upload/use-file-delete.ts (1)
35-41: Consider enhancing error messages.The error handling could be more specific to help with debugging and user feedback.
if (!response?.data) { - throw new Error("Failed to delete file"); + throw new Error("Delete file response is empty or undefined"); } if (!response.data.success) { - throw new Error(response.data.error || "Failed to delete file"); + throw new Error(response.data.error || "Server reported failure during file deletion"); }apps/app/src/hooks/upload/use-file-preview.ts (2)
39-75: Consider refactoring duplicate code paths for evidence and riskTask retrieval.Both blocks for
evidenceandriskTaskshare very similar logic to get and validate the signed URL. Consider abstracting the shared request logic to reduce code duplication and improve maintainability.- if (uploadType === UPLOAD_TYPE.evidence && evidenceId) { - const response = await getFileUrl({ - uploadType, - fileUrl, - evidenceId, - }); - ... - } - if (uploadType === UPLOAD_TYPE.riskTask && taskId) { - const response = await getFileUrl({ - uploadType, - fileUrl, - taskId, - }); - ... - } + const params = { uploadType, fileUrl }; + if (uploadType === UPLOAD_TYPE.evidence && evidenceId) { + (params as any).evidenceId = evidenceId; + } else if (uploadType === UPLOAD_TYPE.riskTask && taskId) { + (params as any).taskId = taskId; + } else { + throw new Error( + `Missing required parameters for upload type: ${uploadType}`, + ); + } + const response = await getFileUrl(params); + if (!response?.data) { + throw new Error("Failed to get signed URL"); + } + const { signedUrl } = response.data as FileUrlResponse; + if (!signedUrl) { + throw new Error("Invalid signed URL response"); + } + return signedUrl;
80-92: Enhance error handling with additional context.When showing error messages to users, consider including more descriptive details or possible next steps to help troubleshoot. Logging a unique error identifier (or the original request data) can make debugging easier.
apps/app/src/components/risks/tasks/task-attachments.tsx (2)
27-29: Provide more robust error feedback.Returning a plain "Error" offers limited guidance to the user. Consider providing more descriptive text or a retry option for a better user experience.
- if (error) { - return <div>Error</div>; - } + if (error) { + return ( + <div> + <p>Failed to load task attachments.</p> + <button onClick={() => mutate()}>Retry</button> + </div> + ); + }
35-37: Handle empty attachments gracefully.Currently, when
data?.datais empty, the component returnsnull. Consider providing a helpful UI message (e.g., "No attachments found") or a prompt to add new attachments.apps/app/src/actions/files/get-file-url.ts (1)
30-41: Improve error messages for S3 key extraction failure.The extracted S3 key might fail in unexpected ways. Providing a more detailed error message can expedite debugging and guide users to correct invalid or non-S3 URLs.
apps/app/src/hooks/upload/use-url-management.ts (2)
47-54: Consider destructuring evidenceId and currentUrls early for null checking.While the implementation works, consider defensive coding by checking if the required properties are defined early in the function.
export function useUrlManagement({ evidenceId, currentUrls, onSuccess, }: UseUrlManagementProps) { + // Ensure required values are defined + if (!evidenceId || !currentUrls) { + throw new Error("evidenceId and currentUrls are required for URL management"); + } const [draftUrls, setDraftUrls] = useState<DraftUrl[]>([]); const { toast } = useToast();
73-120: Ensure proper error handling and consider refining duplicate logic.The URL saving function has comprehensive error handling and proper validation. However, there's a potential redundancy in the error message where
result.serverErroris used both as a condition and in the error message. The URL combination logic is concise but could be more explicit.if (result.serverError) { - throw new Error(result.serverError || "Failed to update URLs"); + throw new Error(result.serverError); } // Consider adding more explicit documentation for the URL deduplication // Combine current URLs with new ones, removing duplicates const allUrls = [...new Set([...currentUrls, ...formattedDrafts])];apps/app/src/components/upload/FileUrl.tsx (2)
40-47: Improve error handling in the domain extraction utility.The domain extraction function has appropriate error handling, but it could be enhanced to provide more meaningful fallbacks.
const getDomainName = (url: string) => { try { const domain = new URL(url).hostname; return domain.replace(/^www\./, ""); } catch (e) { - return url; + // Provide a more informative fallback or truncate long URLs + return url.length > 30 ? url.substring(0, 27) + "..." : url; } };
50-52: Add error handling and user feedback for clipboard operations.The clipboard function is simple but lacks error handling and user feedback mechanisms.
const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); + navigator.clipboard.writeText(text) + .then(() => { + // Consider adding a temporary visual feedback or toast message + console.log("URL copied to clipboard"); + }) + .catch((err) => { + console.error("Failed to copy URL:", err); + }); };apps/app/src/components/upload/FileSection.tsx (2)
26-53: Handle undefined IDs more robustly in hook initialization.The component passes empty strings as fallbacks for potentially undefined IDs, which might not be a robust approach if these values are required by the hooks.
const { isUploading, handleFileUpload } = useFileUpload({ uploadType, - evidenceId: evidenceId || "", - taskId: taskId || "", + evidenceId, + taskId, onSuccess, }); const { handleDelete } = useFileDelete({ uploadType, - evidenceId: evidenceId || "", - taskId: taskId || "", + evidenceId, + taskId, onSuccess, }); const { getPreviewUrl } = useFilePreview({ uploadType, fileUrl: "", - evidenceId: evidenceId || "", - taskId: taskId || "", + evidenceId, + taskId, });Then update the hook implementations to handle undefined values internally.
59-87: Optimize dependency array in useCallback to prevent potential issues.The
handlePreviewClickdependency array includespreviewStates, which could cause unnecessary re-renders or create potential issues with callback stability.const handlePreviewClick = useCallback( async (fileUrl: string) => { try { // Skip if already loading or loaded - if (previewStates[fileUrl]?.isLoading || previewStates[fileUrl]?.url) { + // Get current state within the callback to avoid dependency + const currentState = previewStates[fileUrl]; + if (currentState?.isLoading || currentState?.url) { return; } setPreviewStates((prev) => ({ ...prev, [fileUrl]: { url: null, isLoading: true }, })); const previewUrl = await getPreviewUrl(fileUrl); setPreviewStates((prev) => ({ ...prev, [fileUrl]: { url: previewUrl, isLoading: false }, })); } catch (error) { console.error("Error loading preview:", error); setPreviewStates((prev) => ({ ...prev, [fileUrl]: { url: null, isLoading: false }, })); } }, - [getPreviewUrl, previewStates], + [getPreviewUrl], );apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts (1)
24-33: Consider adding revalidation options or customization.The hook disables revalidation on focus and reconnect, which might be appropriate for this specific use case, but consider allowing props to customize this behavior.
-export function useTaskAttachments({ id }: UseTaskAttachmentProps) { +export function useTaskAttachments({ + id, + revalidateOnFocus = false, + revalidateOnReconnect = false +}: UseTaskAttachmentProps & { + revalidateOnFocus?: boolean; + revalidateOnReconnect?: boolean; +}) { return useSWR( ["task-attachments", id], () => fetchTaskAttachments({ id }), { - revalidateOnFocus: false, - revalidateOnReconnect: false, + revalidateOnFocus, + revalidateOnReconnect, } ); }apps/app/src/components/upload/FileCard.tsx (2)
56-57: Consider supporting additional file types beyond images and PDFs.
The current regex checks only handle image and PDF extensions. If your application might handle other file types (e.g.,.docx,.xlsx,.txt, etc.), consider extending this logic or using a more robust approach that accounts for MIME types.
82-119: Reduce duplication of dialog trigger logic between images and PDFs.
The blocks for images and PDFs both contain an almost identicalDialogTriggerand button to maximize the preview. Extracting common logic or using a helper component can simplify the code and improve maintainability.apps/app/src/actions/files/upload-file.ts (3)
15-21: Enable server-side encryption and/or limit ACL for uploaded objects.
When uploading files to S3, consider specifyingServerSideEncryptionand restricting the ACL to enhance data security. This is often a best practice, especially for sensitive or private user data.
77-79: Expand sanitization checks to handle large or malformed file names.
Currently, the code replaces non-alphanumeric characters with underscores. You might also limit the overall file name length to avoid issues on certain file systems or in logs.
168-172: Provide more informative error feedback or logging.
Right now, the catch block returns a generic “Failed to upload file” message. Improved error logging (e.g., logging the exception details or returning a specific error) makes diagnosing issues faster.apps/app/src/actions/files/delete-file.ts (2)
55-85: Decide if you need to delete the file from S3 as well.
Currently, the code only removes the file URL from the database but leaves the file in S3. If you want to fully remove it to free storage or prevent orphaned data, incorporate S3 deletion.
87-119: Ensure consistent cleanup for risk task file attachments on S3.
Similar to evidence deletion, the actual file remains in S3 even though the DB reference is removed. Confirm whether this is acceptable or integrate a process to remove the S3 object if desired.apps/app/src/hooks/upload/use-file-upload.ts (3)
11-14: Consider simplifying the response type structureThe nested data structure in
ServerResponseseems unnecessarily complex with multiple layers ofdataobjects. This could be simplified for better readability and type safety.interface UploadUrlResponse { uploadUrl: string; fileUrl: string; } -interface ServerResponse { - data: { - data: UploadUrlResponse; - }; - serverError?: string; -} +interface ServerResponse { + data: UploadUrlResponse; + serverError?: string; +}Then update references to this structure accordingly:
-const { uploadUrl } = urlResult.data.data; +const { uploadUrl } = urlResult.data;Also applies to: 16-21
58-70: Enhance error handling for empty IDsWhen
evidenceIdortaskIdare undefined, empty strings are passed to the upload function. This may cause unexpected behavior if the server expects valid IDs.// Get the presigned URL const urlResult = (await uploadFile({ uploadType, - evidenceId: evidenceId || "", - taskId: taskId || "", + ...(evidenceId ? { evidenceId } : {}), + ...(taskId ? { taskId } : {}), fileName: file.name, fileType: file.type, })) as ServerResponse; // Validate server response -if (!urlResult?.data?.data?.uploadUrl) { +if (!urlResult?.data?.uploadUrl) { throw new Error(urlResult?.serverError || "Failed to get upload URL"); }
80-87: Consider exposing upload progress to the UIThe hook tracks upload progress but only logs it to the console. Consider exposing this to the UI for a better user experience.
+const [uploadProgress, setUploadProgress] = useState<number>(0); // ...in the axios call onUploadProgress: (progressEvent: AxiosProgressEvent): void => { if (progressEvent.total) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total, ); console.debug(`Upload progress: ${percentCompleted}%`); + setUploadProgress(percentCompleted); } }, // ...in the return object return { isUploading, handleFileUpload, + uploadProgress, };apps/app/src/components/upload/FileUpload.tsx (3)
73-126: Enhance accessibility for the card variantThe card variant lacks proper accessibility attributes. Add appropriate ARIA attributes to improve screen reader support.
<Card className={cn( "group cursor-pointer transition-all hover:shadow-md border-dashed border-2 border-primary/30 hover:border-primary flex flex-col overflow-hidden", cardHeight, isDragActive && "border-primary border-2 bg-primary/5", isDragAccept && "border-green-500 border-2 bg-green-50", isDragReject && "border-red-500 border-2 bg-red-50", isUploading && "pointer-events-none opacity-60", )} + role="button" + aria-label="Upload file" + aria-disabled={isUploading} >
114-116: Make text content contextualThe text "Upload additional evidence files" is hardcoded, but this component might be used for different types of uploads (like task attachments). Consider making this text configurable through props.
interface FileUploadProps { onFileSelect: (file: File) => void; isUploading: boolean; accept?: Record<string, string[]>; maxSize?: number; variant?: "default" | "card"; cardHeight?: string; + uploadDescription?: string; } // Then in the component <p className="text-xs text-muted-foreground mt-1 text-center"> - Upload additional evidence files + {uploadDescription || "Upload file"} </p>
128-165: Extract the file size calculation for better readabilityThe calculation for displaying the max file size in MB could be extracted to improve readability.
+const formatFileSizeToMB = (bytes: number) => Math.round(bytes / 1024 / 1024); // Default variant return ( // ... <p className="mt-1"> - Max file size: {Math.round(maxSize / 1024 / 1024)}MB + Max file size: {formatFileSizeToMB(maxSize)}MB </p> // ... );apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx (2)
52-56: Consider optimizing database queryThe task query includes
TaskAttachmentbut this data doesn't seem to be directly used in the component anymore, as the TaskAttachments component now presumably fetches its own data. Consider removing this include to optimize the query.include: { owner: true, - TaskAttachment: true, TaskComments: true, },
30-32: Enhance redirection on task not foundWhen a task is not found, the user is redirected to "/risk". Consider redirecting to the specific risk page since you have the riskId available.
if (!task) { - redirect("/risk"); + redirect(`/risk/${riskId}`); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
.vscode/settings.json(1 hunks)apps/app/src/actions/files/delete-file.ts(1 hunks)apps/app/src/actions/files/get-file-url.ts(1 hunks)apps/app/src/actions/files/update-urls.ts(1 hunks)apps/app/src/actions/files/upload-file.ts(1 hunks)apps/app/src/actions/types.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/actions/deleteEvidenceFile.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/hooks/useFileDelete.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx(2 hunks)apps/app/src/app/[locale]/layout.tsx(0 hunks)apps/app/src/app/[locale]/providers.tsx(1 hunks)apps/app/src/app/api/uploadthing/core.ts(0 hunks)apps/app/src/app/api/uploadthing/route.ts(0 hunks)apps/app/src/components/risks/tasks/task-attachment.tsx(0 hunks)apps/app/src/components/risks/tasks/task-attachments.tsx(1 hunks)apps/app/src/components/upload/FileCard.tsx(1 hunks)apps/app/src/components/upload/FileIcon.tsx(1 hunks)apps/app/src/components/upload/FileSection.tsx(1 hunks)apps/app/src/components/upload/FileUpload.tsx(1 hunks)apps/app/src/components/upload/FileUrl.tsx(1 hunks)apps/app/src/env.mjs(0 hunks)apps/app/src/hooks/upload/use-file-delete.ts(1 hunks)apps/app/src/hooks/upload/use-file-preview.ts(1 hunks)apps/app/src/hooks/upload/use-file-upload.ts(1 hunks)apps/app/src/hooks/upload/use-url-management.ts(1 hunks)apps/web/src/app/(home)/layout.tsx(0 hunks)apps/web/src/app/components/main-nav.tsx(1 hunks)apps/web/src/app/layout.tsx(2 hunks)apps/web/src/app/providers.tsx(1 hunks)
💤 Files with no reviewable changes (6)
- apps/web/src/app/(home)/layout.tsx
- apps/app/src/app/[locale]/layout.tsx
- apps/app/src/env.mjs
- apps/app/src/components/risks/tasks/task-attachment.tsx
- apps/app/src/app/api/uploadthing/core.ts
- apps/app/src/app/api/uploadthing/route.ts
✅ Files skipped from review due to trivial changes (4)
- .vscode/settings.json
- apps/app/src/actions/types.ts
- apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/actions/deleteEvidenceFile.ts
- apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/hooks/useFileDelete.ts
🔇 Additional comments (48)
apps/web/src/app/components/main-nav.tsx (1)
9-9: Great accessibility improvement with the aria-label addition!Adding an aria-label to the home link enhances the application's accessibility by providing clear context for screen reader users. This follows WCAG (Web Content Accessibility Guidelines) best practices by ensuring non-visual users understand the purpose and destination of this navigation element.
apps/app/src/app/[locale]/providers.tsx (1)
27-27:Details
✅ Verification successful
Ensure Cloudflare optimizations don't interfere with theme switching.
The addition of
scriptProps={{ "data-cfasync": "false" }}is a good practice to prevent Cloudflare's Rocket Loader from optimizing theme-related scripts. This can help avoid potential issues with theme initialization or switching, especially if there are timing dependencies in the theme logic that might be affected by script optimization.
🌐 Web query:
What is the purpose of data-cfasync="false" attribute with Cloudflare?💡 Result:
The
data-cfasync="false"attribute is used to exclude specific JavaScript scripts from Cloudflare's Rocket Loader optimization feature. Rocket Loader improves page load times by deferring JavaScript execution but can cause compatibility issues with scripts requiring immediate execution or specific loading order. Here’s a detailed breakdown:Key Purposes
Prevent Rocket Loader Interference
Addingdata-cfasync="false"instructs Cloudflare to bypass Rocket Loader for the tagged script, ensuring it loads synchronously and avoids delays. This is critical for:
- Tracking scripts (e.g., Google Analytics, Convert Experiences) that must fire immediately[8][10].
- Scripts reliant on execution order (e.g., jQuery dependencies)[2][9].
Resolve Script Errors
Rocket Loader modifies script tags (e.g., appending hashes totypeattributes liketype="ea10e272b0-text/javascript"), which can break HTML validation or Content Security Policies (CSP)[4]. Excluding scripts prevents such modifications.Avoid Flicker/Delayed Rendering
Without this attribute, deferred loading by Rocket Loader can cause visual issues like layout shifts or delayed element rendering[2][10].Implementation Guidelines
Syntax: Place the attribute before the
srcattribute and use standard quotes:<script data-cfasync="false" src="example.js"></script>Incorrect syntax (e.g., smart quotes
“false”or misplaced attributes) will fail[2][3].CMS/Platform-Specific:
- WordPress: Modify enqueued scripts in
functions.phpto include the attribute[1].- AEM/Sightly: Use custom clientlib logic or adjust template syntax[3].
- GTM/Third-Party Scripts: Manually add the attribute to tracking code snippets[7][8].
When to Use
- Scripts causing layout shifts or functionality issues when Rocket Loader is active.
- Compliance with CSP directives or HTML validation requirements[4][6].
- Critical scripts requiring immediate execution (e.g., A/B testing tools, analytics)[7][10].
Alternatives
- Disable Rocket Loader entirely in Cloudflare’s dashboard (not recommended for performance)[4].
- Use Cloudflare’s Manual Mode to selectively exclude scripts via the dashboard[6].
Citations:
- 1: https://community.cloudflare.com/t/setting-data-cfasync-true-for-google-map-script-in-wordpress/16737
- 2: https://community.cloudflare.com/t/ignore-script-rocket-loader/284792
- 3: https://experienceleaguecommunities.adobe.com/t5/adobe-experience-manager/how-to-add-data-cfsync-false-attribute-in-sightly-to-ignore-js/td-p/575288
- 4: https://www.sitelint.com/blog/resolving-the-script-tag-type-attribute-hash-prepending-issue-with-cloudflare
- 5: https://my.ultrawebhosting.com/knowledgebase/381/Prevent-CloudFlare-from-Loading-a-js-or-Script.html
- 6: https://docs.exoclick.com/docs/faqs/publishers/compliance/cloudflare-rocket-loader-avoid-issues/
- 7: https://support.convert.com/hc/en-us/articles/205151115-run-convert-experiences-on-cloudflare
- 8: https://www.optimizesmart.com/how-cloudflare-impacts-google-analytics-stats/
- 9: https://community.wpbeaverbuilder.com/t/cloudflares-rocket-loader-breaking-bb-page-builder-text-editor/4322
- 10: https://support.convert.com/hc/en-us/articles/13763412056077-preventing-flicker-in-cloudflare-rocket-loader-integration
Cloudflare Rocket Loader Exclusion Verified
The use of
scriptProps={{ "data-cfasync": "false" }}is correctly implemented here. This attribute instructs Cloudflare's Rocket Loader to bypass optimizing this script, ensuring that theme-related scripts execute immediately and maintain their intended order. This prevents any potential issues with the timing dependencies of your theme initialization and switching logic.apps/web/src/app/providers.tsx (3)
13-34: Improved provider structure with better default theme handling.I like the refactoring of the
Providerscomponent. The changes:
- Simplify the code by removing the separate
AnalyticsWrappercomponent- Make the analytics integration more straightforward
- Improve user experience by using
defaultTheme="system"andenableSystemto respect user preferences- Add the
data-cfasync="false"attribute for consistent Cloudflare script handling across the applicationThis aligns well with modern best practices for theme handling in Next.js applications.
22-22: Consistent Cloudflare optimization control across files.The
scriptProps={{ "data-cfasync": "false" }}addition here matches the same change inapps/app/src/app/[locale]/providers.tsx, ensuring consistent handling of theme-related scripts across different parts of the application. This is especially important if the new file upload components need stable theme behavior for proper styling and interaction.
19-20: Better theme defaults for improved user experience.Changing from
defaultTheme="dark"todefaultTheme="system"withenableSystemimproves the user experience by respecting the user's system preferences by default. This is particularly important for applications handling file uploads and previews, where content visibility can be affected by theming.apps/web/src/app/layout.tsx (1)
10-10: Clean import for analytics integration.The import of the
initializeServerfunction from@bubba/analyticsis properly structured and follows good module import practices.apps/app/src/actions/files/update-urls.ts (4)
7-21: Well-structured action with proper schema validation and tracking.The server action is correctly set up with:
- Proper Zod schema validation for input parameters
- Good metadata for tracking and monitoring
- Clear naming convention
25-30: Appropriate authorization check.The authorization check correctly verifies the user's organization membership before proceeding with the database operation.
47-60: Good database update with proper return type.The evidence update operation includes the relevant evidence relation and returns a properly typed response.
61-67: Comprehensive error handling.Good error handling with console logging and a user-friendly error message.
apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts (4)
7-21: Well-structured server action with proper schema validation.The server action correctly uses Zod for input validation and includes appropriate metadata for tracking.
24-29: Proper authorization check.The code appropriately verifies the user's organization membership before accessing the data.
31-41: Correct database query with appropriate field selection.The database query is well-structured with proper field selection to retrieve only the necessary data.
54-60: Good error handling with console logging.The error handling code appropriately logs the error and returns a user-friendly error message.
apps/app/src/hooks/upload/use-file-delete.ts (5)
8-15: Well-defined types for upload operations.The type definitions are clear, strongly typed, and provide appropriate optional properties for flexibility.
17-24: Clear hook interface with destructured props.The hook provides a clean interface that makes it easy to understand the required and optional parameters.
25-42: Good error handling and validation in delete operation.The function properly validates the response from the server action and handles potential errors.
43-57: Good user feedback with toast notifications.The code provides appropriate feedback to users on both success and failure conditions.
62-64: Clean return interface.The hook returns a well-named function that clearly indicates its purpose.
apps/app/src/hooks/upload/use-url-management.ts (6)
1-7: LGTM - Good imports and type definitions.The client directive is correctly included, and the imports are well-organized with appropriate types being imported. The action import from the files directory indicates good separation of concerns.
8-23: Clean interface definitions with clear purpose.The interfaces are well-structured with descriptive names and appropriate types. The
DraftUrlinterface for managing unsaved URLs and the response typeUpdateUrlsActionResponseextending ActionResponse show good type design patterns.
24-45: Well-implemented URL validation and formatting utilities.The URL validation and formatting functions are concise and handle edge cases appropriately. The
isValidUrlfunction correctly uses a try-catch block to handle URL parsing, andformatUrladds the HTTPS protocol if missing.
55-71: LGTM - Clean implementation of draft URL management.The callbacks for adding, updating, and removing draft URLs are well-implemented and properly memoized with useCallback for better performance.
122-155: LGTM - Well-implemented URL removal functionality.The URL removal function is well-structured with proper error handling and success messages. The implementation correctly updates the server and provides appropriate user feedback.
157-165: LGTM - Clean API exports with all necessary functions.The hook exports all the necessary functions for URL management, making it easy to integrate with UI components.
apps/app/src/components/upload/FileUrl.tsx (6)
1-14: LGTM - Component appropriately imports UI dependencies and hooks.The component correctly imports all necessary UI components from the design system and the URL management hook created earlier.
15-37: LGTM - Clean props interface and proper hook setup.The component has a well-defined props interface and correctly initializes the URL management hook with the provided props.
54-75: LGTM - Excellent UI layout with responsive design.The component header has a clean layout with a logical structure, showing the number of links added and providing an add button when no drafts are present.
78-130: LGTM - Well-implemented draft URL input section with good UX.The draft URL input section has excellent UX features, including keyboard shortcuts (Enter to save) and appropriate buttons for adding and saving links.
132-163: LGTM - Clean URL list rendering with informative UI.The URL list section has a logical structure and provides useful information about each URL, including the domain name and full URL.
164-224: LGTM - Excellent action buttons with tooltips for better usability.The action buttons for each URL are well-implemented with tooltips that improve usability. The buttons are appropriately sized and have clear icons.
apps/app/src/components/upload/FileSection.tsx (4)
1-19: LGTM - Well-structured imports and prop types.The component properly defines all necessary types and interfaces, with clear purpose for each prop. The type definition for UploadType handles enums from the action types module correctly.
54-58: LGTM - Clean state management.The component uses appropriate state management for tracking preview states and dialog open state with clear types.
89-101: LGTM - Efficient preview loading with useEffect.The useEffect hook efficiently loads previews for files that haven't been previewed yet when the fileUrls prop changes.
103-151: LGTM - Well-structured UI with responsive grid layout.The component has a clean UI structure with a responsive grid layout that adapts to different screen sizes. The file upload component and file cards are well-integrated.
apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts (2)
3-8: LGTM - Clean interface definition.The hook imports and interface are well-structured, with a clear purpose for the props.
10-22: LGTM - Well-implemented fetch function with error handling.The fetch function correctly handles errors and returns the appropriate data structure.
apps/app/src/components/upload/FileCard.tsx (1)
62-73: Validate that onPreviewClick won't be triggered repeatedly in certain re-renders.
BecausehasLoadedPreviewandpreviewState.isLoadingare used to control loading, re-renders could cause multiple calls if the preview state isn’t managed carefully. Double-check that no extraneous preview requests are made during normal usage, especially ifurlor props change unexpectedly.apps/app/src/hooks/upload/use-file-upload.ts (4)
3-7: Good imports and type usageThe component properly imports React hooks, toast notification, axios with its type, and upload type from the actions. This provides a solid foundation for the file upload functionality.
35-43: Hook interface looks goodThe hook's interface is well-defined with clear props. It appropriately supports optional IDs for different upload contexts and provides a callback for post-upload success handling.
74-88: Good implementation of S3 direct uploadThe implementation correctly uses axios for the S3 upload with appropriate headers and progress tracking. The transformRequest ensures the file is sent correctly, and the progress tracking provides feedback during upload.
97-111: Excellent error handlingThe error handling is comprehensive, differentiating between Axios errors and general errors. The user is provided with appropriate feedback through toast notifications.
apps/app/src/components/upload/FileUpload.tsx (3)
33-45: Comprehensive file type supportThe default supported file types are well-chosen, covering common document and image formats that users might need to upload as evidence or task attachments.
50-57: Efficient file handling implementationThe onDrop callback correctly handles the first file from the dropped files array, which aligns with the
multiple: falsesetting in the dropzone configuration.
59-71: Good dropzone configurationThe configuration for react-dropzone is appropriately set up with the necessary options, including disabling the dropzone when a file is uploading.
apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/page.tsx (3)
4-4: Import path correction looks goodThe import for TaskAttachments has been correctly updated.
36-42: Simplified TaskAttachments implementationThe TaskAttachments component now only requires taskId, improving its API and making it more self-contained. This is a good architecture decision.
13-13:Details
✅ Verification successful
Verify if params should be a Promise
The
paramsprop is typed as a Promise, which is unusual for Next.js page components. Verify if this is intentional or if it should be a plain object.
🏁 Script executed:
#!/bin/bash # Check other page components to see if they also treat params as a Promise rg -A 2 -B 2 "interface PageProps" --type tsLength of output: 3438
Validation of
paramsType UsageThe usage of
paramsas a Promise is consistent across multiple page components (e.g., in/controls/[id]/page.tsx,/risk/[riskId]/comments/page.tsx, and others). This confirms that the design is intentional and correct. No changes are needed.
| if (!attachments) { | ||
| return { | ||
| success: false, | ||
| error: "Task attachments not found", | ||
| }; | ||
| } |
There was a problem hiding this comment.
Fix incorrect empty results check.
The current check if (!attachments) is incorrect because findMany() always returns an array (potentially empty), never null or undefined.
Fix the check to properly verify if there are no attachments:
-if (!attachments) {
+if (attachments.length === 0) {
return {
success: false,
error: "Task attachments not found",
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!attachments) { | |
| return { | |
| success: false, | |
| error: "Task attachments not found", | |
| }; | |
| } | |
| if (attachments.length === 0) { | |
| return { | |
| success: false, | |
| error: "Task attachments not found", | |
| }; | |
| } |
| [evidenceId, onSuccess, toast], | ||
| ); |
There was a problem hiding this comment.
Fix incomplete dependency array in useCallback.
The dependency array is missing uploadType and taskId which could lead to stale closures if these values change.
[evidenceId, onSuccess, toast],
+[evidenceId, taskId, uploadType, onSuccess, toast],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [evidenceId, onSuccess, toast], | |
| ); | |
| [evidenceId, taskId, uploadType, onSuccess, toast], |
| const evidence = await db.organizationEvidence.findFirst({ | ||
| where: { | ||
| id: parsedInput.evidenceId, | ||
| }, | ||
| }); | ||
|
|
||
| if (!evidence) { | ||
| throw new Error("Evidence or file not found"); | ||
| } |
There was a problem hiding this comment.
Validate organization ownership for evidence.
Currently, only the evidenceId is checked. Ensure that the organization ID of the evidence matches the user’s organizationId to prevent unauthorized access under a guessed ID.
const evidence = await db.organizationEvidence.findFirst({
- where: {
- id: parsedInput.evidenceId,
- },
+ where: {
+ id: parsedInput.evidenceId,
+ organizationId: user.organizationId,
+ },
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const evidence = await db.organizationEvidence.findFirst({ | |
| where: { | |
| id: parsedInput.evidenceId, | |
| }, | |
| }); | |
| if (!evidence) { | |
| throw new Error("Evidence or file not found"); | |
| } | |
| const evidence = await db.organizationEvidence.findFirst({ | |
| where: { | |
| id: parsedInput.evidenceId, | |
| organizationId: user.organizationId, | |
| }, | |
| }); | |
| if (!evidence) { | |
| throw new Error("Evidence or file not found"); | |
| } |
| if (uploadType === UPLOAD_TYPE.riskTask) { | ||
| const task = await db.riskMitigationTask.findFirst({ | ||
| where: { | ||
| id: parsedInput.taskId, | ||
| }, | ||
| }); | ||
|
|
||
| if (!task) { | ||
| throw new Error("Task or file not found"); | ||
| } |
There was a problem hiding this comment.
Validate organization ownership for task.
Similar to evidence, confirm that the riskMitigationTask record belongs to the user's organization to prevent unauthorized access by guessing another task's ID.
const task = await db.riskMitigationTask.findFirst({
- where: {
- id: parsedInput.taskId,
- },
+ where: {
+ id: parsedInput.taskId,
+ organizationId: user.organizationId,
+ },
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (uploadType === UPLOAD_TYPE.riskTask) { | |
| const task = await db.riskMitigationTask.findFirst({ | |
| where: { | |
| id: parsedInput.taskId, | |
| }, | |
| }); | |
| if (!task) { | |
| throw new Error("Task or file not found"); | |
| } | |
| if (uploadType === UPLOAD_TYPE.riskTask) { | |
| const task = await db.riskMitigationTask.findFirst({ | |
| where: { | |
| id: parsedInput.taskId, | |
| organizationId: user.organizationId, | |
| }, | |
| }); | |
| if (!task) { | |
| throw new Error("Task or file not found"); | |
| } |
| @@ -0,0 +1,33 @@ | |||
| "ue client" | |||
There was a problem hiding this comment.
Fix typo in client directive.
There's a typo in the client directive that could cause issues with the framework correctly identifying this as a client component.
-"ue client"
+"use client"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "ue client" | |
| "use client" |
| setIsUploading(false); | ||
| } | ||
| }, | ||
| [evidenceId, onSuccess, toast], |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Missing dependencies in useCallback
The useCallback dependency array is missing uploadType and taskId, which are used inside the callback function. This could lead to stale closures if these values change.
-[evidenceId, onSuccess, toast],
+[evidenceId, taskId, uploadType, onSuccess, toast],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| [evidenceId, onSuccess, toast], | |
| [evidenceId, taskId, uploadType, onSuccess, toast], |
| interface FileUploadProps { | ||
| onFileSelect: (file: File) => void; | ||
| isUploading: boolean; | ||
| accept?: Record<string, string[]>; | ||
| maxSize?: number; | ||
| /** | ||
| * The variant of the file upload component | ||
| * @default "default" | ||
| */ | ||
| variant?: "default" | "card"; | ||
| /** | ||
| * The height of the card variant | ||
| * @default "h-[220px]" | ||
| */ | ||
| cardHeight?: string; | ||
| /** | ||
| * Optional click handler for the card variant | ||
| */ | ||
| onClick?: (e: React.MouseEvent) => void; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Unused onClick prop
The component accepts an onClick prop but never uses it in the rendered output. Either remove the prop or apply it to the relevant element.
interface FileUploadProps {
onFileSelect: (file: File) => void;
isUploading: boolean;
accept?: Record<string, string[]>;
maxSize?: number;
/**
* The variant of the file upload component
* @default "default"
*/
variant?: "default" | "card";
/**
* The height of the card variant
* @default "h-[220px]"
*/
cardHeight?: string;
- /**
- * Optional click handler for the card variant
- */
- onClick?: (e: React.MouseEvent) => void;
}Then remove it from the destructuring:
export function FileUpload({
onFileSelect,
isUploading,
accept = {
// ...
},
maxSize = 10 * 1024 * 1024, // 10MB
variant = "default",
cardHeight = "h-[220px]",
- onClick,
}: FileUploadProps) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| interface FileUploadProps { | |
| onFileSelect: (file: File) => void; | |
| isUploading: boolean; | |
| accept?: Record<string, string[]>; | |
| maxSize?: number; | |
| /** | |
| * The variant of the file upload component | |
| * @default "default" | |
| */ | |
| variant?: "default" | "card"; | |
| /** | |
| * The height of the card variant | |
| * @default "h-[220px]" | |
| */ | |
| cardHeight?: string; | |
| /** | |
| * Optional click handler for the card variant | |
| */ | |
| onClick?: (e: React.MouseEvent) => void; | |
| } | |
| interface FileUploadProps { | |
| onFileSelect: (file: File) => void; | |
| isUploading: boolean; | |
| accept?: Record<string, string[]>; | |
| maxSize?: number; | |
| /** | |
| * The variant of the file upload component | |
| * @default "default" | |
| */ | |
| variant?: "default" | "card"; | |
| /** | |
| * The height of the card variant | |
| * @default "h-[220px]" | |
| */ | |
| cardHeight?: string; | |
| } | |
| export function FileUpload({ | |
| onFileSelect, | |
| isUploading, | |
| accept = { | |
| // ... | |
| }, | |
| maxSize = 10 * 1024 * 1024, // 10MB | |
| variant = "default", | |
| cardHeight = "h-[220px]", | |
| }: FileUploadProps) { | |
| // Component implementation goes here... | |
| } |
…ranslations - Add translations for file upload, file card, and file URL components - Update locales/en.ts with detailed upload-related translation keys - Refactor components to use useI18n and useTranslations for internationalization - Remove hardcoded text in upload-related components - Enhance user experience with consistent and localized messaging
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
apps/app/src/components/upload/FileUrl.tsx (3)
43-50: Consider enhancing URL validationThe
getDomainNamefunction provides basic URL validation with a fallback, but doesn't distinguish between valid malformed URLs and completely invalid strings.Consider enhancing the validation:
const getDomainName = (url: string) => { try { const domain = new URL(url).hostname; return domain.replace(/^www\./, ""); } catch (e) { - return url; + // Check if it's potentially a URL without protocol + if (url.includes(".") && !url.includes(" ")) { + try { + const domainWithProtocol = new URL(`https://${url}`).hostname; + return domainWithProtocol.replace(/^www\./, ""); + } catch { + return url; + } + } + return url; } };
53-55: Add feedback for clipboard operationsThe
copyToClipboardfunction successfully copies to clipboard but doesn't provide user feedback when the operation completes.Consider adding some visual feedback:
const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text) + .then(() => { + // Could use a toast notification here + console.log("URL copied to clipboard"); + }) + .catch((err) => { + console.error("Failed to copy URL: ", err); + }); };
139-166: Add aria-label to improve accessibilityWhile the URL list items have good visual representation, they could be improved for screen reader accessibility.
<div key={url} className="group flex items-center gap-3 p-3 rounded-md border bg-card hover:bg-accent/10 transition-colors" + role="listitem" + aria-label={`URL link to ${domain}`} >apps/app/src/components/upload/FileUpload.tsx (3)
53-60: Add file validation feedbackThe
onDropcallback simply selects the first accepted file but doesn't provide feedback if a file is rejected. Consider adding custom validation messages.const onDrop = useCallback( (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { if (acceptedFiles.length > 0) { onFileSelect(acceptedFiles[0]); } + if (rejectedFiles.length > 0) { + // Handle rejected files - could display a toast notification + const error = rejectedFiles[0].errors[0]; + console.error(`File rejected: ${error.message}`); + } }, - [onFileSelect], + [onFileSelect], );Remember to update the type signature to include
FileRejection[]from react-dropzone.
68-74: Consider adding preventDropOnDocument optionTo improve user experience, consider adding the
preventDropOnDocumentoption to the dropzone configuration to avoid unintended file drops on the document.const { getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject, } = useDropzone({ onDrop, accept, maxSize, multiple: false, disabled: isUploading, + preventDropOnDocument: true, });
150-168: Consider adding file size in MB to localizationThe file size is correctly translated in the UI, but the unit "MB" is hardcoded. This should be part of the translation string.
<p className="mt-1"> - {t("maxFileSize", { size: Math.round(maxSize / 1024 / 1024) })} + {t("maxFileSizeWithUnit", { + size: Math.round(maxSize / 1024 / 1024), + unit: "MB" + })} </p>This will allow translators to format the size and unit according to locale conventions.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/app/languine.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
apps/app/src/components/upload-dialog.tsx(0 hunks)apps/app/src/components/upload/FileCard.tsx(1 hunks)apps/app/src/components/upload/FileIcon.tsx(1 hunks)apps/app/src/components/upload/FileSection.tsx(1 hunks)apps/app/src/components/upload/FileUpload.tsx(1 hunks)apps/app/src/components/upload/FileUrl.tsx(1 hunks)apps/app/src/components/uploaded-files-card.tsx(0 hunks)apps/app/src/locales/en.ts(1 hunks)apps/app/src/locales/es.ts(1 hunks)apps/app/src/locales/fr.ts(1 hunks)apps/app/src/locales/no.ts(1 hunks)apps/app/src/locales/pt.ts(1 hunks)
💤 Files with no reviewable changes (2)
- apps/app/src/components/upload-dialog.tsx
- apps/app/src/components/uploaded-files-card.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/app/src/locales/en.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/app/src/components/upload/FileIcon.tsx
- apps/app/src/components/upload/FileCard.tsx
- apps/app/src/components/upload/FileSection.tsx
🔇 Additional comments (9)
apps/app/src/locales/fr.ts (1)
803-841: Well-structured upload localization strings with proper pluralization handling.The new
uploadsection is well-organized with four logical subsections that cover different aspects of the file upload process:
fileSection: Displays counts of uploaded filesfileUpload: Provides prompts and status messages for the upload processfileCard: Handles file preview and managementfileUrl: Manages additional link functionalitiesThe pluralization is properly handled with the
{{s}}pattern for conditional plurals, and template variables like{{count}}and{{size}}are consistently used.apps/app/src/locales/pt.ts (1)
773-811: Consistent Portuguese translations matching the structure of other language files.The Portuguese translations maintain consistency with the structure established in the other language files. The section includes all necessary keys for file upload functionalities with appropriate Portuguese translations. The pluralization mechanism using
{{s}}and template variables like{{count}}and{{size}}are maintained correctly.One minor observation is that line 772 (the line preceding the new section) differs from other language files, which might indicate other differences in the structure of this file compared to the others.
apps/app/src/locales/es.ts (1)
803-841: Spanish translations correctly implemented following consistent structure.The Spanish translations follow the same well-organized structure as the other language files with the four key subsections:
fileSection: For file count displaysfileUpload: For upload interface textfileCard: For file preview and managementfileUrl: For link managementAll template variables (
{{count}},{{size}}, etc.) are properly preserved, and pluralization is handled consistently using the{{s}}pattern. The translations are appropriate for Spanish speakers.apps/app/src/locales/no.ts (1)
803-841: Norwegian translations properly implemented with consistent approach.The Norwegian translations maintain the same structure and functionality as the other language files, with all four subsections properly implemented. The translations are appropriate for Norwegian users and retain all necessary template variables.
The use of pluralization with
{{s}}is consistent with other language implementations, and the overall organization aligns perfectly with the established pattern, making maintenance and updates easier across all supported languages.apps/app/src/components/upload/FileUrl.tsx (2)
3-14: Clean and well-organized importsThe imports are well-structured with UI components grouped together and utility functions separated appropriately.
16-20: Clear interface definition with appropriate typesThe interface is well-defined with descriptive property names and appropriate types. The
onSuccesscallback returning a Promise is a good practice for async operations.apps/app/src/components/upload/FileUpload.tsx (3)
10-29: Remove unused onClick propThe component accepts an
onClickprop but never uses it in the rendered output. This was already identified in a previous review.interface FileUploadProps { onFileSelect: (file: File) => void; isUploading: boolean; accept?: Record<string, string[]>; maxSize?: number; /** * The variant of the file upload component * @default "default" */ variant?: "default" | "card"; /** * The height of the card variant * @default "h-[220px]" */ cardHeight?: string; - /** - * Optional click handler for the card variant - */ - onClick?: (e: React.MouseEvent) => void; }Then remove it from the destructuring:
export function FileUpload({ onFileSelect, isUploading, accept = { // ... }, maxSize = 10 * 1024 * 1024, // 10MB variant = "default", cardHeight = "h-[220px]", - onClick, }: FileUploadProps) {
34-45: Comprehensive file type supportThe component has excellent coverage of common file types with appropriate extensions, making it versatile for different use cases.
140-147: Good use of conditional classes with utility-first CSSThe component effectively uses the
cnutility to conditionally apply classes based on dropzone states, which results in clean, maintainable code.
| <Input | ||
| type="url" | ||
| placeholder={t("enterUrl")} | ||
| value={draft.url} | ||
| onChange={(e) => handleUpdateDraft(draft.id, e.target.value)} | ||
| className="flex-1" | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter" && draft.url.trim()) { | ||
| e.preventDefault(); | ||
| handleSaveUrls(); | ||
| } | ||
| }} | ||
| /> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Add validation before allowing Enter key submission
The component allows submitting URLs with Enter key without proper validation. The user could enter an invalid URL and it would be saved.
🏁 Script executed:
#!/bin/bash
# Check if validation is implemented in the useUrlManagement hook
rg -A 10 "useUrlManagement" --glob "*.{ts,tsx}"Length of output: 5986
Add Robust URL Validation Before Submission
The current code only checks for a non-empty URL using draft.url.trim() on the Enter key event. However, this doesn't guarantee that the URL is valid. Before triggering handleSaveUrls(), please implement proper URL validation (e.g., using the URL constructor inside a try-catch block or a regex-based approach). This will help prevent invalid URLs from being submitted.
- File:
apps/app/src/components/upload/FileUrl.tsx(Lines 90-102) - Ensure that the validation logic is either added directly in the onKeyDown event or centralized within the
useUrlManagementhook for consistency. - Provide immediate feedback to the user when an invalid URL is detected rather than proceeding with the submission.
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| size="icon" | ||
| variant="ghost" | ||
| className="h-8 w-8 rounded-full" | ||
| onClick={() => copyToClipboard(url)} | ||
| > | ||
| <Copy className="h-4 w-4" /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p>{t("copyLink")}</p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
|
|
||
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| size="icon" | ||
| variant="ghost" | ||
| asChild | ||
| className="h-8 w-8 rounded-full" | ||
| > | ||
| <a | ||
| href={url} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="flex items-center justify-center" | ||
| > | ||
| <ExternalLink className="h-4 w-4" /> | ||
| </a> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p>{t("openLink")}</p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
|
|
||
| <TooltipProvider> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| size="icon" | ||
| variant="ghost" | ||
| className="h-8 w-8 rounded-full hover:text-destructive" | ||
| onClick={() => handleUrlRemove(url)} | ||
| > | ||
| <Trash className="h-4 w-4" /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| <p>{t("deleteLink")}</p> | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| </TooltipProvider> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Enhance button accessibility with missing aria attributes
The action buttons (copy, open, delete) use icons without text labels. While they have tooltips, they should also have aria-labels for screen readers.
<Button
size="icon"
variant="ghost"
className="h-8 w-8 rounded-full"
onClick={() => copyToClipboard(url)}
+ aria-label={t("copyLink")}
>
<Copy className="h-4 w-4" />
</Button>Apply similar changes to the other icon-only buttons for "open link" and "delete link".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| className="h-8 w-8 rounded-full" | |
| onClick={() => copyToClipboard(url)} | |
| > | |
| <Copy className="h-4 w-4" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("copyLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| asChild | |
| className="h-8 w-8 rounded-full" | |
| > | |
| <a | |
| href={url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center justify-center" | |
| > | |
| <ExternalLink className="h-4 w-4" /> | |
| </a> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("openLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| className="h-8 w-8 rounded-full hover:text-destructive" | |
| onClick={() => handleUrlRemove(url)} | |
| > | |
| <Trash className="h-4 w-4" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("deleteLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| </div> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| className="h-8 w-8 rounded-full" | |
| onClick={() => copyToClipboard(url)} | |
| aria-label={t("copyLink")} | |
| > | |
| <Copy className="h-4 w-4" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("copyLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| asChild | |
| className="h-8 w-8 rounded-full" | |
| aria-label={t("openLink")} | |
| > | |
| <a | |
| href={url} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="flex items-center justify-center" | |
| > | |
| <ExternalLink className="h-4 w-4" /> | |
| </a> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("openLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| <TooltipProvider> | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <Button | |
| size="icon" | |
| variant="ghost" | |
| className="h-8 w-8 rounded-full hover:text-destructive" | |
| onClick={() => handleUrlRemove(url)} | |
| aria-label={t("deleteLink")} | |
| > | |
| <Trash className="h-4 w-4" /> | |
| </Button> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{t("deleteLink")}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| </TooltipProvider> | |
| </div> |
| <Card | ||
| className={cn( | ||
| "group cursor-pointer transition-all hover:shadow-md border-dashed border-2 border-primary/30 hover:border-primary flex flex-col overflow-hidden", | ||
| cardHeight, | ||
| isDragActive && "border-primary border-2 bg-primary/5", | ||
| isDragAccept && "border-green-500 border-2 bg-green-50", | ||
| isDragReject && "border-red-500 border-2 bg-red-50", | ||
| isUploading && "pointer-events-none opacity-60", | ||
| )} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Improve card variant feedback for screen readers
While the visual feedback for drag states is good, there's no equivalent feedback for screen readers.
<Card
className={cn(
"group cursor-pointer transition-all hover:shadow-md border-dashed border-2 border-primary/30 hover:border-primary flex flex-col overflow-hidden",
cardHeight,
isDragActive && "border-primary border-2 bg-primary/5",
isDragAccept && "border-green-500 border-2 bg-green-50",
isDragReject && "border-red-500 border-2 bg-red-50",
isUploading && "pointer-events-none opacity-60",
)}
+ aria-busy={isUploading}
+ aria-live="polite"
+ aria-label={
+ isUploading
+ ? t("uploadingText")
+ : isDragActive
+ ? t("dropFileHere")
+ : t("addFiles")
+ }
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Card | |
| className={cn( | |
| "group cursor-pointer transition-all hover:shadow-md border-dashed border-2 border-primary/30 hover:border-primary flex flex-col overflow-hidden", | |
| cardHeight, | |
| isDragActive && "border-primary border-2 bg-primary/5", | |
| isDragAccept && "border-green-500 border-2 bg-green-50", | |
| isDragReject && "border-red-500 border-2 bg-red-50", | |
| isUploading && "pointer-events-none opacity-60", | |
| )} | |
| <Card | |
| className={cn( | |
| "group cursor-pointer transition-all hover:shadow-md border-dashed border-2 border-primary/30 hover:border-primary flex flex-col overflow-hidden", | |
| cardHeight, | |
| isDragActive && "border-primary border-2 bg-primary/5", | |
| isDragAccept && "border-green-500 border-2 bg-green-50", | |
| isDragReject && "border-red-500 border-2 bg-red-50", | |
| isUploading && "pointer-events-none opacity-60", | |
| )} | |
| aria-busy={isUploading} | |
| aria-live="polite" | |
| aria-label={ | |
| isUploading | |
| ? t("uploadingText") | |
| : isDragActive | |
| ? t("dropFileHere") | |
| : t("addFiles") | |
| } | |
| > |
- Enhance security in file URL retrieval by adding organizationId checks - Update getFileUrl action to validate evidence and task access - Modify task attachments retrieval to handle empty attachment lists - Refactor file upload and delete hooks to include more dependencies - Update file upload translations with nested i18n keys
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
apps/app/src/hooks/upload/use-file-delete.ts (1)
25-60: Fix potential issues with empty string fallbacks.The implementation uses empty strings as fallbacks for
evidenceIdandtaskId. This might lead to API errors if the backend expects valid IDs when callingdeleteFile.Consider conditionally including these properties only when they have values:
const response = await deleteFile({ uploadType, - evidenceId: evidenceId || "", - taskId: taskId || "", + ...(evidenceId ? { evidenceId } : {}), + ...(taskId ? { taskId } : {}), fileUrl, });This approach only includes the properties when they have meaningful values, which might lead to clearer error messages if an ID is missing.
apps/app/src/actions/files/get-file-url.ts (3)
30-41: Consider more flexible S3 URL parsing.
Currently,extractS3KeyFromUrlrelies onamazonaws.combeing present in the URL. If you ever support custom S3 domains or CloudFront distributions, this approach may fail. Consider a more flexible mechanism or an optional config-driven approach for extracting the key.
76-135: Refactor duplicate S3 logic into a helper function.
BothUPLOAD_TYPE.evidenceandUPLOAD_TYPE.riskTaskshare nearly identical steps for retrieving signed URLs. Extracting these lines into a shared helper will enhance maintainability and reduce duplication.
96-98: Make signed URL expiration configurable.
A hardcodedexpiresIn: 3600(1 hour) may be suitable for now, but best practice is to keep such configurations in environment variables so they can be easily adjusted without code changes.Also applies to: 126-128
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/app/languine.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
apps/app/src/actions/files/get-file-url.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts(1 hunks)apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts(1 hunks)apps/app/src/components/upload/FileUpload.tsx(1 hunks)apps/app/src/hooks/upload/use-file-delete.ts(1 hunks)apps/app/src/hooks/upload/use-file-upload.ts(1 hunks)apps/app/src/locales/en.ts(1 hunks)apps/app/src/locales/es.ts(1 hunks)apps/app/src/locales/fr.ts(1 hunks)apps/app/src/locales/no.ts(1 hunks)apps/app/src/locales/pt.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/hooks/useTaskAttachments.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts
- apps/app/src/hooks/upload/use-file-upload.ts
- apps/app/src/locales/en.ts
- apps/app/src/locales/pt.ts
- apps/app/src/components/upload/FileUpload.tsx
🔇 Additional comments (16)
apps/app/src/hooks/upload/use-file-delete.ts (7)
1-7: Well-structured imports and client directive.The code properly uses the "use client" directive at the top, indicating it's meant for client-side execution in Next.js. The imports are well-organized and include all necessary dependencies.
8-15: Clear type definitions for the hook parameters.The type definitions are well-structured. Using a derived type from the
UPLOAD_TYPEenum ensures type safety when specifying upload types. The interface clearly documents the expected props for the hook.
17-24: Good hook signature with destructured parameters.The hook signature is clean and follows React conventions. Destructuring the parameters in the function signature improves readability.
35-41: Robust error handling for API responses.The code properly checks both for the existence of a response and for success status in the response, providing specific error messages when available. This ensures that users get helpful feedback when operations fail.
43-57: Well-implemented success and error feedback.The code correctly awaits the
onSuccesscallback before showing the success toast, ensuring UI updates complete before the notification appears. The error handling provides clear, user-friendly messages by extracting information from Error objects when available.
59-59: Correct dependency array in useCallback.The dependency array now correctly includes all required dependencies:
evidenceId,taskId,uploadType,onSuccess, andtoast. This ensures the callback is properly updated when these values change, avoiding stale closures.
62-65: Clean API design with a single responsibility.The hook follows the single responsibility principle by focusing solely on file deletion functionality. Returning an object with named methods makes the API clear and provides room for extension if needed in the future.
apps/app/src/actions/files/get-file-url.ts (1)
10-20: Environment variable checks are comprehensive.
Kudos for early validation of AWS credentials, region, and bucket name. This proactive check avoids potentially cryptic runtime failures if any variable is missing.apps/app/src/locales/fr.ts (3)
803-841: Well-structured localization for file upload functionality.The addition of this
uploadsection with comprehensive localization support for file upload features is well-organized and aligns with the PR objectives. The structure with four logical subsections (fileSection, fileUpload, fileCard, and fileUrl) provides a complete set of translations for all aspects of the file management system.
827-828: Consistent confirmation dialog text.The file deletion confirmation dialog text is clear and warns users about the irreversible nature of the action, which is a good practice for destructive operations.
805-806:Details
✅ Verification successful
Verify pluralization support in French.
The pluralization support using
{{count}} fichier{{s}} téléchargésyntax is implemented here. Ensure that the pluralization works correctly in French, as some languages have more complex pluralization rules.
🏁 Script executed:
#!/bin/bash # Search for other pluralization patterns in the French locale file to confirm consistency echo "Checking pluralization patterns in French locale file..." rg -A 1 "\{\{count\}\}" apps/app/src/locales/fr.tsLength of output: 373
Pluralization in the French locale file appears consistent and correct.
The grep search confirms that the{{count}} ...{{s}}pattern is applied uniformly (e.g., in bothfilesUploadedandlinksAdded). Since French typically uses only singular and plural forms, this approach is appropriate. However, please verify that your i18n library correctly interprets these patterns—especially ensuring that the singular form applies when the count is exactly 1, and the plural form for 0 or greater than 1—to cover edge cases if applicable.apps/app/src/locales/es.ts (2)
803-841: Complete Spanish localization for file upload functionality.The Spanish localization for the file upload section is comprehensive and well-structured, matching the same organization as the other language files.
805-806: Consistent pluralization handling across languages.The Spanish translation properly implements pluralization markers with
{{count}} archivo{{s}} subidoand{{count}} enlace{{s}} agregado. This maintains consistency with the pluralization approach used in other language files.Also applies to: 832-833
apps/app/src/locales/no.ts (3)
803-841: Complete Norwegian localization for file upload functionality.The Norwegian localization for the file upload section is well-implemented and follows the same structure as the other language files, ensuring a consistent user experience across different languages.
816-817: Check translation consistency between similar phrases.There are two similar phrases for dropping files:
"dragDropOrClick": "Dra og slipp eller klikk for å laste opp""dragDropOrClickToSelect": "Dra og slipp en fil her, eller klikk for å velge"While they serve slightly different purposes, the translation is nuanced correctly to reflect the distinction between uploading generally versus selecting a specific file.
803-841:Details
❓ Verification inconclusive
Verify Norwegian language pluralization.
Norwegian has different pluralization rules than English, French or Spanish. Ensure that the implementation of
{{count}} fil{{s}} lastet oppprovides the correct pluralization for Norwegian grammar.
🏁 Script executed:
#!/bin/bash # Check if pluralization is implemented consistently across language files echo "Comparing pluralization implementation across language files..." echo "Norwegian implementation:" rg -A 1 "filesUploaded" apps/app/src/locales/no.ts echo "French implementation:" rg -A 1 "filesUploaded" apps/app/src/locales/fr.ts echo "Spanish implementation:" rg -A 1 "filesUploaded" apps/app/src/locales/es.ts echo "English implementation (if exists):" rg -A 1 "filesUploaded" apps/app/src/locales/en.tsLength of output: 995
Action: Verify Correct Norwegian Pluralization
The current Norwegian translation uses the pattern:
"{{count}} fil{{s}} lastet opp"This format must be checked to ensure it produces the grammatically correct forms in Norwegian—namely, that for a count of 1 it renders as “1 fil lastet opp” and for a count greater than 1 it yields “2 filer lastet opp” (i.e. replacing
{{s}}with “er”, not “s”). Note that other locales (English, French, Spanish) correctly use an “s” suffix, which fits their pluralization rules, but Norwegian requires a different suffix.Please confirm that the underlying i18n interpolation/substitution mechanism is configured to provide “er” for Norwegian when needed and update the translation if it does not.
| const evidence = await db.organizationEvidence.findFirst({ | ||
| where: { | ||
| id: parsedInput.evidenceId, | ||
| organizationId: user.organizationId, | ||
| }, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Validate that the file belongs to the respective record.
Although you check organizationId and id ownership, there's no explicit step ensuring that the provided fileUrl corresponds to the stored file for that record. Attackers within the same organization could potentially retrieve any file by guessing or referencing another file's S3 key. Consider verifying the file’s URL in the database, for example:
where: {
id: parsedInput.evidenceId,
organizationId: user.organizationId,
+ fileUrl: parsedInput.fileUrl,
}Likewise for riskMitigationTask.
Also applies to: 108-114
…ed translations - Standardize translation keys across en, es, fr, no, and pt locales - Replace Handlebars-style interpolation with more concise format - Update file upload and file URL translation keys - Simplify translation strings for better readability and consistency
…mp into lewis/risk-register-uploads
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
apps/app/src/components/upload/FileCard.tsx (6)
58-60: Consider more robust file type detection.The current file type detection relies solely on file extension matching, which may not be fully reliable. File extensions can be misleading or manipulated.
Consider enhancing the file type detection with additional checks like MIME type validation when the preview URL is available. You could also extract this logic into a utility function for reuse elsewhere:
- const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(fileName); - const isPdf = /\.pdf$/i.test(fileName); + import { getFileType } from "@/lib/file-utils"; + + const { isImage, isPdf } = getFileType(fileName, previewState.url);
65-76: Optimize useEffect dependency array.The effect hook is designed to load the preview automatically on mount, but the extensive dependency array could cause unnecessary effect triggers.
Consider simplifying the effect to run only once on mount for the initial load, and then handle subsequent loads differently:
useEffect(() => { if (!hasLoadedPreview && !previewState.url && !previewState.isLoading) { onPreviewClick(url); setHasLoadedPreview(true); } -}, [ - hasLoadedPreview, - onPreviewClick, - previewState.isLoading, - previewState.url, - url, -]); +}, []); // Empty dependency array to run only on mountThis assumes the
urldoesn't change after component mount. If it can change, you may need a different approach.
86-137: Extract duplicated preview rendering logic.There's significant code duplication between the card preview and dialog preview rendering logic.
Consider extracting the preview rendering logic into separate components or functions to reduce duplication. For example:
+ const ImagePreview = ({ url, fileName, showMaximizeButton = false }: + { url: string, fileName: string, showMaximizeButton?: boolean }) => ( + <div className="relative w-full h-full"> + <Image + src={url} + alt={fileName} + fill + className="object-contain" + sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw" + /> + {showMaximizeButton && ( + <DialogTrigger asChild> + <Button + size="icon" + variant="ghost" + className="absolute bottom-1 right-1 h-6 w-6 bg-background/80 hover:bg-background" + > + <Maximize2 className="h-3 w-3" /> + </Button> + </DialogTrigger> + )} + </div> + ); + + const PdfPreview = ({ url, fileName, showMaximizeButton = false }: + { url: string, fileName: string, showMaximizeButton?: boolean }) => ( + <div className="relative w-full h-full"> + <iframe + src={url} + className="w-full h-full" + title={fileName} + /> + {showMaximizeButton && ( + <DialogTrigger asChild> + <Button + size="icon" + variant="ghost" + className="absolute bottom-1 right-1 h-6 w-6 bg-background/80 hover:bg-background" + > + <Maximize2 className="h-3 w-3" /> + </Button> + </DialogTrigger> + )} + </div> + );Then use these components in both the card and dialog sections.
203-216: Enhance external link behavior.The external link has good handling for when a preview URL is not available, but could be improved with loading state feedback.
Consider showing a loading state when the link is clicked and a preview is being loaded:
<a href={previewState.url || "#"} target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 flex items-center justify-center" onClick={(e) => { if (!previewState.url) { e.preventDefault(); + const button = e.currentTarget; + const originalContent = button.innerHTML; + button.innerHTML = '<span class="animate-spin">↻</span>'; onPreviewClick(url) + .then(() => { + window.open(previewState.url, '_blank', 'noopener,noreferrer'); + }) + .catch((error) => { + // Handle error + }) + .finally(() => { + button.innerHTML = originalContent; + }); } }} > - <ExternalLink className="h-4 w-4" /> + {previewState.isLoading && !previewState.url ? ( + <Loader2 className="h-4 w-4 animate-spin" /> + ) : ( + <ExternalLink className="h-4 w-4" /> + )} </a>
81-190: Improve accessibility for keyboard navigation.While the component has good accessibility features like alt text and titles, it could be improved for keyboard navigation, especially in the dialog.
Consider adding explicit focus management for the dialog to ensure keyboard users can easily navigate:
<Dialog open={isDialogOpen} onOpenChange={onOpenChange} + onOpenAutoFocus={(event) => { + // Focus the first interactive element in the dialog + const closeButton = document.querySelector('[data-dialog-close]'); + if (closeButton instanceof HTMLElement) { + event.preventDefault(); + closeButton.focus(); + } + }} > {/* Dialog content */} </Dialog>Additionally, consider adding keyboard shortcuts for common actions like closing the dialog with the Escape key (which might already be handled by the Dialog component) and navigating between files.
224-261: Enhance delete confirmation dialog to show file name.The delete confirmation dialog doesn't provide context about which file is being deleted.
Include the file name in the delete confirmation dialog to give users better context:
<AlertDialogTitle> - {t("upload.fileCard.deleteFileConfirmTitle")} + {t("upload.fileCard.deleteFileConfirmTitle", { fileName })} </AlertDialogTitle> <AlertDialogDescription> - {t("upload.fileCard.deleteFileConfirmDescription")} + {t("upload.fileCard.deleteFileConfirmDescription", { fileName })} </AlertDialogDescription>Ensure your translation strings support these parameters.
apps/app/src/locales/es.ts (1)
835-836: Ensure correct pluralization handling in Spanish.The current pluralization approach using
{{count}} archivo{{s}} subidomight work but could potentially cause grammatical issues in Spanish where the adjective "subido" also needs to be pluralized.Consider having separate strings for singular and plural forms to ensure grammatical correctness:
- filesUploaded: "{{count}} archivo{{s}} subido", - upload: "{{count}} archivo{{s}} subido", + filesUploaded: "{{count}} archivo subido|{{count}} archivos subidos", + upload: "{{count}} archivo subido|{{count}} archivos subidos",This depends on how your translation system handles pluralization - if it supports the pipe syntax above or requires completely separate keys for singular and plural forms.
apps/app/src/locales/no.ts (1)
808-819: Some translations appear redundant or very similar.There are several translations with very similar meanings that could potentially be consolidated:
dropFileHereanddropFileHereAlthave similar translationsdragDropOrClickanddragDropOrClickToSelectconvey similar conceptsuploadingTextanduploadingFileare nearly identicalThese might be intentionally different for various UI contexts, but it's worth reviewing if they could be consolidated.
Consider consolidating these similar translations if they're used in similar contexts to reduce maintenance overhead. For example:
fileUpload: { uploadingText: "Laster opp...", - dropFileHere: "Slipp fil her", - dropFileHereAlt: "Slipp filen her", + dropFileHere: "Slipp filen her", // Use a single consistent translation releaseToUpload: "Slipp for å laste opp", addFiles: "Legg til filer", uploadAdditionalEvidence: "Last opp ytterligere bevisfiler", dragDropOrClick: "Dra og slipp eller klikk for å laste opp", - dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge", maxFileSize: "Maks filstørrelse: {size}MB", - uploadingFile: "Laster opp fil..." },Please confirm if these similar translations are intentionally different based on their usage contexts.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts(1 hunks)apps/app/src/components/upload/FileCard.tsx(1 hunks)apps/app/src/locales/en.ts(1 hunks)apps/app/src/locales/es.ts(1 hunks)apps/app/src/locales/fr.ts(1 hunks)apps/app/src/locales/no.ts(1 hunks)apps/app/src/locales/pt.ts(1 hunks)apps/app/src/middleware.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts
- apps/app/src/locales/en.ts
🧰 Additional context used
🪛 Biome (1.9.4)
apps/app/src/locales/pt.ts
[error] 2-9: This property value named languages is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named languages
(lint/suspicious/noDuplicateObjectKeys)
[error] 9-14: This property value named language is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named language
(lint/suspicious/noDuplicateObjectKeys)
[error] 14-168: This property value named common is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named common
(lint/suspicious/noDuplicateObjectKeys)
[error] 168-183: This property value named header is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named header
(lint/suspicious/noDuplicateObjectKeys)
[error] 183-190: This property value named not_found is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named not_found
(lint/suspicious/noDuplicateObjectKeys)
[error] 190-196: This property value named theme is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named theme
(lint/suspicious/noDuplicateObjectKeys)
[error] 196-204: This property value named sidebar is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named sidebar
(lint/suspicious/noDuplicateObjectKeys)
[error] 204-226: This property value named auth is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named auth
(lint/suspicious/noDuplicateObjectKeys)
[error] 226-255: This property value named onboarding is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onboarding
(lint/suspicious/noDuplicateObjectKeys)
[error] 256-264: This property value named overview is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named overview
(lint/suspicious/noDuplicateObjectKeys)
[error] 264-333: This property value named policies is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named policies
(lint/suspicious/noDuplicateObjectKeys)
[error] 333-337: This property value named evidence_tasks is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named evidence_tasks
(lint/suspicious/noDuplicateObjectKeys)
[error] 337-528: This property value named risk is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named risk
(lint/suspicious/noDuplicateObjectKeys)
[error] 528-571: This property value named settings is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named settings
(lint/suspicious/noDuplicateObjectKeys)
[error] 571-578: This property value named user_menu is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named user_menu
(lint/suspicious/noDuplicateObjectKeys)
[error] 578-625: This property value named frameworks is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named frameworks
(lint/suspicious/noDuplicateObjectKeys)
[error] 625-697: This property value named vendor is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named vendor
(lint/suspicious/noDuplicateObjectKeys)
[error] 697-754: This property value named people is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named people
(lint/suspicious/noDuplicateObjectKeys)
[error] 755-759: This property value named errors is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named errors
(lint/suspicious/noDuplicateObjectKeys)
[error] 759-788: This property value named sub_pages is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named sub_pages
(lint/suspicious/noDuplicateObjectKeys)
[error] 788-810: This property value named editor is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named editor
(lint/suspicious/noDuplicateObjectKeys)
[error] 810-812: This property value named evidence is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named evidence
(lint/suspicious/noDuplicateObjectKeys)
[error] 812-861: This property value named upload is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named upload
(lint/suspicious/noDuplicateObjectKeys)
🔇 Additional comments (17)
apps/app/src/middleware.ts (8)
6-8: Locale configuration looks good.The configuration properly sets up internationalization with five supported languages and uses the appropriate URL mapping strategy.
12-14: Good implementation of the i18n middleware.The code now properly awaits the I18nMiddleware and stores the response for later modification, which is the correct pattern. This ensures that internationalization is properly applied while still allowing for additional middleware operations.
16-20: Correct locale path extraction logic.The implementation correctly extracts the locale from the URL path and creates a version without the locale prefix, which is necessary for proper routing decisions.
22-28: Authentication redirection logic is correctly implemented.This segment properly handles redirecting unauthenticated users to the auth page, which is an important security measure.
30-38: Onboarding check is well implemented.The conditional logic correctly ensures that authenticated users who haven't completed onboarding are redirected to the setup page, which maintains a proper user flow.
40-40: Good use of custom headers.Setting the
x-pathnameheader on the response is useful for debugging and tracking purposes. Since you're now using the response from the I18nMiddleware, this is the right place to set additional headers.
42-42: Correct response handling.Returning the modified response from the I18nMiddleware ensures that both internationalization and your custom middleware logic are applied.
46-48: Appropriate route matcher configuration.The matcher pattern correctly excludes API routes, Next.js internal routes, and other special paths that shouldn't go through this middleware.
apps/app/src/components/upload/FileCard.tsx (2)
48-55: LGTM: Well-structured component with clean prop interface.The component has a well-defined interface with appropriate props, making it reusable and maintainable.
34-37: LGTM: Effective state management interface.The
FilePreviewStateinterface provides a clean way to manage preview loading states.apps/app/src/locales/fr.ts (1)
842-884: New localization section for file uploads looks complete and well-structured.The newly added
uploadsection provides comprehensive French translations for the file upload functionality, covering all necessary UI elements including file status, upload instructions, preview options, and URL management.apps/app/src/locales/pt.ts (1)
802-843: New upload localization section properly implemented in Portuguese.The new
uploadsection contains well-structured Portuguese translations for all file upload functionality components, maintaining consistency with other language files.🧰 Tools
🪛 Biome (1.9.4)
[error] 810-812: This property value named evidence is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named evidence(lint/suspicious/noDuplicateObjectKeys)
apps/app/src/locales/es.ts (2)
833-873: Spanish translations for file upload features well implemented.The
uploadsection contains all necessary translations for file management features, aligning with the PR objective of implementing a comprehensive file upload system. The section includes appropriate Spanish wording for file uploads, previews, and additional URL management.
848-848: Ensure consistent placeholder format in maxFileSize string.The
maxFileSizetranslation uses single curly braces for the{size}placeholder, which may need to be consistent with how other placeholders are handled in your application.Confirm that the placeholder syntax in
maxFileSize: "Tamaño máximo de archivo: {size}MB"is consistent with your template engine's requirements. If your application uses double braces elsewhere, you might need to update this to{{size}}.apps/app/src/locales/no.ts (3)
804-807: Identical translations for different keys.There are two keys with identical translations in the
fileSection. This could be intentional if they're used in different contexts, but it's worth verifying.Could you confirm whether
filesUploadedanduploadare meant to have identical translations or if they're used in different contexts requiring different wording?
820-828: The file preview messaging is clear and well-structured.The translations for file preview and deletion actions are clear, comprehensive, and provide good user guidance. The confirmation message for file deletion appropriately warns users about the irreversible nature of the action.
829-840: URL management translations are complete and user-friendly.This section provides good coverage for all URL/link-related functionality including adding, editing, saving, copying, opening, and deleting links. The translations are concise and use appropriate Norwegian terminology.
| <div className="relative w-full h-[80vh]"> | ||
| {isImage ? ( | ||
| <div className="relative w-full h-full"> | ||
| <Image | ||
| src={previewState.url} | ||
| alt={fileName} | ||
| fill | ||
| className="object-contain" | ||
| sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw" | ||
| /> | ||
| </div> | ||
| ) : isPdf ? ( | ||
| <iframe | ||
| src={previewState.url} | ||
| className="w-full h-full" | ||
| title={fileName} | ||
| /> | ||
| ) : ( | ||
| <div className="flex items-center justify-center h-full"> | ||
| <p className="text-muted-foreground"> | ||
| {t("upload.fileCard.previewNotAvailable")} | ||
| </p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| <div className="flex items-center justify-center h-[80vh]"> | ||
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add error handling for preview failures.
The component doesn't handle cases where the file preview fails to load (e.g., network issues, invalid URL).
Add error state handling to improve user experience when previews fail to load:
export function FileCard({
url,
previewState,
isDialogOpen,
onOpenChange,
onPreviewClick,
onDelete,
}: FileCardProps) {
const t = useI18n();
+ const [previewError, setPreviewError] = useState<string | null>(null);
const fileName = url.split("/").pop() || url;
const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(fileName);
const isPdf = /\.pdf$/i.test(fileName);
// Load preview automatically when component mounts
const [hasLoadedPreview, setHasLoadedPreview] = useState(false);
useEffect(() => {
if (!hasLoadedPreview && !previewState.url && !previewState.isLoading) {
- onPreviewClick(url);
+ onPreviewClick(url).catch(error => {
+ setPreviewError(t("upload.fileCard.previewError"));
+ });
setHasLoadedPreview(true);
}
}, [
hasLoadedPreview,
onPreviewClick,
previewState.isLoading,
previewState.url,
url,
]);Then update the render logic to show error states:
{previewState.url ? (
<div className="relative w-full h-[80vh]">
{isImage ? (
// Image preview
) : isPdf ? (
// PDF preview
) : (
// Not available message
)}
</div>
+ ) : previewError ? (
+ <div className="flex flex-col items-center justify-center h-[80vh]">
+ <p className="text-destructive mb-2">{previewError}</p>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setPreviewError(null);
+ onPreviewClick(url).catch(error => {
+ setPreviewError(t("upload.fileCard.previewError"));
+ });
+ }}
+ >
+ {t("upload.fileCard.retry")}
+ </Button>
+ </div>
) : (
// Loading state
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="relative w-full h-[80vh]"> | |
| {isImage ? ( | |
| <div className="relative w-full h-full"> | |
| <Image | |
| src={previewState.url} | |
| alt={fileName} | |
| fill | |
| className="object-contain" | |
| sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw" | |
| /> | |
| </div> | |
| ) : isPdf ? ( | |
| <iframe | |
| src={previewState.url} | |
| className="w-full h-full" | |
| title={fileName} | |
| /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full"> | |
| <p className="text-muted-foreground"> | |
| {t("upload.fileCard.previewNotAvailable")} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="flex items-center justify-center h-[80vh]"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | |
| </div> | |
| )} | |
| import { useState, useEffect } from "react"; | |
| import { Image } from "some-image-library"; | |
| import { Loader2 } from "some-loader-library"; | |
| import { Button } from "some-button-library"; | |
| import { useI18n } from "some-i18n-hook"; | |
| export function FileCard({ | |
| url, | |
| previewState, | |
| isDialogOpen, | |
| onOpenChange, | |
| onPreviewClick, | |
| onDelete, | |
| }: FileCardProps) { | |
| const t = useI18n(); | |
| const [previewError, setPreviewError] = useState<string | null>(null); | |
| const fileName = url.split("/").pop() || url; | |
| const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(fileName); | |
| const isPdf = /\.pdf$/i.test(fileName); | |
| // Load preview automatically when component mounts | |
| const [hasLoadedPreview, setHasLoadedPreview] = useState(false); | |
| useEffect(() => { | |
| if (!hasLoadedPreview && !previewState.url && !previewState.isLoading) { | |
| onPreviewClick(url).catch(error => { | |
| setPreviewError(t("upload.fileCard.previewError")); | |
| }); | |
| setHasLoadedPreview(true); | |
| } | |
| }, [ | |
| hasLoadedPreview, | |
| onPreviewClick, | |
| previewState.isLoading, | |
| previewState.url, | |
| url, | |
| ]); | |
| return ( | |
| <> | |
| {previewState.url ? ( | |
| <div className="relative w-full h-[80vh]"> | |
| {isImage ? ( | |
| <div className="relative w-full h-full"> | |
| <Image | |
| src={previewState.url} | |
| alt={fileName} | |
| fill | |
| className="object-contain" | |
| sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 50vw" | |
| /> | |
| </div> | |
| ) : isPdf ? ( | |
| <iframe | |
| src={previewState.url} | |
| className="w-full h-full" | |
| title={fileName} | |
| /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full"> | |
| <p className="text-muted-foreground"> | |
| {t("upload.fileCard.previewNotAvailable")} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ) : previewError ? ( | |
| <div className="flex flex-col items-center justify-center h-[80vh]"> | |
| <p className="text-destructive mb-2">{previewError}</p> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| setPreviewError(null); | |
| onPreviewClick(url).catch(error => { | |
| setPreviewError(t("upload.fileCard.previewError")); | |
| }); | |
| }} | |
| > | |
| {t("upload.fileCard.retry")} | |
| </Button> | |
| </div> | |
| ) : ( | |
| <div className="flex items-center justify-center h-[80vh]"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| } |
| export default { | ||
| languages: { | ||
| es: "Espanhol", | ||
| fr: "Francês", | ||
| no: "Norueguês", | ||
| pt: "Português", | ||
| en: "Inglês" | ||
| }, | ||
| language: { | ||
| title: "Idiomas", | ||
| description: "Altere o idioma usado na interface do usuário.", | ||
| placeholder: "Selecione o idioma" | ||
| }, | ||
| common: { | ||
| actions: { | ||
| save: "Salvar", | ||
| edit: "Editar", | ||
| "delete": "Excluir", | ||
| cancel: "Cancelar", | ||
| clear: "Limpar", | ||
| create: "Criar", | ||
| send: "Enviar", | ||
| "return": "Retornar", | ||
| success: "Sucesso", | ||
| error: "Erro", | ||
| next: "Próximo", | ||
| complete: "Completo", | ||
| addNew: "Adicionar Novo" | ||
| }, | ||
| assignee: { | ||
| label: "Responsável", | ||
| placeholder: "Selecione o responsável" | ||
| }, | ||
| date: { | ||
| pick: "Escolher uma data", | ||
| due_date: "Data de Vencimento" | ||
| }, | ||
| status: { | ||
| open: "Aberto", | ||
| pending: "Pendente", | ||
| closed: "Fechado", | ||
| archived: "Arquivado", | ||
| compliant: "Conforme", | ||
| non_compliant: "Não Conforme", | ||
| not_started: "Não Iniciado", | ||
| in_progress: "Em Andamento", | ||
| published: "Publicado", | ||
| needs_review: "Necessita Revisão", | ||
| draft: "Rascunho", | ||
| not_assessed: "Não Avaliado", | ||
| assessed: "Avaliado", | ||
| active: "Ativo", | ||
| inactive: "Inativo", | ||
| title: "Status" | ||
| }, | ||
| filters: { | ||
| clear: "Limpar filtros", | ||
| search: "Pesquisar...", | ||
| status: "Status", | ||
| department: "Departamento", | ||
| owner: { | ||
| label: "Responsável", | ||
| placeholder: "Filtrar por responsável" | ||
| } | ||
| }, | ||
| table: { | ||
| title: "Título", | ||
| status: "Status", | ||
| assigned_to: "Atribuído A", | ||
| due_date: "Data de Vencimento", | ||
| last_updated: "Última Atualização", | ||
| no_results: "Nenhum resultado encontrado" | ||
| }, | ||
| empty_states: { | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| title_tasks: "Nenhuma tarefa encontrada", | ||
| title_risks: "Nenhum risco encontrado", | ||
| description: "Tente outra pesquisa ou ajuste os filtros", | ||
| description_filters: "Tente outra pesquisa ou ajuste os filtros", | ||
| description_no_tasks: "Crie uma tarefa para começar", | ||
| description_no_risks: "Crie um risco para começar" | ||
| }, | ||
| no_items: { | ||
| title: "Nenhum item encontrado", | ||
| description: "Tente ajustar sua pesquisa ou filtros" | ||
| } | ||
| }, | ||
| pagination: { | ||
| of: "de", | ||
| items_per_page: "Itens por página", | ||
| rows_per_page: "Linhas por página", | ||
| page_x_of_y: "Página {{current}} de {{total}}", | ||
| go_to_first_page: "Ir para a primeira página", | ||
| go_to_previous_page: "Ir para a página anterior", | ||
| go_to_next_page: "Ir para a próxima página", | ||
| go_to_last_page: "Ir para a última página" | ||
| }, | ||
| comments: { | ||
| title: "Comentários", | ||
| description: "Adicione um comentário usando o formulário abaixo.", | ||
| add: "Novo Comentário", | ||
| "new": "Novo Comentário", | ||
| save: "Salvar Comentário", | ||
| success: "Comentário adicionado com sucesso", | ||
| error: "Falha ao adicionar comentário", | ||
| placeholder: "Escreva seu comentário aqui...", | ||
| empty: { | ||
| title: "Nenhum comentário ainda", | ||
| description: "Seja o primeiro a adicionar um comentário" | ||
| } | ||
| }, | ||
| attachments: { | ||
| title: "Anexos", | ||
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | ||
| upload: "Enviar anexo", | ||
| upload_description: "Carregue um anexo ou adicione um link para um recurso externo.", | ||
| drop: "Solte os arquivos aqui", | ||
| drop_description: "Solte arquivos aqui ou clique para escolher arquivos do seu dispositivo.", | ||
| drop_files_description: "Os arquivos podem ter até ", | ||
| empty: { | ||
| title: "Nenhum anexo", | ||
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'." | ||
| }, | ||
| toasts: { | ||
| error: "Algo deu errado, por favor tente novamente.", | ||
| error_uploading_files: "Não é possível carregar mais de 1 arquivo por vez", | ||
| error_uploading_files_multiple: "Não é possível carregar mais de 10 arquivos", | ||
| error_no_files_selected: "Nenhum arquivo selecionado", | ||
| error_file_rejected: "Arquivo {file} foi rejeitado", | ||
| error_failed_to_upload_files: "Falha ao carregar arquivos", | ||
| error_failed_to_upload_files_multiple: "Falha ao carregar arquivos", | ||
| error_failed_to_upload_files_single: "Falha ao carregar arquivo", | ||
| success_uploading_files: "Arquivos carregados com sucesso", | ||
| success_uploading_files_multiple: "Arquivos carregados com sucesso", | ||
| success_uploading_files_single: "Arquivo carregado com sucesso", | ||
| success_uploading_files_target: "Arquivos carregados", | ||
| uploading_files: "Carregando {target}...", | ||
| remove_file: "Remover arquivo" | ||
| } | ||
| }, | ||
| notifications: { | ||
| inbox: "Caixa de entrada", | ||
| archive: "Arquivo", | ||
| archive_all: "Arquivar tudo", | ||
| no_notifications: "Sem novas notificações" | ||
| }, | ||
| edit: "Editar", | ||
| errors: { | ||
| unexpected_error: "Ocorreu um erro inesperado" | ||
| }, | ||
| description: "Descrição", | ||
| last_updated: "Última atualização", | ||
| frequency: { | ||
| daily: "Diário", | ||
| weekly: "Semanal", | ||
| monthly: "Mensal", | ||
| quarterly: "Trimestral", | ||
| yearly: "Anual" | ||
| } | ||
| }, | ||
| header: { | ||
| discord: { | ||
| button: "Junte-se a nós no Discord" | ||
| }, | ||
| feedback: { | ||
| button: "Feedback", | ||
| title: "Obrigado pelo seu feedback!", | ||
| description: "Voltaremos a você assim que possível", | ||
| placeholder: "Ideias para melhorar esta página ou problemas que você está enfrentando.", | ||
| success: "Obrigado pelo seu feedback!", | ||
| error: "Erro ao enviar feedback - tentar novamente?", | ||
| send: "Enviar Feedback" | ||
| } | ||
| }, | ||
| not_found: { | ||
| title: "404 - Página não encontrada", | ||
| description: "A página que você está procurando não existe.", | ||
| "return": "Retornar ao painel" | ||
| }, | ||
| theme: { | ||
| options: { | ||
| light: "Claro", | ||
| dark: "Escuro", | ||
| system: "Sistema" | ||
| } | ||
| }, | ||
| sidebar: { | ||
| overview: "Visão Geral", | ||
| policies: "Políticas", | ||
| risk: "Gestão de Riscos", | ||
| vendors: "Fornecedores", | ||
| integrations: "Integrações", | ||
| settings: "Configurações", | ||
| evidence: "Tarefas de Evidência", | ||
| people: "Pessoas" | ||
| }, | ||
| auth: { | ||
| title: "Automatize a conformidade com SOC 2, ISO 27001 e GDPR com IA.", | ||
| description: "Crie uma conta gratuita ou faça login com uma conta existente para continuar.", | ||
| options: "Mais opções", | ||
| google: "Continuar com o Google", | ||
| email: { | ||
| description: "Insira seu endereço de e-mail para continuar.", | ||
| placeholder: "Insira o endereço de e-mail", | ||
| button: "Continuar com e-mail", | ||
| magic_link_sent: "Link mágico enviado", | ||
| magic_link_description: "Verifique sua caixa de entrada para um link mágico.", | ||
| magic_link_try_again: "Tente novamente.", | ||
| success: "E-mail enviado - verifique sua caixa de entrada!", | ||
| error: "Erro ao enviar e-mail - tentar novamente?" | ||
| }, | ||
| terms: "Ao clicar em continuar, você reconhece que leu e concorda com os Termos de Serviço e a Política de Privacidade." | ||
| }, | ||
| onboarding: { | ||
| title: "Criar uma organização", | ||
| setup: "Configuração", | ||
| description: "Conte-nos um pouco sobre sua organização.", | ||
| fields: { | ||
| name: { | ||
| label: "Nome da Organização", | ||
| placeholder: "O nome da sua organização" | ||
| }, | ||
| website: { | ||
| label: "Website", | ||
| placeholder: "O website da sua organização" | ||
| }, | ||
| subdomain: { | ||
| label: "Subdomínio", | ||
| placeholder: "exemplo" | ||
| }, | ||
| fullName: { | ||
| label: "Seu Nome", | ||
| placeholder: "Seu nome completo" | ||
| } | ||
| }, | ||
| success: "Obrigado, tudo pronto!", | ||
| error: "Algo deu errado, por favor tente novamente.", | ||
| unavailable: "Indisponível", | ||
| check_availability: "Verificando disponibilidade", | ||
| available: "Disponível" | ||
| }, | ||
| overview: { | ||
| title: "Visão Geral", | ||
| framework_chart: { | ||
| title: "Progresso do Framework" | ||
| }, | ||
| requirement_chart: { | ||
| title: "Status de Conformidade" | ||
| } | ||
| }, | ||
| policies: { | ||
| dashboard: { | ||
| title: "Painel", | ||
| all: "Todas as Políticas", | ||
| policy_status: "Política por Status", | ||
| policies_by_assignee: "Políticas por Responsável", | ||
| policies_by_framework: "Políticas por Framework", | ||
| sub_pages: { | ||
| overview: "Visão Geral", | ||
| edit_policy: "Editar Política" | ||
| } | ||
| }, | ||
| table: { | ||
| name: "Nome da Política", | ||
| statuses: { | ||
| draft: "Rascunho", | ||
| published: "Publicado", | ||
| archived: "Arquivado" | ||
| }, | ||
| filters: { | ||
| owner: { | ||
| label: "Responsável", | ||
| placeholder: "Filtrar por responsável" | ||
| } | ||
| } | ||
| }, | ||
| filters: { | ||
| search: "Pesquisar políticas...", | ||
| all: "Todas as Políticas" | ||
| }, | ||
| status: { | ||
| draft: "Rascunho", | ||
| published: "Publicado", | ||
| needs_review: "Necessita Revisão", | ||
| archived: "Arquivado" | ||
| }, | ||
| policies: "políticas", | ||
| title: "Políticas", | ||
| create_new: "Criar nova política", | ||
| search_placeholder: "Pesquisar políticas...", | ||
| status_filter: "Filtrar por status", | ||
| all_statuses: "Todos os status", | ||
| no_policies_title: "Nenhuma política ainda", | ||
| no_policies_description: "Comece criando sua primeira política", | ||
| create_first: "Criar primeira política", | ||
| no_description: "Nenhuma descrição fornecida", | ||
| last_updated: "Última atualização: {{date}}", | ||
| save: "Salvar", | ||
| saving: "Salvando...", | ||
| saved_success: "Política salva com sucesso", | ||
| saved_error: "Falha ao salvar a política", | ||
| overview: { | ||
| title: "Visão Geral da Política", | ||
| form: { | ||
| update_policy: "Atualizar Política", | ||
| update_policy_description: "Atualize o título ou a descrição da política.", | ||
| update_policy_success: "Política atualizada com sucesso", | ||
| update_policy_error: "Falha ao atualizar a política", | ||
| update_policy_title: "Nome da Política", | ||
| review_frequency: "Frequência de Revisão", | ||
| review_frequency_placeholder: "Selecione uma frequência de revisão", | ||
| review_date: "Data da Revisão", | ||
| review_date_placeholder: "Selecione uma data de revisão" | ||
| } | ||
| } | ||
| }, | ||
| evidence_tasks: { | ||
| evidence_tasks: "Tarefas de Evidência", | ||
| overview: "Visão Geral" | ||
| }, | ||
| risk: { | ||
| risks: "riscos", | ||
| overview: "Visão Geral", | ||
| create: "Criar Novo Risco", | ||
| vendor: { | ||
| title: "Gestão de Fornecedores", | ||
| dashboard: { | ||
| title: "Painel de Fornecedores", | ||
| overview: "Visão Geral dos Fornecedores", | ||
| vendor_status: "Status do Fornecedor", | ||
| vendor_category: "Categorias de Fornecedor", | ||
| vendors_by_assignee: "Fornecedores por Responsável", | ||
| inherent_risk_description: "Nível de risco inicial antes da aplicação de quaisquer controles", | ||
| residual_risk_description: "Nível de risco restante após a aplicação de controles" | ||
| }, | ||
| register: { | ||
| title: "Registro de Fornecedor", | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Responsável" | ||
| } | ||
| }, | ||
| assessment: { | ||
| title: "Avaliação de Fornecedor", | ||
| update_success: "Avaliação de risco do fornecedor atualizada com sucesso", | ||
| update_error: "Falha ao atualizar a avaliação de risco do fornecedor", | ||
| inherent_risk: "Risco Inerente", | ||
| residual_risk: "Risco Residual" | ||
| }, | ||
| form: { | ||
| vendor_details: "Detalhes do Fornecedor", | ||
| vendor_name: "Nome", | ||
| vendor_name_placeholder: "Insira o nome do fornecedor", | ||
| vendor_website: "Website", | ||
| vendor_website_placeholder: "Insira o website do fornecedor", | ||
| vendor_description: "Descrição", | ||
| vendor_description_placeholder: "Insira a descrição do fornecedor", | ||
| vendor_category: "Categoria", | ||
| vendor_category_placeholder: "Selecione a categoria", | ||
| vendor_status: "Status", | ||
| vendor_status_placeholder: "Selecione o status", | ||
| create_vendor_success: "Fornecedor criado com sucesso", | ||
| create_vendor_error: "Falha ao criar fornecedor", | ||
| update_vendor: "Atualizar Fornecedor", | ||
| update_vendor_success: "Fornecedor atualizado com sucesso", | ||
| update_vendor_error: "Falha ao atualizar fornecedor", | ||
| add_comment: "Adicionar Comentário" | ||
| }, | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Proprietário" | ||
| }, | ||
| filters: { | ||
| search_placeholder: "Pesquisar fornecedores...", | ||
| status_placeholder: "Filtrar por status", | ||
| category_placeholder: "Filtrar por categoria", | ||
| owner_placeholder: "Filtrar por proprietário" | ||
| }, | ||
| empty_states: { | ||
| no_vendors: { | ||
| title: "Nenhum fornecedor ainda", | ||
| description: "Comece criando seu primeiro fornecedor" | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum fornecedor corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros" | ||
| } | ||
| }, | ||
| actions: { | ||
| create: "Criar Fornecedor" | ||
| }, | ||
| status: { | ||
| not_assessed: "Não Avaliado", | ||
| in_progress: "Em Andamento", | ||
| assessed: "Avaliado" | ||
| }, | ||
| category: { | ||
| cloud: "Nuvem", | ||
| infrastructure: "Infraestrutura", | ||
| software_as_a_service: "Software como Serviço", | ||
| finance: "Finanças", | ||
| marketing: "Marketing", | ||
| sales: "Vendas", | ||
| hr: "RH", | ||
| other: "Outro" | ||
| }, | ||
| risk_level: { | ||
| low: "Baixo Risco", | ||
| medium: "Risco Médio", | ||
| high: "Alto Risco", | ||
| unknown: "Risco Desconhecido" | ||
| } | ||
| }, | ||
| dashboard: { | ||
| title: "Painel", | ||
| overview: "Visão Geral do Risco", | ||
| risk_status: "Status do Risco", | ||
| risks_by_department: "Riscos por Departamento", | ||
| risks_by_assignee: "Riscos por Responsável", | ||
| inherent_risk_description: "O risco inerente é calculado como probabilidade * impacto. Calculado antes de qualquer controle ser aplicado.", | ||
| residual_risk_description: "O risco residual é calculado como probabilidade * impacto. Este é o nível de risco após a aplicação de controles.", | ||
| risk_assessment_description: "Compare os níveis de risco inerente e residual" | ||
| }, | ||
| register: { | ||
| title: "Registro de Risco", | ||
| table: { | ||
| risk: "Risco" | ||
| }, | ||
| empty: { | ||
| no_risks: { | ||
| title: "Crie um risco para começar", | ||
| description: "Acompanhe e classifique riscos, crie e atribua tarefas de mitigação para sua equipe e gerencie seu registro de risco tudo em uma interface simples." | ||
| }, | ||
| create_risk: "Criar um risco" | ||
| } | ||
| }, | ||
| metrics: { | ||
| probability: "Probabilidade", | ||
| impact: "Impacto", | ||
| inherentRisk: "Risco Inerente", | ||
| residualRisk: "Risco Residual" | ||
| }, | ||
| form: { | ||
| update_inherent_risk: "Salvar Risco Inerente", | ||
| update_inherent_risk_description: "Atualize o risco inerente do risco. Este é o nível de risco antes de qualquer controle ser aplicado.", | ||
| update_inherent_risk_success: "Risco inerente atualizado com sucesso", | ||
| update_inherent_risk_error: "Falha ao atualizar risco inerente", | ||
| update_residual_risk: "Salvar Risco Residual", | ||
| update_residual_risk_description: "Atualize o risco residual do risco. Este é o nível de risco após a aplicação de controles.", | ||
| update_residual_risk_success: "Risco residual atualizado com sucesso", | ||
| update_residual_risk_error: "Falha ao atualizar risco residual", | ||
| update_risk: "Atualizar Risco", | ||
| update_risk_description: "Atualize o título ou a descrição do risco.", | ||
| update_risk_success: "Risco atualizado com sucesso", | ||
| update_risk_error: "Falha ao atualizar risco", | ||
| create_risk_success: "Risco criado com sucesso", | ||
| create_risk_error: "Falha ao criar risco", | ||
| risk_details: "Detalhes do Risco", | ||
| risk_title: "Título do Risco", | ||
| risk_title_description: "Digite um nome para o risco", | ||
| risk_description: "Descrição", | ||
| risk_description_description: "Digite uma descrição para o risco", | ||
| risk_category: "Categoria", | ||
| risk_category_placeholder: "Selecione uma categoria", | ||
| risk_department: "Departamento", | ||
| risk_department_placeholder: "Selecione um departamento", | ||
| risk_status: "Status do Risco", | ||
| risk_status_placeholder: "Selecione um status de risco" | ||
| }, | ||
| tasks: { | ||
| title: "Tarefas", | ||
| attachments: "Anexos", | ||
| overview: "Visão Geral da Tarefa", | ||
| form: { | ||
| title: "Detalhes da Tarefa", | ||
| task_title: "Título da Tarefa", | ||
| status: "Status da Tarefa", | ||
| status_placeholder: "Selecione um status de tarefa", | ||
| task_title_description: "Digite um nome para a tarefa", | ||
| description: "Descrição", | ||
| description_description: "Digite uma descrição para a tarefa", | ||
| due_date: "Data de Vencimento", | ||
| due_date_description: "Selecione a data de vencimento para a tarefa", | ||
| success: "Tarefa criada com sucesso", | ||
| error: "Falha ao criar tarefa" | ||
| }, | ||
| sheet: { | ||
| title: "Criar Tarefa", | ||
| update: "Atualizar Tarefa", | ||
| update_description: "Atualize o título ou a descrição da tarefa." | ||
| }, | ||
| empty: { | ||
| description_create: "Crie uma tarefa de mitigação para este risco, adicione um plano de tratamento e atribua a um membro da equipe." | ||
| } | ||
| } | ||
| }, | ||
| settings: { | ||
| general: { | ||
| title: "Geral", | ||
| org_name: "Nome da Organização", | ||
| org_name_description: "Este é o nome visível da sua organização. Você deve usar o nome legal da sua organização.", | ||
| org_name_tip: "Por favor, use no máximo 32 caracteres.", | ||
| org_website: "Website da Organização", | ||
| org_website_description: "Este é o URL do website oficial da sua organização. Certifique-se de incluir o URL completo com https://.", | ||
| org_website_tip: "Por favor, insira um URL válido incluindo https://", | ||
| org_website_error: "Erro ao atualizar o website da organização", | ||
| org_website_updated: "Website da organização atualizado", | ||
| org_delete: "Excluir organização", | ||
| org_delete_description: "Remova permanentemente sua organização e todo o seu conteúdo da plataforma Comp AI. Esta ação não é reversível - continue com cautela.", | ||
| org_delete_alert_title: "Você tem certeza absoluta?", | ||
| org_delete_alert_description: "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua organização e removerá seus dados de nossos servidores.", | ||
| org_delete_error: "Erro ao excluir organização", | ||
| org_delete_success: "Organização excluída", | ||
| org_name_updated: "Nome da organização atualizado", | ||
| org_name_error: "Erro ao atualizar o nome da organização", | ||
| save_button: "Salvar", | ||
| delete_button: "Excluir", | ||
| delete_confirm: "EXCLUIR", | ||
| delete_confirm_tip: "Digite EXCLUIR para confirmar.", | ||
| cancel_button: "Cancelar" | ||
| }, | ||
| members: { | ||
| title: "Membros" | ||
| }, | ||
| billing: { | ||
| title: "Faturamento" | ||
| } | ||
| }, | ||
| user_menu: { | ||
| theme: "Tema", | ||
| language: "Idioma", | ||
| sign_out: "Sair", | ||
| account: "Conta", | ||
| support: "Suporte", | ||
| settings: "Configurações", | ||
| teams: "Equipes" | ||
| }, | ||
| frameworks: { | ||
| title: "Estruturas", | ||
| controls: { | ||
| title: "Controles", | ||
| description: "Revise e gerencie controles de conformidade", | ||
| table: { | ||
| status: "Status", | ||
| control: "Controle", | ||
| artifacts: "Artefatos", | ||
| actions: "Ações" | ||
| }, | ||
| statuses: { | ||
| not_started: "Não Iniciado", | ||
| completed: "Concluído", | ||
| in_progress: "Em Andamento" | ||
| } | ||
| }, | ||
| overview: { | ||
| error: "Falha ao carregar frameworks", | ||
| loading: "Carregando frameworks...", | ||
| empty: { | ||
| title: "Nenhum framework selecionado", | ||
| description: "Selecione frameworks para começar sua jornada de conformidade" | ||
| }, | ||
| progress: { | ||
| title: "Progresso do Framework", | ||
| empty: { | ||
| title: "Nenhum framework ainda", | ||
| description: "Comece adicionando um framework de conformidade para acompanhar seu progresso", | ||
| action: "Adicionar Framework" | ||
| } | ||
| }, | ||
| grid: { | ||
| welcome: { | ||
| title: "Bem-vindo ao Comp AI", | ||
| description: "Comece selecionando os frameworks de conformidade que você gostaria de implementar. Nós o ajudaremos a gerenciar e acompanhar sua jornada de conformidade em múltiplos padrões.", | ||
| action: "Começar" | ||
| }, | ||
| title: "Selecionar Frameworks", | ||
| version: "Versão", | ||
| actions: { | ||
| clear: "Limpar", | ||
| confirm: "Confirmar Seleção" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| vendor: { | ||
| title: "Painel", | ||
| register_title: "Gerenciamento de Fornecedores", | ||
| dashboard: { | ||
| title: "Painel", | ||
| overview: "Visão Geral do Fornecedor", | ||
| vendor_status: "Status do Fornecedor", | ||
| vendor_category: "Categorias de Fornecedor", | ||
| vendors_by_assignee: "Fornecedores por Responsável", | ||
| inherent_risk_description: "Nível de risco inicial antes que quaisquer controles sejam aplicados", | ||
| residual_risk_description: "Nível de risco restante após a aplicação de controles" | ||
| }, | ||
| register: { | ||
| title: "Registro de Fornecedor", | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Proprietário" | ||
| } | ||
| }, | ||
| category: { | ||
| cloud: "Nuvem", | ||
| infrastructure: "Infraestrutura", | ||
| software_as_a_service: "SaaS", | ||
| finance: "Finanças", | ||
| marketing: "Marketing", | ||
| sales: "Vendas", | ||
| hr: "RH", | ||
| other: "Outro" | ||
| }, | ||
| vendors: "fornecedores", | ||
| form: { | ||
| vendor_details: "Detalhes do Fornecedor", | ||
| vendor_name: "Nome", | ||
| vendor_name_placeholder: "Digite o nome do fornecedor", | ||
| vendor_website: "Website", | ||
| vendor_website_placeholder: "Digite o website do fornecedor", | ||
| vendor_description: "Descrição", | ||
| vendor_description_placeholder: "Digite a descrição do fornecedor", | ||
| vendor_category: "Categoria", | ||
| vendor_category_placeholder: "Selecione a categoria", | ||
| vendor_status: "Status", | ||
| vendor_status_placeholder: "Selecione o status", | ||
| create_vendor_success: "Fornecedor criado com sucesso", | ||
| create_vendor_error: "Falha ao criar fornecedor", | ||
| update_vendor_success: "Fornecedor atualizado com sucesso", | ||
| update_vendor_error: "Falha ao atualizar fornecedor", | ||
| contacts: "Contatos do Fornecedor", | ||
| contact_name: "Nome do Contato", | ||
| contact_email: "Email do Contato", | ||
| contact_role: "Função do Contato", | ||
| add_contact: "Adicionar Contato", | ||
| new_contact: "Novo Contato", | ||
| min_one_contact_required: "Um fornecedor deve ter pelo menos um contato" | ||
| }, | ||
| empty_states: { | ||
| no_vendors: { | ||
| title: "Nenhum fornecedor ainda", | ||
| description: "Comece criando seu primeiro fornecedor" | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum fornecedor corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros" | ||
| } | ||
| } | ||
| }, | ||
| people: { | ||
| title: "Pessoas", | ||
| details: { | ||
| taskProgress: "Progresso da Tarefa", | ||
| tasks: "Tarefas", | ||
| noTasks: "Nenhuma tarefa atribuída ainda" | ||
| }, | ||
| description: "Gerencie os membros da sua equipe e seus papéis.", | ||
| filters: { | ||
| search: "Pesquisar pessoas...", | ||
| role: "Filtrar por papel" | ||
| }, | ||
| actions: { | ||
| invite: "Adicionar Funcionário", | ||
| clear: "Limpar filtros" | ||
| }, | ||
| table: { | ||
| name: "Nome", | ||
| email: "Email", | ||
| department: "Departamento", | ||
| externalId: "ID Externo" | ||
| }, | ||
| empty: { | ||
| no_employees: { | ||
| title: "Nenhum funcionário ainda", | ||
| description: "Comece convidando seu primeiro membro da equipe." | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum funcionário corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros" | ||
| } | ||
| }, | ||
| invite: { | ||
| title: "Adicionar Funcionário", | ||
| description: "Adicione um funcionário à sua organização.", | ||
| email: { | ||
| label: "Endereço de email", | ||
| placeholder: "Digite o endereço de email" | ||
| }, | ||
| role: { | ||
| label: "Função", | ||
| placeholder: "Selecione uma função" | ||
| }, | ||
| name: { | ||
| label: "Nome", | ||
| placeholder: "Digite o nome" | ||
| }, | ||
| department: { | ||
| label: "Departamento", | ||
| placeholder: "Selecione um departamento" | ||
| }, | ||
| submit: "Adicionar Funcionário", | ||
| success: "Funcionário adicionado com sucesso", | ||
| error: "Falha ao adicionar funcionário" | ||
| } | ||
| }, | ||
| errors: { | ||
| unexpected: "Algo deu errado, por favor tente novamente" | ||
| }, | ||
| sub_pages: { | ||
| risk: { | ||
| overview: "Gestão de Riscos", | ||
| register: "Registro de Riscos", | ||
| risk_overview: "Visão Geral dos Riscos", | ||
| risk_comments: "Comentários sobre Riscos", | ||
| tasks: { | ||
| task_overview: "Visão Geral da Tarefa" | ||
| } | ||
| }, | ||
| policies: { | ||
| all: "Todas as Políticas", | ||
| editor: "Editor de Políticas", | ||
| policy_details: "Detalhes da política" | ||
| }, | ||
| people: { | ||
| all: "Pessoas", | ||
| employee_details: "Detalhes do Funcionário" | ||
| }, | ||
| settings: { | ||
| members: "Membros da Equipe" | ||
| }, | ||
| frameworks: { | ||
| overview: "Estruturas" | ||
| }, | ||
| evidence: { | ||
| title: "Evidência", | ||
| list: "Lista de Evidências", | ||
| overview: "Visão Geral das Evidências" | ||
| } | ||
| }, | ||
| editor: { | ||
| ai: { | ||
| thinking: "A IA está pensando", | ||
| thinking_spinner: "A IA está pensando", | ||
| edit_or_generate: "Editar ou gerar...", | ||
| tell_ai_what_to_do_next: "Diga à IA o que fazer a seguir", | ||
| request_limit_reached: "Você atingiu seu limite de solicitações para o dia." | ||
| }, | ||
| ai_selector: { | ||
| improve: "Melhorar a escrita", | ||
| fix: "Corrigir gramática", | ||
| shorter: "Tornar mais curto", | ||
| longer: "Tornar mais longo", | ||
| "continue": "Continuar escrevendo", | ||
| replace: "Substituir seleção", | ||
| insert: "Inserir abaixo", | ||
| discard: "Descartar" | ||
| } | ||
| }, | ||
| evidence: { | ||
| title: "Tarefas de Evidência", | ||
| list: "Todas as Evidências" | ||
| } | ||
| languages: { | ||
| es: "Espanhol", | ||
| fr: "Francês", | ||
| no: "Norueguês", | ||
| pt: "Português", | ||
| en: "Inglês", | ||
| }, | ||
| language: { | ||
| title: "Idiomas", | ||
| description: "Altere o idioma usado na interface do usuário.", | ||
| placeholder: "Selecione o idioma", | ||
| }, | ||
| common: { | ||
| actions: { | ||
| save: "Salvar", | ||
| edit: "Editar", | ||
| delete: "Excluir", | ||
| cancel: "Cancelar", | ||
| clear: "Limpar", | ||
| create: "Criar", | ||
| send: "Enviar", | ||
| return: "Retornar", | ||
| success: "Sucesso", | ||
| error: "Erro", | ||
| next: "Próximo", | ||
| complete: "Completo", | ||
| addNew: "Adicionar Novo", | ||
| }, | ||
| assignee: { | ||
| label: "Responsável", | ||
| placeholder: "Selecione o responsável", | ||
| }, | ||
| date: { | ||
| pick: "Escolher uma data", | ||
| due_date: "Data de Vencimento", | ||
| }, | ||
| status: { | ||
| open: "Aberto", | ||
| pending: "Pendente", | ||
| closed: "Fechado", | ||
| archived: "Arquivado", | ||
| compliant: "Conforme", | ||
| non_compliant: "Não Conforme", | ||
| not_started: "Não Iniciado", | ||
| in_progress: "Em Andamento", | ||
| published: "Publicado", | ||
| needs_review: "Necessita Revisão", | ||
| draft: "Rascunho", | ||
| not_assessed: "Não Avaliado", | ||
| assessed: "Avaliado", | ||
| active: "Ativo", | ||
| inactive: "Inativo", | ||
| title: "Status", | ||
| }, | ||
| filters: { | ||
| clear: "Limpar filtros", | ||
| search: "Pesquisar...", | ||
| status: "Status", | ||
| department: "Departamento", | ||
| owner: { | ||
| label: "Responsável", | ||
| placeholder: "Filtrar por responsável", | ||
| }, | ||
| }, | ||
| table: { | ||
| title: "Título", | ||
| status: "Status", | ||
| assigned_to: "Atribuído A", | ||
| due_date: "Data de Vencimento", | ||
| last_updated: "Última Atualização", | ||
| no_results: "Nenhum resultado encontrado", | ||
| }, | ||
| empty_states: { | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| title_tasks: "Nenhuma tarefa encontrada", | ||
| title_risks: "Nenhum risco encontrado", | ||
| description: "Tente outra pesquisa ou ajuste os filtros", | ||
| description_filters: "Tente outra pesquisa ou ajuste os filtros", | ||
| description_no_tasks: "Crie uma tarefa para começar", | ||
| description_no_risks: "Crie um risco para começar", | ||
| }, | ||
| no_items: { | ||
| title: "Nenhum item encontrado", | ||
| description: "Tente ajustar sua pesquisa ou filtros", | ||
| }, | ||
| }, | ||
| pagination: { | ||
| of: "de", | ||
| items_per_page: "Itens por página", | ||
| rows_per_page: "Linhas por página", | ||
| page_x_of_y: "Página {{current}} de {{total}}", | ||
| go_to_first_page: "Ir para a primeira página", | ||
| go_to_previous_page: "Ir para a página anterior", | ||
| go_to_next_page: "Ir para a próxima página", | ||
| go_to_last_page: "Ir para a última página", | ||
| }, | ||
| comments: { | ||
| title: "Comentários", | ||
| description: "Adicione um comentário usando o formulário abaixo.", | ||
| add: "Novo Comentário", | ||
| new: "Novo Comentário", | ||
| save: "Salvar Comentário", | ||
| success: "Comentário adicionado com sucesso", | ||
| error: "Falha ao adicionar comentário", | ||
| placeholder: "Escreva seu comentário aqui...", | ||
| empty: { | ||
| title: "Nenhum comentário ainda", | ||
| description: "Seja o primeiro a adicionar um comentário", | ||
| }, | ||
| }, | ||
| attachments: { | ||
| title: "Anexos", | ||
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | ||
| upload: "Enviar anexo", | ||
| upload_description: | ||
| "Carregue um anexo ou adicione um link para um recurso externo.", | ||
| drop: "Solte os arquivos aqui", | ||
| drop_description: | ||
| "Solte arquivos aqui ou clique para escolher arquivos do seu dispositivo.", | ||
| drop_files_description: "Os arquivos podem ter até ", | ||
| empty: { | ||
| title: "Nenhum anexo", | ||
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | ||
| }, | ||
| toasts: { | ||
| error: "Algo deu errado, por favor tente novamente.", | ||
| error_uploading_files: | ||
| "Não é possível carregar mais de 1 arquivo por vez", | ||
| error_uploading_files_multiple: | ||
| "Não é possível carregar mais de 10 arquivos", | ||
| error_no_files_selected: "Nenhum arquivo selecionado", | ||
| error_file_rejected: "Arquivo {file} foi rejeitado", | ||
| error_failed_to_upload_files: "Falha ao carregar arquivos", | ||
| error_failed_to_upload_files_multiple: "Falha ao carregar arquivos", | ||
| error_failed_to_upload_files_single: "Falha ao carregar arquivo", | ||
| success_uploading_files: "Arquivos carregados com sucesso", | ||
| success_uploading_files_multiple: "Arquivos carregados com sucesso", | ||
| success_uploading_files_single: "Arquivo carregado com sucesso", | ||
| success_uploading_files_target: "Arquivos carregados", | ||
| uploading_files: "Carregando {target}...", | ||
| remove_file: "Remover arquivo", | ||
| }, | ||
| }, | ||
| notifications: { | ||
| inbox: "Caixa de entrada", | ||
| archive: "Arquivo", | ||
| archive_all: "Arquivar tudo", | ||
| no_notifications: "Sem novas notificações", | ||
| }, | ||
| edit: "Editar", | ||
| errors: { | ||
| unexpected_error: "Ocorreu um erro inesperado", | ||
| }, | ||
| description: "Descrição", | ||
| last_updated: "Última atualização", | ||
| frequency: { | ||
| daily: "Diário", | ||
| weekly: "Semanal", | ||
| monthly: "Mensal", | ||
| quarterly: "Trimestral", | ||
| yearly: "Anual", | ||
| }, | ||
| }, | ||
| header: { | ||
| discord: { | ||
| button: "Junte-se a nós no Discord", | ||
| }, | ||
| feedback: { | ||
| button: "Feedback", | ||
| title: "Obrigado pelo seu feedback!", | ||
| description: "Voltaremos a você assim que possível", | ||
| placeholder: | ||
| "Ideias para melhorar esta página ou problemas que você está enfrentando.", | ||
| success: "Obrigado pelo seu feedback!", | ||
| error: "Erro ao enviar feedback - tentar novamente?", | ||
| send: "Enviar Feedback", | ||
| }, | ||
| }, | ||
| not_found: { | ||
| title: "404 - Página não encontrada", | ||
| description: "A página que você está procurando não existe.", | ||
| return: "Retornar ao painel", | ||
| }, | ||
| theme: { | ||
| options: { | ||
| light: "Claro", | ||
| dark: "Escuro", | ||
| system: "Sistema", | ||
| }, | ||
| }, | ||
| sidebar: { | ||
| overview: "Visão Geral", | ||
| policies: "Políticas", | ||
| risk: "Gestão de Riscos", | ||
| vendors: "Fornecedores", | ||
| integrations: "Integrações", | ||
| settings: "Configurações", | ||
| evidence: "Tarefas de Evidência", | ||
| people: "Pessoas", | ||
| }, | ||
| auth: { | ||
| title: "Automatize a conformidade com SOC 2, ISO 27001 e GDPR com IA.", | ||
| description: | ||
| "Crie uma conta gratuita ou faça login com uma conta existente para continuar.", | ||
| options: "Mais opções", | ||
| google: "Continuar com o Google", | ||
| email: { | ||
| description: "Insira seu endereço de e-mail para continuar.", | ||
| placeholder: "Insira o endereço de e-mail", | ||
| button: "Continuar com e-mail", | ||
| magic_link_sent: "Link mágico enviado", | ||
| magic_link_description: | ||
| "Verifique sua caixa de entrada para um link mágico.", | ||
| magic_link_try_again: "Tente novamente.", | ||
| success: "E-mail enviado - verifique sua caixa de entrada!", | ||
| error: "Erro ao enviar e-mail - tentar novamente?", | ||
| }, | ||
| terms: | ||
| "Ao clicar em continuar, você reconhece que leu e concorda com os Termos de Serviço e a Política de Privacidade.", | ||
| }, | ||
| onboarding: { | ||
| title: "Criar uma organização", | ||
| setup: "Configuração", | ||
| description: "Conte-nos um pouco sobre sua organização.", | ||
| fields: { | ||
| name: { | ||
| label: "Nome da Organização", | ||
| placeholder: "O nome da sua organização", | ||
| }, | ||
| website: { | ||
| label: "Website", | ||
| placeholder: "O website da sua organização", | ||
| }, | ||
| subdomain: { | ||
| label: "Subdomínio", | ||
| placeholder: "exemplo", | ||
| }, | ||
| fullName: { | ||
| label: "Seu Nome", | ||
| placeholder: "Seu nome completo", | ||
| }, | ||
| }, | ||
| success: "Obrigado, tudo pronto!", | ||
| error: "Algo deu errado, por favor tente novamente.", | ||
| unavailable: "Indisponível", | ||
| check_availability: "Verificando disponibilidade", | ||
| available: "Disponível", | ||
| }, | ||
| overview: { | ||
| title: "Visão Geral", | ||
| framework_chart: { | ||
| title: "Progresso do Framework", | ||
| }, | ||
| requirement_chart: { | ||
| title: "Status de Conformidade", | ||
| }, | ||
| }, | ||
| policies: { | ||
| dashboard: { | ||
| title: "Painel", | ||
| all: "Todas as Políticas", | ||
| policy_status: "Política por Status", | ||
| policies_by_assignee: "Políticas por Responsável", | ||
| policies_by_framework: "Políticas por Framework", | ||
| sub_pages: { | ||
| overview: "Visão Geral", | ||
| edit_policy: "Editar Política", | ||
| }, | ||
| }, | ||
| table: { | ||
| name: "Nome da Política", | ||
| statuses: { | ||
| draft: "Rascunho", | ||
| published: "Publicado", | ||
| archived: "Arquivado", | ||
| }, | ||
| filters: { | ||
| owner: { | ||
| label: "Responsável", | ||
| placeholder: "Filtrar por responsável", | ||
| }, | ||
| }, | ||
| }, | ||
| filters: { | ||
| search: "Pesquisar políticas...", | ||
| all: "Todas as Políticas", | ||
| }, | ||
| status: { | ||
| draft: "Rascunho", | ||
| published: "Publicado", | ||
| needs_review: "Necessita Revisão", | ||
| archived: "Arquivado", | ||
| }, | ||
| policies: "políticas", | ||
| title: "Políticas", | ||
| create_new: "Criar nova política", | ||
| search_placeholder: "Pesquisar políticas...", | ||
| status_filter: "Filtrar por status", | ||
| all_statuses: "Todos os status", | ||
| no_policies_title: "Nenhuma política ainda", | ||
| no_policies_description: "Comece criando sua primeira política", | ||
| create_first: "Criar primeira política", | ||
| no_description: "Nenhuma descrição fornecida", | ||
| last_updated: "Última atualização: {{date}}", | ||
| save: "Salvar", | ||
| saving: "Salvando...", | ||
| saved_success: "Política salva com sucesso", | ||
| saved_error: "Falha ao salvar a política", | ||
| overview: { | ||
| title: "Visão Geral da Política", | ||
| form: { | ||
| update_policy: "Atualizar Política", | ||
| update_policy_description: | ||
| "Atualize o título ou a descrição da política.", | ||
| update_policy_success: "Política atualizada com sucesso", | ||
| update_policy_error: "Falha ao atualizar a política", | ||
| update_policy_title: "Nome da Política", | ||
| review_frequency: "Frequência de Revisão", | ||
| review_frequency_placeholder: "Selecione uma frequência de revisão", | ||
| review_date: "Data da Revisão", | ||
| review_date_placeholder: "Selecione uma data de revisão", | ||
| }, | ||
| }, | ||
| }, | ||
| evidence_tasks: { | ||
| evidence_tasks: "Tarefas de Evidência", | ||
| overview: "Visão Geral", | ||
| }, | ||
| risk: { | ||
| risks: "riscos", | ||
| overview: "Visão Geral", | ||
| create: "Criar Novo Risco", | ||
| vendor: { | ||
| title: "Gestão de Fornecedores", | ||
| dashboard: { | ||
| title: "Painel de Fornecedores", | ||
| overview: "Visão Geral dos Fornecedores", | ||
| vendor_status: "Status do Fornecedor", | ||
| vendor_category: "Categorias de Fornecedor", | ||
| vendors_by_assignee: "Fornecedores por Responsável", | ||
| inherent_risk_description: | ||
| "Nível de risco inicial antes da aplicação de quaisquer controles", | ||
| residual_risk_description: | ||
| "Nível de risco restante após a aplicação de controles", | ||
| }, | ||
| register: { | ||
| title: "Registro de Fornecedor", | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Responsável", | ||
| }, | ||
| }, | ||
| assessment: { | ||
| title: "Avaliação de Fornecedor", | ||
| update_success: | ||
| "Avaliação de risco do fornecedor atualizada com sucesso", | ||
| update_error: "Falha ao atualizar a avaliação de risco do fornecedor", | ||
| inherent_risk: "Risco Inerente", | ||
| residual_risk: "Risco Residual", | ||
| }, | ||
| form: { | ||
| vendor_details: "Detalhes do Fornecedor", | ||
| vendor_name: "Nome", | ||
| vendor_name_placeholder: "Insira o nome do fornecedor", | ||
| vendor_website: "Website", | ||
| vendor_website_placeholder: "Insira o website do fornecedor", | ||
| vendor_description: "Descrição", | ||
| vendor_description_placeholder: "Insira a descrição do fornecedor", | ||
| vendor_category: "Categoria", | ||
| vendor_category_placeholder: "Selecione a categoria", | ||
| vendor_status: "Status", | ||
| vendor_status_placeholder: "Selecione o status", | ||
| create_vendor_success: "Fornecedor criado com sucesso", | ||
| create_vendor_error: "Falha ao criar fornecedor", | ||
| update_vendor: "Atualizar Fornecedor", | ||
| update_vendor_success: "Fornecedor atualizado com sucesso", | ||
| update_vendor_error: "Falha ao atualizar fornecedor", | ||
| add_comment: "Adicionar Comentário", | ||
| }, | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Proprietário", | ||
| }, | ||
| filters: { | ||
| search_placeholder: "Pesquisar fornecedores...", | ||
| status_placeholder: "Filtrar por status", | ||
| category_placeholder: "Filtrar por categoria", | ||
| owner_placeholder: "Filtrar por proprietário", | ||
| }, | ||
| empty_states: { | ||
| no_vendors: { | ||
| title: "Nenhum fornecedor ainda", | ||
| description: "Comece criando seu primeiro fornecedor", | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum fornecedor corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros", | ||
| }, | ||
| }, | ||
| actions: { | ||
| create: "Criar Fornecedor", | ||
| }, | ||
| status: { | ||
| not_assessed: "Não Avaliado", | ||
| in_progress: "Em Andamento", | ||
| assessed: "Avaliado", | ||
| }, | ||
| category: { | ||
| cloud: "Nuvem", | ||
| infrastructure: "Infraestrutura", | ||
| software_as_a_service: "Software como Serviço", | ||
| finance: "Finanças", | ||
| marketing: "Marketing", | ||
| sales: "Vendas", | ||
| hr: "RH", | ||
| other: "Outro", | ||
| }, | ||
| risk_level: { | ||
| low: "Baixo Risco", | ||
| medium: "Risco Médio", | ||
| high: "Alto Risco", | ||
| unknown: "Risco Desconhecido", | ||
| }, | ||
| }, | ||
| dashboard: { | ||
| title: "Painel", | ||
| overview: "Visão Geral do Risco", | ||
| risk_status: "Status do Risco", | ||
| risks_by_department: "Riscos por Departamento", | ||
| risks_by_assignee: "Riscos por Responsável", | ||
| inherent_risk_description: | ||
| "O risco inerente é calculado como probabilidade * impacto. Calculado antes de qualquer controle ser aplicado.", | ||
| residual_risk_description: | ||
| "O risco residual é calculado como probabilidade * impacto. Este é o nível de risco após a aplicação de controles.", | ||
| risk_assessment_description: | ||
| "Compare os níveis de risco inerente e residual", | ||
| }, | ||
| register: { | ||
| title: "Registro de Risco", | ||
| table: { | ||
| risk: "Risco", | ||
| }, | ||
| empty: { | ||
| no_risks: { | ||
| title: "Crie um risco para começar", | ||
| description: | ||
| "Acompanhe e classifique riscos, crie e atribua tarefas de mitigação para sua equipe e gerencie seu registro de risco tudo em uma interface simples.", | ||
| }, | ||
| create_risk: "Criar um risco", | ||
| }, | ||
| }, | ||
| metrics: { | ||
| probability: "Probabilidade", | ||
| impact: "Impacto", | ||
| inherentRisk: "Risco Inerente", | ||
| residualRisk: "Risco Residual", | ||
| }, | ||
| form: { | ||
| update_inherent_risk: "Salvar Risco Inerente", | ||
| update_inherent_risk_description: | ||
| "Atualize o risco inerente do risco. Este é o nível de risco antes de qualquer controle ser aplicado.", | ||
| update_inherent_risk_success: "Risco inerente atualizado com sucesso", | ||
| update_inherent_risk_error: "Falha ao atualizar risco inerente", | ||
| update_residual_risk: "Salvar Risco Residual", | ||
| update_residual_risk_description: | ||
| "Atualize o risco residual do risco. Este é o nível de risco após a aplicação de controles.", | ||
| update_residual_risk_success: "Risco residual atualizado com sucesso", | ||
| update_residual_risk_error: "Falha ao atualizar risco residual", | ||
| update_risk: "Atualizar Risco", | ||
| update_risk_description: "Atualize o título ou a descrição do risco.", | ||
| update_risk_success: "Risco atualizado com sucesso", | ||
| update_risk_error: "Falha ao atualizar risco", | ||
| create_risk_success: "Risco criado com sucesso", | ||
| create_risk_error: "Falha ao criar risco", | ||
| risk_details: "Detalhes do Risco", | ||
| risk_title: "Título do Risco", | ||
| risk_title_description: "Digite um nome para o risco", | ||
| risk_description: "Descrição", | ||
| risk_description_description: "Digite uma descrição para o risco", | ||
| risk_category: "Categoria", | ||
| risk_category_placeholder: "Selecione uma categoria", | ||
| risk_department: "Departamento", | ||
| risk_department_placeholder: "Selecione um departamento", | ||
| risk_status: "Status do Risco", | ||
| risk_status_placeholder: "Selecione um status de risco", | ||
| }, | ||
| tasks: { | ||
| title: "Tarefas", | ||
| attachments: "Anexos", | ||
| overview: "Visão Geral da Tarefa", | ||
| form: { | ||
| title: "Detalhes da Tarefa", | ||
| task_title: "Título da Tarefa", | ||
| status: "Status da Tarefa", | ||
| status_placeholder: "Selecione um status de tarefa", | ||
| task_title_description: "Digite um nome para a tarefa", | ||
| description: "Descrição", | ||
| description_description: "Digite uma descrição para a tarefa", | ||
| due_date: "Data de Vencimento", | ||
| due_date_description: "Selecione a data de vencimento para a tarefa", | ||
| success: "Tarefa criada com sucesso", | ||
| error: "Falha ao criar tarefa", | ||
| }, | ||
| sheet: { | ||
| title: "Criar Tarefa", | ||
| update: "Atualizar Tarefa", | ||
| update_description: "Atualize o título ou a descrição da tarefa.", | ||
| }, | ||
| empty: { | ||
| description_create: | ||
| "Crie uma tarefa de mitigação para este risco, adicione um plano de tratamento e atribua a um membro da equipe.", | ||
| }, | ||
| }, | ||
| }, | ||
| settings: { | ||
| general: { | ||
| title: "Geral", | ||
| org_name: "Nome da Organização", | ||
| org_name_description: | ||
| "Este é o nome visível da sua organização. Você deve usar o nome legal da sua organização.", | ||
| org_name_tip: "Por favor, use no máximo 32 caracteres.", | ||
| org_website: "Website da Organização", | ||
| org_website_description: | ||
| "Este é o URL do website oficial da sua organização. Certifique-se de incluir o URL completo com https://.", | ||
| org_website_tip: "Por favor, insira um URL válido incluindo https://", | ||
| org_website_error: "Erro ao atualizar o website da organização", | ||
| org_website_updated: "Website da organização atualizado", | ||
| org_delete: "Excluir organização", | ||
| org_delete_description: | ||
| "Remova permanentemente sua organização e todo o seu conteúdo da plataforma Comp AI. Esta ação não é reversível - continue com cautela.", | ||
| org_delete_alert_title: "Você tem certeza absoluta?", | ||
| org_delete_alert_description: | ||
| "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua organização e removerá seus dados de nossos servidores.", | ||
| org_delete_error: "Erro ao excluir organização", | ||
| org_delete_success: "Organização excluída", | ||
| org_name_updated: "Nome da organização atualizado", | ||
| org_name_error: "Erro ao atualizar o nome da organização", | ||
| save_button: "Salvar", | ||
| delete_button: "Excluir", | ||
| delete_confirm: "EXCLUIR", | ||
| delete_confirm_tip: "Digite EXCLUIR para confirmar.", | ||
| cancel_button: "Cancelar", | ||
| }, | ||
| members: { | ||
| title: "Membros", | ||
| }, | ||
| billing: { | ||
| title: "Faturamento", | ||
| }, | ||
| }, | ||
| user_menu: { | ||
| theme: "Tema", | ||
| language: "Idioma", | ||
| sign_out: "Sair", | ||
| account: "Conta", | ||
| support: "Suporte", | ||
| settings: "Configurações", | ||
| teams: "Equipes", | ||
| }, | ||
| frameworks: { | ||
| title: "Estruturas", | ||
| controls: { | ||
| title: "Controles", | ||
| description: "Revise e gerencie controles de conformidade", | ||
| table: { | ||
| status: "Status", | ||
| control: "Controle", | ||
| artifacts: "Artefatos", | ||
| actions: "Ações", | ||
| }, | ||
| statuses: { | ||
| not_started: "Não Iniciado", | ||
| completed: "Concluído", | ||
| in_progress: "Em Andamento", | ||
| }, | ||
| }, | ||
| overview: { | ||
| error: "Falha ao carregar frameworks", | ||
| loading: "Carregando frameworks...", | ||
| empty: { | ||
| title: "Nenhum framework selecionado", | ||
| description: | ||
| "Selecione frameworks para começar sua jornada de conformidade", | ||
| }, | ||
| progress: { | ||
| title: "Progresso do Framework", | ||
| empty: { | ||
| title: "Nenhum framework ainda", | ||
| description: | ||
| "Comece adicionando um framework de conformidade para acompanhar seu progresso", | ||
| action: "Adicionar Framework", | ||
| }, | ||
| }, | ||
| grid: { | ||
| welcome: { | ||
| title: "Bem-vindo ao Comp AI", | ||
| description: | ||
| "Comece selecionando os frameworks de conformidade que você gostaria de implementar. Nós o ajudaremos a gerenciar e acompanhar sua jornada de conformidade em múltiplos padrões.", | ||
| action: "Começar", | ||
| }, | ||
| title: "Selecionar Frameworks", | ||
| version: "Versão", | ||
| actions: { | ||
| clear: "Limpar", | ||
| confirm: "Confirmar Seleção", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| vendor: { | ||
| title: "Painel", | ||
| register_title: "Gerenciamento de Fornecedores", | ||
| dashboard: { | ||
| title: "Painel", | ||
| overview: "Visão Geral do Fornecedor", | ||
| vendor_status: "Status do Fornecedor", | ||
| vendor_category: "Categorias de Fornecedor", | ||
| vendors_by_assignee: "Fornecedores por Responsável", | ||
| inherent_risk_description: | ||
| "Nível de risco inicial antes que quaisquer controles sejam aplicados", | ||
| residual_risk_description: | ||
| "Nível de risco restante após a aplicação de controles", | ||
| }, | ||
| register: { | ||
| title: "Registro de Fornecedor", | ||
| table: { | ||
| name: "Nome", | ||
| category: "Categoria", | ||
| status: "Status", | ||
| owner: "Proprietário", | ||
| }, | ||
| }, | ||
| category: { | ||
| cloud: "Nuvem", | ||
| infrastructure: "Infraestrutura", | ||
| software_as_a_service: "SaaS", | ||
| finance: "Finanças", | ||
| marketing: "Marketing", | ||
| sales: "Vendas", | ||
| hr: "RH", | ||
| other: "Outro", | ||
| }, | ||
| vendors: "fornecedores", | ||
| form: { | ||
| vendor_details: "Detalhes do Fornecedor", | ||
| vendor_name: "Nome", | ||
| vendor_name_placeholder: "Digite o nome do fornecedor", | ||
| vendor_website: "Website", | ||
| vendor_website_placeholder: "Digite o website do fornecedor", | ||
| vendor_description: "Descrição", | ||
| vendor_description_placeholder: "Digite a descrição do fornecedor", | ||
| vendor_category: "Categoria", | ||
| vendor_category_placeholder: "Selecione a categoria", | ||
| vendor_status: "Status", | ||
| vendor_status_placeholder: "Selecione o status", | ||
| create_vendor_success: "Fornecedor criado com sucesso", | ||
| create_vendor_error: "Falha ao criar fornecedor", | ||
| update_vendor_success: "Fornecedor atualizado com sucesso", | ||
| update_vendor_error: "Falha ao atualizar fornecedor", | ||
| contacts: "Contatos do Fornecedor", | ||
| contact_name: "Nome do Contato", | ||
| contact_email: "Email do Contato", | ||
| contact_role: "Função do Contato", | ||
| add_contact: "Adicionar Contato", | ||
| new_contact: "Novo Contato", | ||
| min_one_contact_required: "Um fornecedor deve ter pelo menos um contato", | ||
| }, | ||
| empty_states: { | ||
| no_vendors: { | ||
| title: "Nenhum fornecedor ainda", | ||
| description: "Comece criando seu primeiro fornecedor", | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum fornecedor corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros", | ||
| }, | ||
| }, | ||
| }, | ||
| people: { | ||
| title: "Pessoas", | ||
| details: { | ||
| taskProgress: "Progresso da Tarefa", | ||
| tasks: "Tarefas", | ||
| noTasks: "Nenhuma tarefa atribuída ainda", | ||
| }, | ||
| description: "Gerencie os membros da sua equipe e seus papéis.", | ||
| filters: { | ||
| search: "Pesquisar pessoas...", | ||
| role: "Filtrar por papel", | ||
| }, | ||
| actions: { | ||
| invite: "Adicionar Funcionário", | ||
| clear: "Limpar filtros", | ||
| }, | ||
| table: { | ||
| name: "Nome", | ||
| email: "Email", | ||
| department: "Departamento", | ||
| externalId: "ID Externo", | ||
| }, | ||
| empty: { | ||
| no_employees: { | ||
| title: "Nenhum funcionário ainda", | ||
| description: "Comece convidando seu primeiro membro da equipe.", | ||
| }, | ||
| no_results: { | ||
| title: "Nenhum resultado encontrado", | ||
| description: "Nenhum funcionário corresponde à sua pesquisa", | ||
| description_with_filters: "Tente ajustar seus filtros", | ||
| }, | ||
| }, | ||
| invite: { | ||
| title: "Adicionar Funcionário", | ||
| description: "Adicione um funcionário à sua organização.", | ||
| email: { | ||
| label: "Endereço de email", | ||
| placeholder: "Digite o endereço de email", | ||
| }, | ||
| role: { | ||
| label: "Função", | ||
| placeholder: "Selecione uma função", | ||
| }, | ||
| name: { | ||
| label: "Nome", | ||
| placeholder: "Digite o nome", | ||
| }, | ||
| department: { | ||
| label: "Departamento", | ||
| placeholder: "Selecione um departamento", | ||
| }, | ||
| submit: "Adicionar Funcionário", | ||
| success: "Funcionário adicionado com sucesso", | ||
| error: "Falha ao adicionar funcionário", | ||
| }, | ||
| }, | ||
| errors: { | ||
| unexpected: "Algo deu errado, por favor tente novamente", | ||
| }, | ||
| sub_pages: { | ||
| risk: { | ||
| overview: "Gestão de Riscos", | ||
| register: "Registro de Riscos", | ||
| risk_overview: "Visão Geral dos Riscos", | ||
| risk_comments: "Comentários sobre Riscos", | ||
| tasks: { | ||
| task_overview: "Visão Geral da Tarefa", | ||
| }, | ||
| }, | ||
| policies: { | ||
| all: "Todas as Políticas", | ||
| editor: "Editor de Políticas", | ||
| policy_details: "Detalhes da política", | ||
| }, | ||
| people: { | ||
| all: "Pessoas", | ||
| employee_details: "Detalhes do Funcionário", | ||
| }, | ||
| settings: { | ||
| members: "Membros da Equipe", | ||
| }, | ||
| frameworks: { | ||
| overview: "Estruturas", | ||
| }, | ||
| evidence: { | ||
| title: "Evidência", | ||
| list: "Lista de Evidências", | ||
| overview: "Visão Geral das Evidências", | ||
| }, | ||
| }, | ||
| editor: { | ||
| ai: { | ||
| thinking: "A IA está pensando", | ||
| thinking_spinner: "A IA está pensando", | ||
| edit_or_generate: "Editar ou gerar...", | ||
| tell_ai_what_to_do_next: "Diga à IA o que fazer a seguir", | ||
| request_limit_reached: | ||
| "Você atingiu seu limite de solicitações para o dia.", | ||
| }, | ||
| ai_selector: { | ||
| improve: "Melhorar a escrita", | ||
| fix: "Corrigir gramática", | ||
| shorter: "Tornar mais curto", | ||
| longer: "Tornar mais longo", | ||
| continue: "Continuar escrevendo", | ||
| replace: "Substituir seleção", | ||
| insert: "Inserir abaixo", | ||
| discard: "Descartar", | ||
| }, | ||
| }, | ||
| evidence: { | ||
| title: "Tarefas de Evidência", | ||
| list: "Todas as Evidências", | ||
| }, | ||
| upload: { | ||
| fileSection: { | ||
| filesUploaded: "{{count}} arquivo{{s}} enviado", | ||
| upload: "{{count}} arquivo{{s}} enviado", | ||
| }, | ||
| fileUpload: { | ||
| uploadingText: "Enviando...", | ||
| dropFileHere: "Solte o arquivo aqui", | ||
| releaseToUpload: "Solte para enviar", | ||
| addFiles: "Adicionar Arquivos", | ||
| uploadAdditionalEvidence: "Enviar arquivos de evidência adicionais", | ||
| dragDropOrClick: "Arraste e solte ou clique para enviar", | ||
| dropFileHereAlt: "Solte o arquivo aqui", | ||
| dragDropOrClickToSelect: | ||
| "Arraste e solte um arquivo aqui ou clique para selecionar", | ||
| maxFileSize: "Tamanho máximo do arquivo: {{size}}MB", | ||
| uploadingFile: "Enviando arquivo...", | ||
| }, | ||
| fileCard: { | ||
| preview: "Pré-visualização", | ||
| previewNotAvailable: | ||
| "Pré-visualização não disponível. Clique no botão de download para visualizar o arquivo.", | ||
| filePreview: "Pré-visualização do arquivo: {fileName}", | ||
| openFile: "Abrir arquivo", | ||
| deleteFile: "Excluir arquivo", | ||
| deleteFileConfirmTitle: "Excluir Arquivo", | ||
| deleteFileConfirmDescription: | ||
| "Você tem certeza de que deseja excluir este arquivo? Esta ação não pode ser desfeita.", | ||
| }, | ||
| fileUrl: { | ||
| additionalLinks: "Links Adicionais", | ||
| add: "Adicionar", | ||
| linksAdded: "{count} link{s} adicionado", | ||
| enterUrl: "Digite a URL", | ||
| addAnotherLink: "Adicionar Outro Link", | ||
| saveLinks: "Salvar Links", | ||
| urlBadge: "URL", | ||
| copyLink: "Copiar link", | ||
| openLink: "Abrir link", | ||
| deleteLink: "Excluir link", | ||
| }, | ||
| }, | ||
| languages: { |
There was a problem hiding this comment.
Fix duplicate object properties to prevent unexpected behavior.
The static analysis tool detected multiple duplicate property keys in this file, including languages, language, common, and several others. In JavaScript/TypeScript objects, when properties with the same name are defined multiple times, only the last definition is used, making the earlier definitions ineffective.
This file structure appears to have duplicate keys throughout. You need to restructure this localization file to eliminate duplicate keys. Please verify if the properties at the bottom of the file (from line 844 onwards) should replace earlier definitions or if they need to be reorganized.
// Example showing the issue - you would need to apply this pattern across the file
export default {
- languages: {
- es: "Espanhol",
- // other language definitions...
- },
- // Later in the file...
- languages: {
- es: "Espanhol",
- // duplicate definitions...
- },
+ languages: {
+ es: "Espanhol",
+ // single consolidated definition...
+ },
// Other properties...
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export default { | |
| languages: { | |
| es: "Espanhol", | |
| fr: "Francês", | |
| no: "Norueguês", | |
| pt: "Português", | |
| en: "Inglês" | |
| }, | |
| language: { | |
| title: "Idiomas", | |
| description: "Altere o idioma usado na interface do usuário.", | |
| placeholder: "Selecione o idioma" | |
| }, | |
| common: { | |
| actions: { | |
| save: "Salvar", | |
| edit: "Editar", | |
| "delete": "Excluir", | |
| cancel: "Cancelar", | |
| clear: "Limpar", | |
| create: "Criar", | |
| send: "Enviar", | |
| "return": "Retornar", | |
| success: "Sucesso", | |
| error: "Erro", | |
| next: "Próximo", | |
| complete: "Completo", | |
| addNew: "Adicionar Novo" | |
| }, | |
| assignee: { | |
| label: "Responsável", | |
| placeholder: "Selecione o responsável" | |
| }, | |
| date: { | |
| pick: "Escolher uma data", | |
| due_date: "Data de Vencimento" | |
| }, | |
| status: { | |
| open: "Aberto", | |
| pending: "Pendente", | |
| closed: "Fechado", | |
| archived: "Arquivado", | |
| compliant: "Conforme", | |
| non_compliant: "Não Conforme", | |
| not_started: "Não Iniciado", | |
| in_progress: "Em Andamento", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| draft: "Rascunho", | |
| not_assessed: "Não Avaliado", | |
| assessed: "Avaliado", | |
| active: "Ativo", | |
| inactive: "Inativo", | |
| title: "Status" | |
| }, | |
| filters: { | |
| clear: "Limpar filtros", | |
| search: "Pesquisar...", | |
| status: "Status", | |
| department: "Departamento", | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável" | |
| } | |
| }, | |
| table: { | |
| title: "Título", | |
| status: "Status", | |
| assigned_to: "Atribuído A", | |
| due_date: "Data de Vencimento", | |
| last_updated: "Última Atualização", | |
| no_results: "Nenhum resultado encontrado" | |
| }, | |
| empty_states: { | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| title_tasks: "Nenhuma tarefa encontrada", | |
| title_risks: "Nenhum risco encontrado", | |
| description: "Tente outra pesquisa ou ajuste os filtros", | |
| description_filters: "Tente outra pesquisa ou ajuste os filtros", | |
| description_no_tasks: "Crie uma tarefa para começar", | |
| description_no_risks: "Crie um risco para começar" | |
| }, | |
| no_items: { | |
| title: "Nenhum item encontrado", | |
| description: "Tente ajustar sua pesquisa ou filtros" | |
| } | |
| }, | |
| pagination: { | |
| of: "de", | |
| items_per_page: "Itens por página", | |
| rows_per_page: "Linhas por página", | |
| page_x_of_y: "Página {{current}} de {{total}}", | |
| go_to_first_page: "Ir para a primeira página", | |
| go_to_previous_page: "Ir para a página anterior", | |
| go_to_next_page: "Ir para a próxima página", | |
| go_to_last_page: "Ir para a última página" | |
| }, | |
| comments: { | |
| title: "Comentários", | |
| description: "Adicione um comentário usando o formulário abaixo.", | |
| add: "Novo Comentário", | |
| "new": "Novo Comentário", | |
| save: "Salvar Comentário", | |
| success: "Comentário adicionado com sucesso", | |
| error: "Falha ao adicionar comentário", | |
| placeholder: "Escreva seu comentário aqui...", | |
| empty: { | |
| title: "Nenhum comentário ainda", | |
| description: "Seja o primeiro a adicionar um comentário" | |
| } | |
| }, | |
| attachments: { | |
| title: "Anexos", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | |
| upload: "Enviar anexo", | |
| upload_description: "Carregue um anexo ou adicione um link para um recurso externo.", | |
| drop: "Solte os arquivos aqui", | |
| drop_description: "Solte arquivos aqui ou clique para escolher arquivos do seu dispositivo.", | |
| drop_files_description: "Os arquivos podem ter até ", | |
| empty: { | |
| title: "Nenhum anexo", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'." | |
| }, | |
| toasts: { | |
| error: "Algo deu errado, por favor tente novamente.", | |
| error_uploading_files: "Não é possível carregar mais de 1 arquivo por vez", | |
| error_uploading_files_multiple: "Não é possível carregar mais de 10 arquivos", | |
| error_no_files_selected: "Nenhum arquivo selecionado", | |
| error_file_rejected: "Arquivo {file} foi rejeitado", | |
| error_failed_to_upload_files: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_multiple: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_single: "Falha ao carregar arquivo", | |
| success_uploading_files: "Arquivos carregados com sucesso", | |
| success_uploading_files_multiple: "Arquivos carregados com sucesso", | |
| success_uploading_files_single: "Arquivo carregado com sucesso", | |
| success_uploading_files_target: "Arquivos carregados", | |
| uploading_files: "Carregando {target}...", | |
| remove_file: "Remover arquivo" | |
| } | |
| }, | |
| notifications: { | |
| inbox: "Caixa de entrada", | |
| archive: "Arquivo", | |
| archive_all: "Arquivar tudo", | |
| no_notifications: "Sem novas notificações" | |
| }, | |
| edit: "Editar", | |
| errors: { | |
| unexpected_error: "Ocorreu um erro inesperado" | |
| }, | |
| description: "Descrição", | |
| last_updated: "Última atualização", | |
| frequency: { | |
| daily: "Diário", | |
| weekly: "Semanal", | |
| monthly: "Mensal", | |
| quarterly: "Trimestral", | |
| yearly: "Anual" | |
| } | |
| }, | |
| header: { | |
| discord: { | |
| button: "Junte-se a nós no Discord" | |
| }, | |
| feedback: { | |
| button: "Feedback", | |
| title: "Obrigado pelo seu feedback!", | |
| description: "Voltaremos a você assim que possível", | |
| placeholder: "Ideias para melhorar esta página ou problemas que você está enfrentando.", | |
| success: "Obrigado pelo seu feedback!", | |
| error: "Erro ao enviar feedback - tentar novamente?", | |
| send: "Enviar Feedback" | |
| } | |
| }, | |
| not_found: { | |
| title: "404 - Página não encontrada", | |
| description: "A página que você está procurando não existe.", | |
| "return": "Retornar ao painel" | |
| }, | |
| theme: { | |
| options: { | |
| light: "Claro", | |
| dark: "Escuro", | |
| system: "Sistema" | |
| } | |
| }, | |
| sidebar: { | |
| overview: "Visão Geral", | |
| policies: "Políticas", | |
| risk: "Gestão de Riscos", | |
| vendors: "Fornecedores", | |
| integrations: "Integrações", | |
| settings: "Configurações", | |
| evidence: "Tarefas de Evidência", | |
| people: "Pessoas" | |
| }, | |
| auth: { | |
| title: "Automatize a conformidade com SOC 2, ISO 27001 e GDPR com IA.", | |
| description: "Crie uma conta gratuita ou faça login com uma conta existente para continuar.", | |
| options: "Mais opções", | |
| google: "Continuar com o Google", | |
| email: { | |
| description: "Insira seu endereço de e-mail para continuar.", | |
| placeholder: "Insira o endereço de e-mail", | |
| button: "Continuar com e-mail", | |
| magic_link_sent: "Link mágico enviado", | |
| magic_link_description: "Verifique sua caixa de entrada para um link mágico.", | |
| magic_link_try_again: "Tente novamente.", | |
| success: "E-mail enviado - verifique sua caixa de entrada!", | |
| error: "Erro ao enviar e-mail - tentar novamente?" | |
| }, | |
| terms: "Ao clicar em continuar, você reconhece que leu e concorda com os Termos de Serviço e a Política de Privacidade." | |
| }, | |
| onboarding: { | |
| title: "Criar uma organização", | |
| setup: "Configuração", | |
| description: "Conte-nos um pouco sobre sua organização.", | |
| fields: { | |
| name: { | |
| label: "Nome da Organização", | |
| placeholder: "O nome da sua organização" | |
| }, | |
| website: { | |
| label: "Website", | |
| placeholder: "O website da sua organização" | |
| }, | |
| subdomain: { | |
| label: "Subdomínio", | |
| placeholder: "exemplo" | |
| }, | |
| fullName: { | |
| label: "Seu Nome", | |
| placeholder: "Seu nome completo" | |
| } | |
| }, | |
| success: "Obrigado, tudo pronto!", | |
| error: "Algo deu errado, por favor tente novamente.", | |
| unavailable: "Indisponível", | |
| check_availability: "Verificando disponibilidade", | |
| available: "Disponível" | |
| }, | |
| overview: { | |
| title: "Visão Geral", | |
| framework_chart: { | |
| title: "Progresso do Framework" | |
| }, | |
| requirement_chart: { | |
| title: "Status de Conformidade" | |
| } | |
| }, | |
| policies: { | |
| dashboard: { | |
| title: "Painel", | |
| all: "Todas as Políticas", | |
| policy_status: "Política por Status", | |
| policies_by_assignee: "Políticas por Responsável", | |
| policies_by_framework: "Políticas por Framework", | |
| sub_pages: { | |
| overview: "Visão Geral", | |
| edit_policy: "Editar Política" | |
| } | |
| }, | |
| table: { | |
| name: "Nome da Política", | |
| statuses: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| archived: "Arquivado" | |
| }, | |
| filters: { | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável" | |
| } | |
| } | |
| }, | |
| filters: { | |
| search: "Pesquisar políticas...", | |
| all: "Todas as Políticas" | |
| }, | |
| status: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| archived: "Arquivado" | |
| }, | |
| policies: "políticas", | |
| title: "Políticas", | |
| create_new: "Criar nova política", | |
| search_placeholder: "Pesquisar políticas...", | |
| status_filter: "Filtrar por status", | |
| all_statuses: "Todos os status", | |
| no_policies_title: "Nenhuma política ainda", | |
| no_policies_description: "Comece criando sua primeira política", | |
| create_first: "Criar primeira política", | |
| no_description: "Nenhuma descrição fornecida", | |
| last_updated: "Última atualização: {{date}}", | |
| save: "Salvar", | |
| saving: "Salvando...", | |
| saved_success: "Política salva com sucesso", | |
| saved_error: "Falha ao salvar a política", | |
| overview: { | |
| title: "Visão Geral da Política", | |
| form: { | |
| update_policy: "Atualizar Política", | |
| update_policy_description: "Atualize o título ou a descrição da política.", | |
| update_policy_success: "Política atualizada com sucesso", | |
| update_policy_error: "Falha ao atualizar a política", | |
| update_policy_title: "Nome da Política", | |
| review_frequency: "Frequência de Revisão", | |
| review_frequency_placeholder: "Selecione uma frequência de revisão", | |
| review_date: "Data da Revisão", | |
| review_date_placeholder: "Selecione uma data de revisão" | |
| } | |
| } | |
| }, | |
| evidence_tasks: { | |
| evidence_tasks: "Tarefas de Evidência", | |
| overview: "Visão Geral" | |
| }, | |
| risk: { | |
| risks: "riscos", | |
| overview: "Visão Geral", | |
| create: "Criar Novo Risco", | |
| vendor: { | |
| title: "Gestão de Fornecedores", | |
| dashboard: { | |
| title: "Painel de Fornecedores", | |
| overview: "Visão Geral dos Fornecedores", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: "Nível de risco inicial antes da aplicação de quaisquer controles", | |
| residual_risk_description: "Nível de risco restante após a aplicação de controles" | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Responsável" | |
| } | |
| }, | |
| assessment: { | |
| title: "Avaliação de Fornecedor", | |
| update_success: "Avaliação de risco do fornecedor atualizada com sucesso", | |
| update_error: "Falha ao atualizar a avaliação de risco do fornecedor", | |
| inherent_risk: "Risco Inerente", | |
| residual_risk: "Risco Residual" | |
| }, | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Insira o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Insira o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Insira a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor: "Atualizar Fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| add_comment: "Adicionar Comentário" | |
| }, | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário" | |
| }, | |
| filters: { | |
| search_placeholder: "Pesquisar fornecedores...", | |
| status_placeholder: "Filtrar por status", | |
| category_placeholder: "Filtrar por categoria", | |
| owner_placeholder: "Filtrar por proprietário" | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor" | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros" | |
| } | |
| }, | |
| actions: { | |
| create: "Criar Fornecedor" | |
| }, | |
| status: { | |
| not_assessed: "Não Avaliado", | |
| in_progress: "Em Andamento", | |
| assessed: "Avaliado" | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "Software como Serviço", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro" | |
| }, | |
| risk_level: { | |
| low: "Baixo Risco", | |
| medium: "Risco Médio", | |
| high: "Alto Risco", | |
| unknown: "Risco Desconhecido" | |
| } | |
| }, | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Risco", | |
| risk_status: "Status do Risco", | |
| risks_by_department: "Riscos por Departamento", | |
| risks_by_assignee: "Riscos por Responsável", | |
| inherent_risk_description: "O risco inerente é calculado como probabilidade * impacto. Calculado antes de qualquer controle ser aplicado.", | |
| residual_risk_description: "O risco residual é calculado como probabilidade * impacto. Este é o nível de risco após a aplicação de controles.", | |
| risk_assessment_description: "Compare os níveis de risco inerente e residual" | |
| }, | |
| register: { | |
| title: "Registro de Risco", | |
| table: { | |
| risk: "Risco" | |
| }, | |
| empty: { | |
| no_risks: { | |
| title: "Crie um risco para começar", | |
| description: "Acompanhe e classifique riscos, crie e atribua tarefas de mitigação para sua equipe e gerencie seu registro de risco tudo em uma interface simples." | |
| }, | |
| create_risk: "Criar um risco" | |
| } | |
| }, | |
| metrics: { | |
| probability: "Probabilidade", | |
| impact: "Impacto", | |
| inherentRisk: "Risco Inerente", | |
| residualRisk: "Risco Residual" | |
| }, | |
| form: { | |
| update_inherent_risk: "Salvar Risco Inerente", | |
| update_inherent_risk_description: "Atualize o risco inerente do risco. Este é o nível de risco antes de qualquer controle ser aplicado.", | |
| update_inherent_risk_success: "Risco inerente atualizado com sucesso", | |
| update_inherent_risk_error: "Falha ao atualizar risco inerente", | |
| update_residual_risk: "Salvar Risco Residual", | |
| update_residual_risk_description: "Atualize o risco residual do risco. Este é o nível de risco após a aplicação de controles.", | |
| update_residual_risk_success: "Risco residual atualizado com sucesso", | |
| update_residual_risk_error: "Falha ao atualizar risco residual", | |
| update_risk: "Atualizar Risco", | |
| update_risk_description: "Atualize o título ou a descrição do risco.", | |
| update_risk_success: "Risco atualizado com sucesso", | |
| update_risk_error: "Falha ao atualizar risco", | |
| create_risk_success: "Risco criado com sucesso", | |
| create_risk_error: "Falha ao criar risco", | |
| risk_details: "Detalhes do Risco", | |
| risk_title: "Título do Risco", | |
| risk_title_description: "Digite um nome para o risco", | |
| risk_description: "Descrição", | |
| risk_description_description: "Digite uma descrição para o risco", | |
| risk_category: "Categoria", | |
| risk_category_placeholder: "Selecione uma categoria", | |
| risk_department: "Departamento", | |
| risk_department_placeholder: "Selecione um departamento", | |
| risk_status: "Status do Risco", | |
| risk_status_placeholder: "Selecione um status de risco" | |
| }, | |
| tasks: { | |
| title: "Tarefas", | |
| attachments: "Anexos", | |
| overview: "Visão Geral da Tarefa", | |
| form: { | |
| title: "Detalhes da Tarefa", | |
| task_title: "Título da Tarefa", | |
| status: "Status da Tarefa", | |
| status_placeholder: "Selecione um status de tarefa", | |
| task_title_description: "Digite um nome para a tarefa", | |
| description: "Descrição", | |
| description_description: "Digite uma descrição para a tarefa", | |
| due_date: "Data de Vencimento", | |
| due_date_description: "Selecione a data de vencimento para a tarefa", | |
| success: "Tarefa criada com sucesso", | |
| error: "Falha ao criar tarefa" | |
| }, | |
| sheet: { | |
| title: "Criar Tarefa", | |
| update: "Atualizar Tarefa", | |
| update_description: "Atualize o título ou a descrição da tarefa." | |
| }, | |
| empty: { | |
| description_create: "Crie uma tarefa de mitigação para este risco, adicione um plano de tratamento e atribua a um membro da equipe." | |
| } | |
| } | |
| }, | |
| settings: { | |
| general: { | |
| title: "Geral", | |
| org_name: "Nome da Organização", | |
| org_name_description: "Este é o nome visível da sua organização. Você deve usar o nome legal da sua organização.", | |
| org_name_tip: "Por favor, use no máximo 32 caracteres.", | |
| org_website: "Website da Organização", | |
| org_website_description: "Este é o URL do website oficial da sua organização. Certifique-se de incluir o URL completo com https://.", | |
| org_website_tip: "Por favor, insira um URL válido incluindo https://", | |
| org_website_error: "Erro ao atualizar o website da organização", | |
| org_website_updated: "Website da organização atualizado", | |
| org_delete: "Excluir organização", | |
| org_delete_description: "Remova permanentemente sua organização e todo o seu conteúdo da plataforma Comp AI. Esta ação não é reversível - continue com cautela.", | |
| org_delete_alert_title: "Você tem certeza absoluta?", | |
| org_delete_alert_description: "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua organização e removerá seus dados de nossos servidores.", | |
| org_delete_error: "Erro ao excluir organização", | |
| org_delete_success: "Organização excluída", | |
| org_name_updated: "Nome da organização atualizado", | |
| org_name_error: "Erro ao atualizar o nome da organização", | |
| save_button: "Salvar", | |
| delete_button: "Excluir", | |
| delete_confirm: "EXCLUIR", | |
| delete_confirm_tip: "Digite EXCLUIR para confirmar.", | |
| cancel_button: "Cancelar" | |
| }, | |
| members: { | |
| title: "Membros" | |
| }, | |
| billing: { | |
| title: "Faturamento" | |
| } | |
| }, | |
| user_menu: { | |
| theme: "Tema", | |
| language: "Idioma", | |
| sign_out: "Sair", | |
| account: "Conta", | |
| support: "Suporte", | |
| settings: "Configurações", | |
| teams: "Equipes" | |
| }, | |
| frameworks: { | |
| title: "Estruturas", | |
| controls: { | |
| title: "Controles", | |
| description: "Revise e gerencie controles de conformidade", | |
| table: { | |
| status: "Status", | |
| control: "Controle", | |
| artifacts: "Artefatos", | |
| actions: "Ações" | |
| }, | |
| statuses: { | |
| not_started: "Não Iniciado", | |
| completed: "Concluído", | |
| in_progress: "Em Andamento" | |
| } | |
| }, | |
| overview: { | |
| error: "Falha ao carregar frameworks", | |
| loading: "Carregando frameworks...", | |
| empty: { | |
| title: "Nenhum framework selecionado", | |
| description: "Selecione frameworks para começar sua jornada de conformidade" | |
| }, | |
| progress: { | |
| title: "Progresso do Framework", | |
| empty: { | |
| title: "Nenhum framework ainda", | |
| description: "Comece adicionando um framework de conformidade para acompanhar seu progresso", | |
| action: "Adicionar Framework" | |
| } | |
| }, | |
| grid: { | |
| welcome: { | |
| title: "Bem-vindo ao Comp AI", | |
| description: "Comece selecionando os frameworks de conformidade que você gostaria de implementar. Nós o ajudaremos a gerenciar e acompanhar sua jornada de conformidade em múltiplos padrões.", | |
| action: "Começar" | |
| }, | |
| title: "Selecionar Frameworks", | |
| version: "Versão", | |
| actions: { | |
| clear: "Limpar", | |
| confirm: "Confirmar Seleção" | |
| } | |
| } | |
| } | |
| }, | |
| vendor: { | |
| title: "Painel", | |
| register_title: "Gerenciamento de Fornecedores", | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Fornecedor", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: "Nível de risco inicial antes que quaisquer controles sejam aplicados", | |
| residual_risk_description: "Nível de risco restante após a aplicação de controles" | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário" | |
| } | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "SaaS", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro" | |
| }, | |
| vendors: "fornecedores", | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Digite o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Digite o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Digite a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| contacts: "Contatos do Fornecedor", | |
| contact_name: "Nome do Contato", | |
| contact_email: "Email do Contato", | |
| contact_role: "Função do Contato", | |
| add_contact: "Adicionar Contato", | |
| new_contact: "Novo Contato", | |
| min_one_contact_required: "Um fornecedor deve ter pelo menos um contato" | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor" | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros" | |
| } | |
| } | |
| }, | |
| people: { | |
| title: "Pessoas", | |
| details: { | |
| taskProgress: "Progresso da Tarefa", | |
| tasks: "Tarefas", | |
| noTasks: "Nenhuma tarefa atribuída ainda" | |
| }, | |
| description: "Gerencie os membros da sua equipe e seus papéis.", | |
| filters: { | |
| search: "Pesquisar pessoas...", | |
| role: "Filtrar por papel" | |
| }, | |
| actions: { | |
| invite: "Adicionar Funcionário", | |
| clear: "Limpar filtros" | |
| }, | |
| table: { | |
| name: "Nome", | |
| email: "Email", | |
| department: "Departamento", | |
| externalId: "ID Externo" | |
| }, | |
| empty: { | |
| no_employees: { | |
| title: "Nenhum funcionário ainda", | |
| description: "Comece convidando seu primeiro membro da equipe." | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum funcionário corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros" | |
| } | |
| }, | |
| invite: { | |
| title: "Adicionar Funcionário", | |
| description: "Adicione um funcionário à sua organização.", | |
| email: { | |
| label: "Endereço de email", | |
| placeholder: "Digite o endereço de email" | |
| }, | |
| role: { | |
| label: "Função", | |
| placeholder: "Selecione uma função" | |
| }, | |
| name: { | |
| label: "Nome", | |
| placeholder: "Digite o nome" | |
| }, | |
| department: { | |
| label: "Departamento", | |
| placeholder: "Selecione um departamento" | |
| }, | |
| submit: "Adicionar Funcionário", | |
| success: "Funcionário adicionado com sucesso", | |
| error: "Falha ao adicionar funcionário" | |
| } | |
| }, | |
| errors: { | |
| unexpected: "Algo deu errado, por favor tente novamente" | |
| }, | |
| sub_pages: { | |
| risk: { | |
| overview: "Gestão de Riscos", | |
| register: "Registro de Riscos", | |
| risk_overview: "Visão Geral dos Riscos", | |
| risk_comments: "Comentários sobre Riscos", | |
| tasks: { | |
| task_overview: "Visão Geral da Tarefa" | |
| } | |
| }, | |
| policies: { | |
| all: "Todas as Políticas", | |
| editor: "Editor de Políticas", | |
| policy_details: "Detalhes da política" | |
| }, | |
| people: { | |
| all: "Pessoas", | |
| employee_details: "Detalhes do Funcionário" | |
| }, | |
| settings: { | |
| members: "Membros da Equipe" | |
| }, | |
| frameworks: { | |
| overview: "Estruturas" | |
| }, | |
| evidence: { | |
| title: "Evidência", | |
| list: "Lista de Evidências", | |
| overview: "Visão Geral das Evidências" | |
| } | |
| }, | |
| editor: { | |
| ai: { | |
| thinking: "A IA está pensando", | |
| thinking_spinner: "A IA está pensando", | |
| edit_or_generate: "Editar ou gerar...", | |
| tell_ai_what_to_do_next: "Diga à IA o que fazer a seguir", | |
| request_limit_reached: "Você atingiu seu limite de solicitações para o dia." | |
| }, | |
| ai_selector: { | |
| improve: "Melhorar a escrita", | |
| fix: "Corrigir gramática", | |
| shorter: "Tornar mais curto", | |
| longer: "Tornar mais longo", | |
| "continue": "Continuar escrevendo", | |
| replace: "Substituir seleção", | |
| insert: "Inserir abaixo", | |
| discard: "Descartar" | |
| } | |
| }, | |
| evidence: { | |
| title: "Tarefas de Evidência", | |
| list: "Todas as Evidências" | |
| } | |
| languages: { | |
| es: "Espanhol", | |
| fr: "Francês", | |
| no: "Norueguês", | |
| pt: "Português", | |
| en: "Inglês", | |
| }, | |
| language: { | |
| title: "Idiomas", | |
| description: "Altere o idioma usado na interface do usuário.", | |
| placeholder: "Selecione o idioma", | |
| }, | |
| common: { | |
| actions: { | |
| save: "Salvar", | |
| edit: "Editar", | |
| delete: "Excluir", | |
| cancel: "Cancelar", | |
| clear: "Limpar", | |
| create: "Criar", | |
| send: "Enviar", | |
| return: "Retornar", | |
| success: "Sucesso", | |
| error: "Erro", | |
| next: "Próximo", | |
| complete: "Completo", | |
| addNew: "Adicionar Novo", | |
| }, | |
| assignee: { | |
| label: "Responsável", | |
| placeholder: "Selecione o responsável", | |
| }, | |
| date: { | |
| pick: "Escolher uma data", | |
| due_date: "Data de Vencimento", | |
| }, | |
| status: { | |
| open: "Aberto", | |
| pending: "Pendente", | |
| closed: "Fechado", | |
| archived: "Arquivado", | |
| compliant: "Conforme", | |
| non_compliant: "Não Conforme", | |
| not_started: "Não Iniciado", | |
| in_progress: "Em Andamento", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| draft: "Rascunho", | |
| not_assessed: "Não Avaliado", | |
| assessed: "Avaliado", | |
| active: "Ativo", | |
| inactive: "Inativo", | |
| title: "Status", | |
| }, | |
| filters: { | |
| clear: "Limpar filtros", | |
| search: "Pesquisar...", | |
| status: "Status", | |
| department: "Departamento", | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável", | |
| }, | |
| }, | |
| table: { | |
| title: "Título", | |
| status: "Status", | |
| assigned_to: "Atribuído A", | |
| due_date: "Data de Vencimento", | |
| last_updated: "Última Atualização", | |
| no_results: "Nenhum resultado encontrado", | |
| }, | |
| empty_states: { | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| title_tasks: "Nenhuma tarefa encontrada", | |
| title_risks: "Nenhum risco encontrado", | |
| description: "Tente outra pesquisa ou ajuste os filtros", | |
| description_filters: "Tente outra pesquisa ou ajuste os filtros", | |
| description_no_tasks: "Crie uma tarefa para começar", | |
| description_no_risks: "Crie um risco para começar", | |
| }, | |
| no_items: { | |
| title: "Nenhum item encontrado", | |
| description: "Tente ajustar sua pesquisa ou filtros", | |
| }, | |
| }, | |
| pagination: { | |
| of: "de", | |
| items_per_page: "Itens por página", | |
| rows_per_page: "Linhas por página", | |
| page_x_of_y: "Página {{current}} de {{total}}", | |
| go_to_first_page: "Ir para a primeira página", | |
| go_to_previous_page: "Ir para a página anterior", | |
| go_to_next_page: "Ir para a próxima página", | |
| go_to_last_page: "Ir para a última página", | |
| }, | |
| comments: { | |
| title: "Comentários", | |
| description: "Adicione um comentário usando o formulário abaixo.", | |
| add: "Novo Comentário", | |
| new: "Novo Comentário", | |
| save: "Salvar Comentário", | |
| success: "Comentário adicionado com sucesso", | |
| error: "Falha ao adicionar comentário", | |
| placeholder: "Escreva seu comentário aqui...", | |
| empty: { | |
| title: "Nenhum comentário ainda", | |
| description: "Seja o primeiro a adicionar um comentário", | |
| }, | |
| }, | |
| attachments: { | |
| title: "Anexos", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | |
| upload: "Enviar anexo", | |
| upload_description: | |
| "Carregue um anexo ou adicione um link para um recurso externo.", | |
| drop: "Solte os arquivos aqui", | |
| drop_description: | |
| "Solte arquivos aqui ou clique para escolher arquivos do seu dispositivo.", | |
| drop_files_description: "Os arquivos podem ter até ", | |
| empty: { | |
| title: "Nenhum anexo", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | |
| }, | |
| toasts: { | |
| error: "Algo deu errado, por favor tente novamente.", | |
| error_uploading_files: | |
| "Não é possível carregar mais de 1 arquivo por vez", | |
| error_uploading_files_multiple: | |
| "Não é possível carregar mais de 10 arquivos", | |
| error_no_files_selected: "Nenhum arquivo selecionado", | |
| error_file_rejected: "Arquivo {file} foi rejeitado", | |
| error_failed_to_upload_files: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_multiple: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_single: "Falha ao carregar arquivo", | |
| success_uploading_files: "Arquivos carregados com sucesso", | |
| success_uploading_files_multiple: "Arquivos carregados com sucesso", | |
| success_uploading_files_single: "Arquivo carregado com sucesso", | |
| success_uploading_files_target: "Arquivos carregados", | |
| uploading_files: "Carregando {target}...", | |
| remove_file: "Remover arquivo", | |
| }, | |
| }, | |
| notifications: { | |
| inbox: "Caixa de entrada", | |
| archive: "Arquivo", | |
| archive_all: "Arquivar tudo", | |
| no_notifications: "Sem novas notificações", | |
| }, | |
| edit: "Editar", | |
| errors: { | |
| unexpected_error: "Ocorreu um erro inesperado", | |
| }, | |
| description: "Descrição", | |
| last_updated: "Última atualização", | |
| frequency: { | |
| daily: "Diário", | |
| weekly: "Semanal", | |
| monthly: "Mensal", | |
| quarterly: "Trimestral", | |
| yearly: "Anual", | |
| }, | |
| }, | |
| header: { | |
| discord: { | |
| button: "Junte-se a nós no Discord", | |
| }, | |
| feedback: { | |
| button: "Feedback", | |
| title: "Obrigado pelo seu feedback!", | |
| description: "Voltaremos a você assim que possível", | |
| placeholder: | |
| "Ideias para melhorar esta página ou problemas que você está enfrentando.", | |
| success: "Obrigado pelo seu feedback!", | |
| error: "Erro ao enviar feedback - tentar novamente?", | |
| send: "Enviar Feedback", | |
| }, | |
| }, | |
| not_found: { | |
| title: "404 - Página não encontrada", | |
| description: "A página que você está procurando não existe.", | |
| return: "Retornar ao painel", | |
| }, | |
| theme: { | |
| options: { | |
| light: "Claro", | |
| dark: "Escuro", | |
| system: "Sistema", | |
| }, | |
| }, | |
| sidebar: { | |
| overview: "Visão Geral", | |
| policies: "Políticas", | |
| risk: "Gestão de Riscos", | |
| vendors: "Fornecedores", | |
| integrations: "Integrações", | |
| settings: "Configurações", | |
| evidence: "Tarefas de Evidência", | |
| people: "Pessoas", | |
| }, | |
| auth: { | |
| title: "Automatize a conformidade com SOC 2, ISO 27001 e GDPR com IA.", | |
| description: | |
| "Crie uma conta gratuita ou faça login com uma conta existente para continuar.", | |
| options: "Mais opções", | |
| google: "Continuar com o Google", | |
| email: { | |
| description: "Insira seu endereço de e-mail para continuar.", | |
| placeholder: "Insira o endereço de e-mail", | |
| button: "Continuar com e-mail", | |
| magic_link_sent: "Link mágico enviado", | |
| magic_link_description: | |
| "Verifique sua caixa de entrada para um link mágico.", | |
| magic_link_try_again: "Tente novamente.", | |
| success: "E-mail enviado - verifique sua caixa de entrada!", | |
| error: "Erro ao enviar e-mail - tentar novamente?", | |
| }, | |
| terms: | |
| "Ao clicar em continuar, você reconhece que leu e concorda com os Termos de Serviço e a Política de Privacidade.", | |
| }, | |
| onboarding: { | |
| title: "Criar uma organização", | |
| setup: "Configuração", | |
| description: "Conte-nos um pouco sobre sua organização.", | |
| fields: { | |
| name: { | |
| label: "Nome da Organização", | |
| placeholder: "O nome da sua organização", | |
| }, | |
| website: { | |
| label: "Website", | |
| placeholder: "O website da sua organização", | |
| }, | |
| subdomain: { | |
| label: "Subdomínio", | |
| placeholder: "exemplo", | |
| }, | |
| fullName: { | |
| label: "Seu Nome", | |
| placeholder: "Seu nome completo", | |
| }, | |
| }, | |
| success: "Obrigado, tudo pronto!", | |
| error: "Algo deu errado, por favor tente novamente.", | |
| unavailable: "Indisponível", | |
| check_availability: "Verificando disponibilidade", | |
| available: "Disponível", | |
| }, | |
| overview: { | |
| title: "Visão Geral", | |
| framework_chart: { | |
| title: "Progresso do Framework", | |
| }, | |
| requirement_chart: { | |
| title: "Status de Conformidade", | |
| }, | |
| }, | |
| policies: { | |
| dashboard: { | |
| title: "Painel", | |
| all: "Todas as Políticas", | |
| policy_status: "Política por Status", | |
| policies_by_assignee: "Políticas por Responsável", | |
| policies_by_framework: "Políticas por Framework", | |
| sub_pages: { | |
| overview: "Visão Geral", | |
| edit_policy: "Editar Política", | |
| }, | |
| }, | |
| table: { | |
| name: "Nome da Política", | |
| statuses: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| archived: "Arquivado", | |
| }, | |
| filters: { | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável", | |
| }, | |
| }, | |
| }, | |
| filters: { | |
| search: "Pesquisar políticas...", | |
| all: "Todas as Políticas", | |
| }, | |
| status: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| archived: "Arquivado", | |
| }, | |
| policies: "políticas", | |
| title: "Políticas", | |
| create_new: "Criar nova política", | |
| search_placeholder: "Pesquisar políticas...", | |
| status_filter: "Filtrar por status", | |
| all_statuses: "Todos os status", | |
| no_policies_title: "Nenhuma política ainda", | |
| no_policies_description: "Comece criando sua primeira política", | |
| create_first: "Criar primeira política", | |
| no_description: "Nenhuma descrição fornecida", | |
| last_updated: "Última atualização: {{date}}", | |
| save: "Salvar", | |
| saving: "Salvando...", | |
| saved_success: "Política salva com sucesso", | |
| saved_error: "Falha ao salvar a política", | |
| overview: { | |
| title: "Visão Geral da Política", | |
| form: { | |
| update_policy: "Atualizar Política", | |
| update_policy_description: | |
| "Atualize o título ou a descrição da política.", | |
| update_policy_success: "Política atualizada com sucesso", | |
| update_policy_error: "Falha ao atualizar a política", | |
| update_policy_title: "Nome da Política", | |
| review_frequency: "Frequência de Revisão", | |
| review_frequency_placeholder: "Selecione uma frequência de revisão", | |
| review_date: "Data da Revisão", | |
| review_date_placeholder: "Selecione uma data de revisão", | |
| }, | |
| }, | |
| }, | |
| evidence_tasks: { | |
| evidence_tasks: "Tarefas de Evidência", | |
| overview: "Visão Geral", | |
| }, | |
| risk: { | |
| risks: "riscos", | |
| overview: "Visão Geral", | |
| create: "Criar Novo Risco", | |
| vendor: { | |
| title: "Gestão de Fornecedores", | |
| dashboard: { | |
| title: "Painel de Fornecedores", | |
| overview: "Visão Geral dos Fornecedores", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: | |
| "Nível de risco inicial antes da aplicação de quaisquer controles", | |
| residual_risk_description: | |
| "Nível de risco restante após a aplicação de controles", | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Responsável", | |
| }, | |
| }, | |
| assessment: { | |
| title: "Avaliação de Fornecedor", | |
| update_success: | |
| "Avaliação de risco do fornecedor atualizada com sucesso", | |
| update_error: "Falha ao atualizar a avaliação de risco do fornecedor", | |
| inherent_risk: "Risco Inerente", | |
| residual_risk: "Risco Residual", | |
| }, | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Insira o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Insira o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Insira a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor: "Atualizar Fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| add_comment: "Adicionar Comentário", | |
| }, | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário", | |
| }, | |
| filters: { | |
| search_placeholder: "Pesquisar fornecedores...", | |
| status_placeholder: "Filtrar por status", | |
| category_placeholder: "Filtrar por categoria", | |
| owner_placeholder: "Filtrar por proprietário", | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| actions: { | |
| create: "Criar Fornecedor", | |
| }, | |
| status: { | |
| not_assessed: "Não Avaliado", | |
| in_progress: "Em Andamento", | |
| assessed: "Avaliado", | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "Software como Serviço", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro", | |
| }, | |
| risk_level: { | |
| low: "Baixo Risco", | |
| medium: "Risco Médio", | |
| high: "Alto Risco", | |
| unknown: "Risco Desconhecido", | |
| }, | |
| }, | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Risco", | |
| risk_status: "Status do Risco", | |
| risks_by_department: "Riscos por Departamento", | |
| risks_by_assignee: "Riscos por Responsável", | |
| inherent_risk_description: | |
| "O risco inerente é calculado como probabilidade * impacto. Calculado antes de qualquer controle ser aplicado.", | |
| residual_risk_description: | |
| "O risco residual é calculado como probabilidade * impacto. Este é o nível de risco após a aplicação de controles.", | |
| risk_assessment_description: | |
| "Compare os níveis de risco inerente e residual", | |
| }, | |
| register: { | |
| title: "Registro de Risco", | |
| table: { | |
| risk: "Risco", | |
| }, | |
| empty: { | |
| no_risks: { | |
| title: "Crie um risco para começar", | |
| description: | |
| "Acompanhe e classifique riscos, crie e atribua tarefas de mitigação para sua equipe e gerencie seu registro de risco tudo em uma interface simples.", | |
| }, | |
| create_risk: "Criar um risco", | |
| }, | |
| }, | |
| metrics: { | |
| probability: "Probabilidade", | |
| impact: "Impacto", | |
| inherentRisk: "Risco Inerente", | |
| residualRisk: "Risco Residual", | |
| }, | |
| form: { | |
| update_inherent_risk: "Salvar Risco Inerente", | |
| update_inherent_risk_description: | |
| "Atualize o risco inerente do risco. Este é o nível de risco antes de qualquer controle ser aplicado.", | |
| update_inherent_risk_success: "Risco inerente atualizado com sucesso", | |
| update_inherent_risk_error: "Falha ao atualizar risco inerente", | |
| update_residual_risk: "Salvar Risco Residual", | |
| update_residual_risk_description: | |
| "Atualize o risco residual do risco. Este é o nível de risco após a aplicação de controles.", | |
| update_residual_risk_success: "Risco residual atualizado com sucesso", | |
| update_residual_risk_error: "Falha ao atualizar risco residual", | |
| update_risk: "Atualizar Risco", | |
| update_risk_description: "Atualize o título ou a descrição do risco.", | |
| update_risk_success: "Risco atualizado com sucesso", | |
| update_risk_error: "Falha ao atualizar risco", | |
| create_risk_success: "Risco criado com sucesso", | |
| create_risk_error: "Falha ao criar risco", | |
| risk_details: "Detalhes do Risco", | |
| risk_title: "Título do Risco", | |
| risk_title_description: "Digite um nome para o risco", | |
| risk_description: "Descrição", | |
| risk_description_description: "Digite uma descrição para o risco", | |
| risk_category: "Categoria", | |
| risk_category_placeholder: "Selecione uma categoria", | |
| risk_department: "Departamento", | |
| risk_department_placeholder: "Selecione um departamento", | |
| risk_status: "Status do Risco", | |
| risk_status_placeholder: "Selecione um status de risco", | |
| }, | |
| tasks: { | |
| title: "Tarefas", | |
| attachments: "Anexos", | |
| overview: "Visão Geral da Tarefa", | |
| form: { | |
| title: "Detalhes da Tarefa", | |
| task_title: "Título da Tarefa", | |
| status: "Status da Tarefa", | |
| status_placeholder: "Selecione um status de tarefa", | |
| task_title_description: "Digite um nome para a tarefa", | |
| description: "Descrição", | |
| description_description: "Digite uma descrição para a tarefa", | |
| due_date: "Data de Vencimento", | |
| due_date_description: "Selecione a data de vencimento para a tarefa", | |
| success: "Tarefa criada com sucesso", | |
| error: "Falha ao criar tarefa", | |
| }, | |
| sheet: { | |
| title: "Criar Tarefa", | |
| update: "Atualizar Tarefa", | |
| update_description: "Atualize o título ou a descrição da tarefa.", | |
| }, | |
| empty: { | |
| description_create: | |
| "Crie uma tarefa de mitigação para este risco, adicione um plano de tratamento e atribua a um membro da equipe.", | |
| }, | |
| }, | |
| }, | |
| settings: { | |
| general: { | |
| title: "Geral", | |
| org_name: "Nome da Organização", | |
| org_name_description: | |
| "Este é o nome visível da sua organização. Você deve usar o nome legal da sua organização.", | |
| org_name_tip: "Por favor, use no máximo 32 caracteres.", | |
| org_website: "Website da Organização", | |
| org_website_description: | |
| "Este é o URL do website oficial da sua organização. Certifique-se de incluir o URL completo com https://.", | |
| org_website_tip: "Por favor, insira um URL válido incluindo https://", | |
| org_website_error: "Erro ao atualizar o website da organização", | |
| org_website_updated: "Website da organização atualizado", | |
| org_delete: "Excluir organização", | |
| org_delete_description: | |
| "Remova permanentemente sua organização e todo o seu conteúdo da plataforma Comp AI. Esta ação não é reversível - continue com cautela.", | |
| org_delete_alert_title: "Você tem certeza absoluta?", | |
| org_delete_alert_description: | |
| "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua organização e removerá seus dados de nossos servidores.", | |
| org_delete_error: "Erro ao excluir organização", | |
| org_delete_success: "Organização excluída", | |
| org_name_updated: "Nome da organização atualizado", | |
| org_name_error: "Erro ao atualizar o nome da organização", | |
| save_button: "Salvar", | |
| delete_button: "Excluir", | |
| delete_confirm: "EXCLUIR", | |
| delete_confirm_tip: "Digite EXCLUIR para confirmar.", | |
| cancel_button: "Cancelar", | |
| }, | |
| members: { | |
| title: "Membros", | |
| }, | |
| billing: { | |
| title: "Faturamento", | |
| }, | |
| }, | |
| user_menu: { | |
| theme: "Tema", | |
| language: "Idioma", | |
| sign_out: "Sair", | |
| account: "Conta", | |
| support: "Suporte", | |
| settings: "Configurações", | |
| teams: "Equipes", | |
| }, | |
| frameworks: { | |
| title: "Estruturas", | |
| controls: { | |
| title: "Controles", | |
| description: "Revise e gerencie controles de conformidade", | |
| table: { | |
| status: "Status", | |
| control: "Controle", | |
| artifacts: "Artefatos", | |
| actions: "Ações", | |
| }, | |
| statuses: { | |
| not_started: "Não Iniciado", | |
| completed: "Concluído", | |
| in_progress: "Em Andamento", | |
| }, | |
| }, | |
| overview: { | |
| error: "Falha ao carregar frameworks", | |
| loading: "Carregando frameworks...", | |
| empty: { | |
| title: "Nenhum framework selecionado", | |
| description: | |
| "Selecione frameworks para começar sua jornada de conformidade", | |
| }, | |
| progress: { | |
| title: "Progresso do Framework", | |
| empty: { | |
| title: "Nenhum framework ainda", | |
| description: | |
| "Comece adicionando um framework de conformidade para acompanhar seu progresso", | |
| action: "Adicionar Framework", | |
| }, | |
| }, | |
| grid: { | |
| welcome: { | |
| title: "Bem-vindo ao Comp AI", | |
| description: | |
| "Comece selecionando os frameworks de conformidade que você gostaria de implementar. Nós o ajudaremos a gerenciar e acompanhar sua jornada de conformidade em múltiplos padrões.", | |
| action: "Começar", | |
| }, | |
| title: "Selecionar Frameworks", | |
| version: "Versão", | |
| actions: { | |
| clear: "Limpar", | |
| confirm: "Confirmar Seleção", | |
| }, | |
| }, | |
| }, | |
| }, | |
| vendor: { | |
| title: "Painel", | |
| register_title: "Gerenciamento de Fornecedores", | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Fornecedor", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: | |
| "Nível de risco inicial antes que quaisquer controles sejam aplicados", | |
| residual_risk_description: | |
| "Nível de risco restante após a aplicação de controles", | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário", | |
| }, | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "SaaS", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro", | |
| }, | |
| vendors: "fornecedores", | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Digite o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Digite o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Digite a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| contacts: "Contatos do Fornecedor", | |
| contact_name: "Nome do Contato", | |
| contact_email: "Email do Contato", | |
| contact_role: "Função do Contato", | |
| add_contact: "Adicionar Contato", | |
| new_contact: "Novo Contato", | |
| min_one_contact_required: "Um fornecedor deve ter pelo menos um contato", | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| }, | |
| people: { | |
| title: "Pessoas", | |
| details: { | |
| taskProgress: "Progresso da Tarefa", | |
| tasks: "Tarefas", | |
| noTasks: "Nenhuma tarefa atribuída ainda", | |
| }, | |
| description: "Gerencie os membros da sua equipe e seus papéis.", | |
| filters: { | |
| search: "Pesquisar pessoas...", | |
| role: "Filtrar por papel", | |
| }, | |
| actions: { | |
| invite: "Adicionar Funcionário", | |
| clear: "Limpar filtros", | |
| }, | |
| table: { | |
| name: "Nome", | |
| email: "Email", | |
| department: "Departamento", | |
| externalId: "ID Externo", | |
| }, | |
| empty: { | |
| no_employees: { | |
| title: "Nenhum funcionário ainda", | |
| description: "Comece convidando seu primeiro membro da equipe.", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum funcionário corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| invite: { | |
| title: "Adicionar Funcionário", | |
| description: "Adicione um funcionário à sua organização.", | |
| email: { | |
| label: "Endereço de email", | |
| placeholder: "Digite o endereço de email", | |
| }, | |
| role: { | |
| label: "Função", | |
| placeholder: "Selecione uma função", | |
| }, | |
| name: { | |
| label: "Nome", | |
| placeholder: "Digite o nome", | |
| }, | |
| department: { | |
| label: "Departamento", | |
| placeholder: "Selecione um departamento", | |
| }, | |
| submit: "Adicionar Funcionário", | |
| success: "Funcionário adicionado com sucesso", | |
| error: "Falha ao adicionar funcionário", | |
| }, | |
| }, | |
| errors: { | |
| unexpected: "Algo deu errado, por favor tente novamente", | |
| }, | |
| sub_pages: { | |
| risk: { | |
| overview: "Gestão de Riscos", | |
| register: "Registro de Riscos", | |
| risk_overview: "Visão Geral dos Riscos", | |
| risk_comments: "Comentários sobre Riscos", | |
| tasks: { | |
| task_overview: "Visão Geral da Tarefa", | |
| }, | |
| }, | |
| policies: { | |
| all: "Todas as Políticas", | |
| editor: "Editor de Políticas", | |
| policy_details: "Detalhes da política", | |
| }, | |
| people: { | |
| all: "Pessoas", | |
| employee_details: "Detalhes do Funcionário", | |
| }, | |
| settings: { | |
| members: "Membros da Equipe", | |
| }, | |
| frameworks: { | |
| overview: "Estruturas", | |
| }, | |
| evidence: { | |
| title: "Evidência", | |
| list: "Lista de Evidências", | |
| overview: "Visão Geral das Evidências", | |
| }, | |
| }, | |
| editor: { | |
| ai: { | |
| thinking: "A IA está pensando", | |
| thinking_spinner: "A IA está pensando", | |
| edit_or_generate: "Editar ou gerar...", | |
| tell_ai_what_to_do_next: "Diga à IA o que fazer a seguir", | |
| request_limit_reached: | |
| "Você atingiu seu limite de solicitações para o dia.", | |
| }, | |
| ai_selector: { | |
| improve: "Melhorar a escrita", | |
| fix: "Corrigir gramática", | |
| shorter: "Tornar mais curto", | |
| longer: "Tornar mais longo", | |
| continue: "Continuar escrevendo", | |
| replace: "Substituir seleção", | |
| insert: "Inserir abaixo", | |
| discard: "Descartar", | |
| }, | |
| }, | |
| evidence: { | |
| title: "Tarefas de Evidência", | |
| list: "Todas as Evidências", | |
| }, | |
| upload: { | |
| fileSection: { | |
| filesUploaded: "{{count}} arquivo{{s}} enviado", | |
| upload: "{{count}} arquivo{{s}} enviado", | |
| }, | |
| fileUpload: { | |
| uploadingText: "Enviando...", | |
| dropFileHere: "Solte o arquivo aqui", | |
| releaseToUpload: "Solte para enviar", | |
| addFiles: "Adicionar Arquivos", | |
| uploadAdditionalEvidence: "Enviar arquivos de evidência adicionais", | |
| dragDropOrClick: "Arraste e solte ou clique para enviar", | |
| dropFileHereAlt: "Solte o arquivo aqui", | |
| dragDropOrClickToSelect: | |
| "Arraste e solte um arquivo aqui ou clique para selecionar", | |
| maxFileSize: "Tamanho máximo do arquivo: {{size}}MB", | |
| uploadingFile: "Enviando arquivo...", | |
| }, | |
| fileCard: { | |
| preview: "Pré-visualização", | |
| previewNotAvailable: | |
| "Pré-visualização não disponível. Clique no botão de download para visualizar o arquivo.", | |
| filePreview: "Pré-visualização do arquivo: {fileName}", | |
| openFile: "Abrir arquivo", | |
| deleteFile: "Excluir arquivo", | |
| deleteFileConfirmTitle: "Excluir Arquivo", | |
| deleteFileConfirmDescription: | |
| "Você tem certeza de que deseja excluir este arquivo? Esta ação não pode ser desfeita.", | |
| }, | |
| fileUrl: { | |
| additionalLinks: "Links Adicionais", | |
| add: "Adicionar", | |
| linksAdded: "{count} link{s} adicionado", | |
| enterUrl: "Digite a URL", | |
| addAnotherLink: "Adicionar Outro Link", | |
| saveLinks: "Salvar Links", | |
| urlBadge: "URL", | |
| copyLink: "Copiar link", | |
| openLink: "Abrir link", | |
| deleteLink: "Excluir link", | |
| }, | |
| }, | |
| languages: { | |
| export default { | |
| languages: { | |
| es: "Espanhol", | |
| fr: "Francês", | |
| no: "Norueguês", | |
| pt: "Português", | |
| en: "Inglês", | |
| }, | |
| language: { | |
| title: "Idiomas", | |
| description: "Altere o idioma usado na interface do usuário.", | |
| placeholder: "Selecione o idioma", | |
| }, | |
| common: { | |
| actions: { | |
| save: "Salvar", | |
| edit: "Editar", | |
| delete: "Excluir", | |
| cancel: "Cancelar", | |
| clear: "Limpar", | |
| create: "Criar", | |
| send: "Enviar", | |
| return: "Retornar", | |
| success: "Sucesso", | |
| error: "Erro", | |
| next: "Próximo", | |
| complete: "Completo", | |
| addNew: "Adicionar Novo", | |
| }, | |
| assignee: { | |
| label: "Responsável", | |
| placeholder: "Selecione o responsável", | |
| }, | |
| date: { | |
| pick: "Escolher uma data", | |
| due_date: "Data de Vencimento", | |
| }, | |
| status: { | |
| open: "Aberto", | |
| pending: "Pendente", | |
| closed: "Fechado", | |
| archived: "Arquivado", | |
| compliant: "Conforme", | |
| non_compliant: "Não Conforme", | |
| not_started: "Não Iniciado", | |
| in_progress: "Em Andamento", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| draft: "Rascunho", | |
| not_assessed: "Não Avaliado", | |
| assessed: "Avaliado", | |
| active: "Ativo", | |
| inactive: "Inativo", | |
| title: "Status", | |
| }, | |
| filters: { | |
| clear: "Limpar filtros", | |
| search: "Pesquisar...", | |
| status: "Status", | |
| department: "Departamento", | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável", | |
| }, | |
| }, | |
| table: { | |
| title: "Título", | |
| status: "Status", | |
| assigned_to: "Atribuído A", | |
| due_date: "Data de Vencimento", | |
| last_updated: "Última Atualização", | |
| no_results: "Nenhum resultado encontrado", | |
| }, | |
| empty_states: { | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| title_tasks: "Nenhuma tarefa encontrada", | |
| title_risks: "Nenhum risco encontrado", | |
| description: "Tente outra pesquisa ou ajuste os filtros", | |
| description_filters: "Tente outra pesquisa ou ajuste os filtros", | |
| description_no_tasks: "Crie uma tarefa para começar", | |
| description_no_risks: "Crie um risco para começar", | |
| }, | |
| no_items: { | |
| title: "Nenhum item encontrado", | |
| description: "Tente ajustar sua pesquisa ou filtros", | |
| }, | |
| }, | |
| pagination: { | |
| of: "de", | |
| items_per_page: "Itens por página", | |
| rows_per_page: "Linhas por página", | |
| page_x_of_y: "Página {{current}} de {{total}}", | |
| go_to_first_page: "Ir para a primeira página", | |
| go_to_previous_page: "Ir para a página anterior", | |
| go_to_next_page: "Ir para a próxima página", | |
| go_to_last_page: "Ir para a última página", | |
| }, | |
| comments: { | |
| title: "Comentários", | |
| description: "Adicione um comentário usando o formulário abaixo.", | |
| add: "Novo Comentário", | |
| new: "Novo Comentário", | |
| save: "Salvar Comentário", | |
| success: "Comentário adicionado com sucesso", | |
| error: "Falha ao adicionar comentário", | |
| placeholder: "Escreva seu comentário aqui...", | |
| empty: { | |
| title: "Nenhum comentário ainda", | |
| description: "Seja o primeiro a adicionar um comentário", | |
| }, | |
| }, | |
| attachments: { | |
| title: "Anexos", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | |
| upload: "Enviar anexo", | |
| upload_description: | |
| "Carregue um anexo ou adicione um link para um recurso externo.", | |
| drop: "Solte os arquivos aqui", | |
| drop_description: | |
| "Solte arquivos aqui ou clique para escolher arquivos do seu dispositivo.", | |
| drop_files_description: "Os arquivos podem ter até ", | |
| empty: { | |
| title: "Nenhum anexo", | |
| description: "Adicione um arquivo clicando em 'Adicionar Anexo'.", | |
| }, | |
| toasts: { | |
| error: "Algo deu errado, por favor tente novamente.", | |
| error_uploading_files: | |
| "Não é possível carregar mais de 1 arquivo por vez", | |
| error_uploading_files_multiple: | |
| "Não é possível carregar mais de 10 arquivos", | |
| error_no_files_selected: "Nenhum arquivo selecionado", | |
| error_file_rejected: "Arquivo {file} foi rejeitado", | |
| error_failed_to_upload_files: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_multiple: "Falha ao carregar arquivos", | |
| error_failed_to_upload_files_single: "Falha ao carregar arquivo", | |
| success_uploading_files: "Arquivos carregados com sucesso", | |
| success_uploading_files_multiple: "Arquivos carregados com sucesso", | |
| success_uploading_files_single: "Arquivo carregado com sucesso", | |
| success_uploading_files_target: "Arquivos carregados", | |
| uploading_files: "Carregando {target}...", | |
| remove_file: "Remover arquivo", | |
| }, | |
| }, | |
| notifications: { | |
| inbox: "Caixa de entrada", | |
| archive: "Arquivo", | |
| archive_all: "Arquivar tudo", | |
| no_notifications: "Sem novas notificações", | |
| }, | |
| edit: "Editar", | |
| errors: { | |
| unexpected_error: "Ocorreu um erro inesperado", | |
| }, | |
| description: "Descrição", | |
| last_updated: "Última atualização", | |
| frequency: { | |
| daily: "Diário", | |
| weekly: "Semanal", | |
| monthly: "Mensal", | |
| quarterly: "Trimestral", | |
| yearly: "Anual", | |
| }, | |
| }, | |
| header: { | |
| discord: { | |
| button: "Junte-se a nós no Discord", | |
| }, | |
| feedback: { | |
| button: "Feedback", | |
| title: "Obrigado pelo seu feedback!", | |
| description: "Voltaremos a você assim que possível", | |
| placeholder: | |
| "Ideias para melhorar esta página ou problemas que você está enfrentando.", | |
| success: "Obrigado pelo seu feedback!", | |
| error: "Erro ao enviar feedback - tentar novamente?", | |
| send: "Enviar Feedback", | |
| }, | |
| }, | |
| not_found: { | |
| title: "404 - Página não encontrada", | |
| description: "A página que você está procurando não existe.", | |
| return: "Retornar ao painel", | |
| }, | |
| theme: { | |
| options: { | |
| light: "Claro", | |
| dark: "Escuro", | |
| system: "Sistema", | |
| }, | |
| }, | |
| sidebar: { | |
| overview: "Visão Geral", | |
| policies: "Políticas", | |
| risk: "Gestão de Riscos", | |
| vendors: "Fornecedores", | |
| integrations: "Integrações", | |
| settings: "Configurações", | |
| evidence: "Tarefas de Evidência", | |
| people: "Pessoas", | |
| }, | |
| auth: { | |
| title: "Automatize a conformidade com SOC 2, ISO 27001 e GDPR com IA.", | |
| description: | |
| "Crie uma conta gratuita ou faça login com uma conta existente para continuar.", | |
| options: "Mais opções", | |
| google: "Continuar com o Google", | |
| email: { | |
| description: "Insira seu endereço de e-mail para continuar.", | |
| placeholder: "Insira o endereço de e-mail", | |
| button: "Continuar com e-mail", | |
| magic_link_sent: "Link mágico enviado", | |
| magic_link_description: | |
| "Verifique sua caixa de entrada para um link mágico.", | |
| magic_link_try_again: "Tente novamente.", | |
| success: "E-mail enviado - verifique sua caixa de entrada!", | |
| error: "Erro ao enviar e-mail - tentar novamente?", | |
| }, | |
| terms: | |
| "Ao clicar em continuar, você reconhece que leu e concorda com os Termos de Serviço e a Política de Privacidade.", | |
| }, | |
| onboarding: { | |
| title: "Criar uma organização", | |
| setup: "Configuração", | |
| description: "Conte-nos um pouco sobre sua organização.", | |
| fields: { | |
| name: { | |
| label: "Nome da Organização", | |
| placeholder: "O nome da sua organização", | |
| }, | |
| website: { | |
| label: "Website", | |
| placeholder: "O website da sua organização", | |
| }, | |
| subdomain: { | |
| label: "Subdomínio", | |
| placeholder: "exemplo", | |
| }, | |
| fullName: { | |
| label: "Seu Nome", | |
| placeholder: "Seu nome completo", | |
| }, | |
| }, | |
| success: "Obrigado, tudo pronto!", | |
| error: "Algo deu errado, por favor tente novamente.", | |
| unavailable: "Indisponível", | |
| check_availability: "Verificando disponibilidade", | |
| available: "Disponível", | |
| }, | |
| overview: { | |
| title: "Visão Geral", | |
| framework_chart: { | |
| title: "Progresso do Framework", | |
| }, | |
| requirement_chart: { | |
| title: "Status de Conformidade", | |
| }, | |
| }, | |
| policies: { | |
| dashboard: { | |
| title: "Painel", | |
| all: "Todas as Políticas", | |
| policy_status: "Política por Status", | |
| policies_by_assignee: "Políticas por Responsável", | |
| policies_by_framework: "Políticas por Framework", | |
| sub_pages: { | |
| overview: "Visão Geral", | |
| edit_policy: "Editar Política", | |
| }, | |
| }, | |
| table: { | |
| name: "Nome da Política", | |
| statuses: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| archived: "Arquivado", | |
| }, | |
| filters: { | |
| owner: { | |
| label: "Responsável", | |
| placeholder: "Filtrar por responsável", | |
| }, | |
| }, | |
| }, | |
| filters: { | |
| search: "Pesquisar políticas...", | |
| all: "Todas as Políticas", | |
| }, | |
| status: { | |
| draft: "Rascunho", | |
| published: "Publicado", | |
| needs_review: "Necessita Revisão", | |
| archived: "Arquivado", | |
| }, | |
| policies: "políticas", | |
| title: "Políticas", | |
| create_new: "Criar nova política", | |
| search_placeholder: "Pesquisar políticas...", | |
| status_filter: "Filtrar por status", | |
| all_statuses: "Todos os status", | |
| no_policies_title: "Nenhuma política ainda", | |
| no_policies_description: "Comece criando sua primeira política", | |
| create_first: "Criar primeira política", | |
| no_description: "Nenhuma descrição fornecida", | |
| last_updated: "Última atualização: {{date}}", | |
| save: "Salvar", | |
| saving: "Salvando...", | |
| saved_success: "Política salva com sucesso", | |
| saved_error: "Falha ao salvar a política", | |
| overview: { | |
| title: "Visão Geral da Política", | |
| form: { | |
| update_policy: "Atualizar Política", | |
| update_policy_description: | |
| "Atualize o título ou a descrição da política.", | |
| update_policy_success: "Política atualizada com sucesso", | |
| update_policy_error: "Falha ao atualizar a política", | |
| update_policy_title: "Nome da Política", | |
| review_frequency: "Frequência de Revisão", | |
| review_frequency_placeholder: "Selecione uma frequência de revisão", | |
| review_date: "Data da Revisão", | |
| review_date_placeholder: "Selecione uma data de revisão", | |
| }, | |
| }, | |
| }, | |
| evidence_tasks: { | |
| evidence_tasks: "Tarefas de Evidência", | |
| overview: "Visão Geral", | |
| }, | |
| risk: { | |
| risks: "riscos", | |
| overview: "Visão Geral", | |
| create: "Criar Novo Risco", | |
| vendor: { | |
| title: "Gestão de Fornecedores", | |
| dashboard: { | |
| title: "Painel de Fornecedores", | |
| overview: "Visão Geral dos Fornecedores", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: | |
| "Nível de risco inicial antes da aplicação de quaisquer controles", | |
| residual_risk_description: | |
| "Nível de risco restante após a aplicação de controles", | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Responsável", | |
| }, | |
| }, | |
| assessment: { | |
| title: "Avaliação de Fornecedor", | |
| update_success: | |
| "Avaliação de risco do fornecedor atualizada com sucesso", | |
| update_error: "Falha ao atualizar a avaliação de risco do fornecedor", | |
| inherent_risk: "Risco Inerente", | |
| residual_risk: "Risco Residual", | |
| }, | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Insira o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Insira o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Insira a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor: "Atualizar Fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| add_comment: "Adicionar Comentário", | |
| }, | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário", | |
| }, | |
| filters: { | |
| search_placeholder: "Pesquisar fornecedores...", | |
| status_placeholder: "Filtrar por status", | |
| category_placeholder: "Filtrar por categoria", | |
| owner_placeholder: "Filtrar por proprietário", | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| actions: { | |
| create: "Criar Fornecedor", | |
| }, | |
| status: { | |
| not_assessed: "Não Avaliado", | |
| in_progress: "Em Andamento", | |
| assessed: "Avaliado", | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "Software como Serviço", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro", | |
| }, | |
| risk_level: { | |
| low: "Baixo Risco", | |
| medium: "Risco Médio", | |
| high: "Alto Risco", | |
| unknown: "Risco Desconhecido", | |
| }, | |
| }, | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Risco", | |
| risk_status: "Status do Risco", | |
| risks_by_department: "Riscos por Departamento", | |
| risks_by_assignee: "Riscos por Responsável", | |
| inherent_risk_description: | |
| "O risco inerente é calculado como probabilidade * impacto. Calculado antes de qualquer controle ser aplicado.", | |
| residual_risk_description: | |
| "O risco residual é calculado como probabilidade * impacto. Este é o nível de risco após a aplicação de controles.", | |
| risk_assessment_description: | |
| "Compare os níveis de risco inerente e residual", | |
| }, | |
| register: { | |
| title: "Registro de Risco", | |
| table: { | |
| risk: "Risco", | |
| }, | |
| empty: { | |
| no_risks: { | |
| title: "Crie um risco para começar", | |
| description: | |
| "Acompanhe e classifique riscos, crie e atribua tarefas de mitigação para sua equipe e gerencie seu registro de risco tudo em uma interface simples.", | |
| }, | |
| create_risk: "Criar um risco", | |
| }, | |
| }, | |
| metrics: { | |
| probability: "Probabilidade", | |
| impact: "Impacto", | |
| inherentRisk: "Risco Inerente", | |
| residualRisk: "Risco Residual", | |
| }, | |
| form: { | |
| update_inherent_risk: "Salvar Risco Inerente", | |
| update_inherent_risk_description: | |
| "Atualize o risco inerente do risco. Este é o nível de risco antes de qualquer controle ser aplicado.", | |
| update_inherent_risk_success: "Risco inerente atualizado com sucesso", | |
| update_inherent_risk_error: "Falha ao atualizar risco inerente", | |
| update_residual_risk: "Salvar Risco Residual", | |
| update_residual_risk_description: | |
| "Atualize o risco residual do risco. Este é o nível de risco após a aplicação de controles.", | |
| update_residual_risk_success: "Risco residual atualizado com sucesso", | |
| update_residual_risk_error: "Falha ao atualizar risco residual", | |
| update_risk: "Atualizar Risco", | |
| update_risk_description: "Atualize o título ou a descrição do risco.", | |
| update_risk_success: "Risco atualizado com sucesso", | |
| update_risk_error: "Falha ao atualizar risco", | |
| create_risk_success: "Risco criado com sucesso", | |
| create_risk_error: "Falha ao criar risco", | |
| risk_details: "Detalhes do Risco", | |
| risk_title: "Título do Risco", | |
| risk_title_description: "Digite um nome para o risco", | |
| risk_description: "Descrição", | |
| risk_description_description: "Digite uma descrição para o risco", | |
| risk_category: "Categoria", | |
| risk_category_placeholder: "Selecione uma categoria", | |
| risk_department: "Departamento", | |
| risk_department_placeholder: "Selecione um departamento", | |
| risk_status: "Status do Risco", | |
| risk_status_placeholder: "Selecione um status de risco", | |
| }, | |
| tasks: { | |
| title: "Tarefas", | |
| attachments: "Anexos", | |
| overview: "Visão Geral da Tarefa", | |
| form: { | |
| title: "Detalhes da Tarefa", | |
| task_title: "Título da Tarefa", | |
| status: "Status da Tarefa", | |
| status_placeholder: "Selecione um status de tarefa", | |
| task_title_description: "Digite um nome para a tarefa", | |
| description: "Descrição", | |
| description_description: "Digite uma descrição para a tarefa", | |
| due_date: "Data de Vencimento", | |
| due_date_description: "Selecione a data de vencimento para a tarefa", | |
| success: "Tarefa criada com sucesso", | |
| error: "Falha ao criar tarefa", | |
| }, | |
| sheet: { | |
| title: "Criar Tarefa", | |
| update: "Atualizar Tarefa", | |
| update_description: "Atualize o título ou a descrição da tarefa.", | |
| }, | |
| empty: { | |
| description_create: | |
| "Crie uma tarefa de mitigação para este risco, adicione um plano de tratamento e atribua a um membro da equipe.", | |
| }, | |
| }, | |
| }, | |
| settings: { | |
| general: { | |
| title: "Geral", | |
| org_name: "Nome da Organização", | |
| org_name_description: | |
| "Este é o nome visível da sua organização. Você deve usar o nome legal da sua organização.", | |
| org_name_tip: "Por favor, use no máximo 32 caracteres.", | |
| org_website: "Website da Organização", | |
| org_website_description: | |
| "Este é o URL do website oficial da sua organização. Certifique-se de incluir o URL completo com https://.", | |
| org_website_tip: "Por favor, insira um URL válido incluindo https://", | |
| org_website_error: "Erro ao atualizar o website da organização", | |
| org_website_updated: "Website da organização atualizado", | |
| org_delete: "Excluir organização", | |
| org_delete_description: | |
| "Remova permanentemente sua organização e todo o seu conteúdo da plataforma Comp AI. Esta ação não é reversível - continue com cautela.", | |
| org_delete_alert_title: "Você tem certeza absoluta?", | |
| org_delete_alert_description: | |
| "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua organização e removerá seus dados de nossos servidores.", | |
| org_delete_error: "Erro ao excluir organização", | |
| org_delete_success: "Organização excluída", | |
| org_name_updated: "Nome da organização atualizado", | |
| org_name_error: "Erro ao atualizar o nome da organização", | |
| save_button: "Salvar", | |
| delete_button: "Excluir", | |
| delete_confirm: "EXCLUIR", | |
| delete_confirm_tip: "Digite EXCLUIR para confirmar.", | |
| cancel_button: "Cancelar", | |
| }, | |
| members: { | |
| title: "Membros", | |
| }, | |
| billing: { | |
| title: "Faturamento", | |
| }, | |
| }, | |
| user_menu: { | |
| theme: "Tema", | |
| language: "Idioma", | |
| sign_out: "Sair", | |
| account: "Conta", | |
| support: "Suporte", | |
| settings: "Configurações", | |
| teams: "Equipes", | |
| }, | |
| frameworks: { | |
| title: "Estruturas", | |
| controls: { | |
| title: "Controles", | |
| description: "Revise e gerencie controles de conformidade", | |
| table: { | |
| status: "Status", | |
| control: "Controle", | |
| artifacts: "Artefatos", | |
| actions: "Ações", | |
| }, | |
| statuses: { | |
| not_started: "Não Iniciado", | |
| completed: "Concluído", | |
| in_progress: "Em Andamento", | |
| }, | |
| }, | |
| overview: { | |
| error: "Falha ao carregar frameworks", | |
| loading: "Carregando frameworks...", | |
| empty: { | |
| title: "Nenhum framework selecionado", | |
| description: | |
| "Selecione frameworks para começar sua jornada de conformidade", | |
| }, | |
| progress: { | |
| title: "Progresso do Framework", | |
| empty: { | |
| title: "Nenhum framework ainda", | |
| description: | |
| "Comece adicionando um framework de conformidade para acompanhar seu progresso", | |
| action: "Adicionar Framework", | |
| }, | |
| }, | |
| grid: { | |
| welcome: { | |
| title: "Bem-vindo ao Comp AI", | |
| description: | |
| "Comece selecionando os frameworks de conformidade que você gostaria de implementar. Nós o ajudaremos a gerenciar e acompanhar sua jornada de conformidade em múltiplos padrões.", | |
| action: "Começar", | |
| }, | |
| title: "Selecionar Frameworks", | |
| version: "Versão", | |
| actions: { | |
| clear: "Limpar", | |
| confirm: "Confirmar Seleção", | |
| }, | |
| }, | |
| }, | |
| }, | |
| vendor: { | |
| title: "Painel", | |
| register_title: "Gerenciamento de Fornecedores", | |
| dashboard: { | |
| title: "Painel", | |
| overview: "Visão Geral do Fornecedor", | |
| vendor_status: "Status do Fornecedor", | |
| vendor_category: "Categorias de Fornecedor", | |
| vendors_by_assignee: "Fornecedores por Responsável", | |
| inherent_risk_description: | |
| "Nível de risco inicial antes que quaisquer controles sejam aplicados", | |
| residual_risk_description: | |
| "Nível de risco restante após a aplicação de controles", | |
| }, | |
| register: { | |
| title: "Registro de Fornecedor", | |
| table: { | |
| name: "Nome", | |
| category: "Categoria", | |
| status: "Status", | |
| owner: "Proprietário", | |
| }, | |
| }, | |
| category: { | |
| cloud: "Nuvem", | |
| infrastructure: "Infraestrutura", | |
| software_as_a_service: "SaaS", | |
| finance: "Finanças", | |
| marketing: "Marketing", | |
| sales: "Vendas", | |
| hr: "RH", | |
| other: "Outro", | |
| }, | |
| vendors: "fornecedores", | |
| form: { | |
| vendor_details: "Detalhes do Fornecedor", | |
| vendor_name: "Nome", | |
| vendor_name_placeholder: "Digite o nome do fornecedor", | |
| vendor_website: "Website", | |
| vendor_website_placeholder: "Digite o website do fornecedor", | |
| vendor_description: "Descrição", | |
| vendor_description_placeholder: "Digite a descrição do fornecedor", | |
| vendor_category: "Categoria", | |
| vendor_category_placeholder: "Selecione a categoria", | |
| vendor_status: "Status", | |
| vendor_status_placeholder: "Selecione o status", | |
| create_vendor_success: "Fornecedor criado com sucesso", | |
| create_vendor_error: "Falha ao criar fornecedor", | |
| update_vendor_success: "Fornecedor atualizado com sucesso", | |
| update_vendor_error: "Falha ao atualizar fornecedor", | |
| contacts: "Contatos do Fornecedor", | |
| contact_name: "Nome do Contato", | |
| contact_email: "Email do Contato", | |
| contact_role: "Função do Contato", | |
| add_contact: "Adicionar Contato", | |
| new_contact: "Novo Contato", | |
| min_one_contact_required: "Um fornecedor deve ter pelo menos um contato", | |
| }, | |
| empty_states: { | |
| no_vendors: { | |
| title: "Nenhum fornecedor ainda", | |
| description: "Comece criando seu primeiro fornecedor", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum fornecedor corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| }, | |
| people: { | |
| title: "Pessoas", | |
| details: { | |
| taskProgress: "Progresso da Tarefa", | |
| tasks: "Tarefas", | |
| noTasks: "Nenhuma tarefa atribuída ainda", | |
| }, | |
| description: "Gerencie os membros da sua equipe e seus papéis.", | |
| filters: { | |
| search: "Pesquisar pessoas...", | |
| role: "Filtrar por papel", | |
| }, | |
| actions: { | |
| invite: "Adicionar Funcionário", | |
| clear: "Limpar filtros", | |
| }, | |
| table: { | |
| name: "Nome", | |
| email: "Email", | |
| department: "Departamento", | |
| externalId: "ID Externo", | |
| }, | |
| empty: { | |
| no_employees: { | |
| title: "Nenhum funcionário ainda", | |
| description: "Comece convidando seu primeiro membro da equipe.", | |
| }, | |
| no_results: { | |
| title: "Nenhum resultado encontrado", | |
| description: "Nenhum funcionário corresponde à sua pesquisa", | |
| description_with_filters: "Tente ajustar seus filtros", | |
| }, | |
| }, | |
| invite: { | |
| title: "Adicionar Funcionário", | |
| description: "Adicione um funcionário à sua organização.", | |
| email: { | |
| label: "Endereço de email", | |
| placeholder: "Digite o endereço de email", | |
| }, | |
| role: { | |
| label: "Função", | |
| placeholder: "Selecione uma função", | |
| }, | |
| name: { | |
| label: "Nome", | |
| placeholder: "Digite o nome", | |
| }, | |
| department: { | |
| label: "Departamento", | |
| placeholder: "Selecione um departamento", | |
| }, | |
| submit: "Adicionar Funcionário", | |
| success: "Funcionário adicionado com sucesso", | |
| error: "Falha ao adicionar funcionário", | |
| }, | |
| }, | |
| errors: { | |
| unexpected: "Algo deu errado, por favor tente novamente", | |
| }, | |
| sub_pages: { | |
| risk: { | |
| overview: "Gestão de Riscos", | |
| register: "Registro de Riscos", | |
| risk_overview: "Visão Geral dos Riscos", | |
| risk_comments: "Comentários sobre Riscos", | |
| tasks: { | |
| task_overview: "Visão Geral da Tarefa", | |
| }, | |
| }, | |
| policies: { | |
| all: "Todas as Políticas", | |
| editor: "Editor de Políticas", | |
| policy_details: "Detalhes da política", | |
| }, | |
| people: { | |
| all: "Pessoas", | |
| employee_details: "Detalhes do Funcionário", | |
| }, | |
| settings: { | |
| members: "Membros da Equipe", | |
| }, | |
| frameworks: { | |
| overview: "Estruturas", | |
| }, | |
| evidence: { | |
| title: "Evidência", | |
| list: "Lista de Evidências", | |
| overview: "Visão Geral das Evidências", | |
| }, | |
| }, | |
| editor: { | |
| ai: { | |
| thinking: "A IA está pensando", | |
| thinking_spinner: "A IA está pensando", | |
| edit_or_generate: "Editar ou gerar...", | |
| tell_ai_what_to_do_next: "Diga à IA o que fazer a seguir", | |
| request_limit_reached: | |
| "Você atingiu seu limite de solicitações para o dia.", | |
| }, | |
| ai_selector: { | |
| improve: "Melhorar a escrita", | |
| fix: "Corrigir gramática", | |
| shorter: "Tornar mais curto", | |
| longer: "Tornar mais longo", | |
| continue: "Continuar escrevendo", | |
| replace: "Substituir seleção", | |
| insert: "Inserir abaixo", | |
| discard: "Descartar", | |
| }, | |
| }, | |
| evidence: { | |
| title: "Tarefas de Evidência", | |
| list: "Todas as Evidências", | |
| }, | |
| upload: { | |
| fileSection: { | |
| filesUploaded: "{{count}} arquivo{{s}} enviado", | |
| upload: "{{count}} arquivo{{s}} enviado", | |
| }, | |
| fileUpload: { | |
| uploadingText: "Enviando...", | |
| dropFileHere: "Solte o arquivo aqui", | |
| releaseToUpload: "Solte para enviar", | |
| addFiles: "Adicionar Arquivos", | |
| uploadAdditionalEvidence: "Enviar arquivos de evidência adicionais", | |
| dragDropOrClick: "Arraste e solte ou clique para enviar", | |
| dropFileHereAlt: "Solte o arquivo aqui", | |
| dragDropOrClickToSelect: | |
| "Arraste e solte um arquivo aqui ou clique para selecionar", | |
| maxFileSize: "Tamanho máximo do arquivo: {{size}}MB", | |
| uploadingFile: "Enviando arquivo...", | |
| }, | |
| fileCard: { | |
| preview: "Pré-visualização", | |
| previewNotAvailable: | |
| "Pré-visualização não disponível. Clique no botão de download para visualizar o arquivo.", | |
| filePreview: "Pré-visualização do arquivo: {fileName}", | |
| openFile: "Abrir arquivo", | |
| deleteFile: "Excluir arquivo", | |
| deleteFileConfirmTitle: "Excluir Arquivo", | |
| deleteFileConfirmDescription: | |
| "Você tem certeza de que deseja excluir este arquivo? Esta ação não pode ser desfeita.", | |
| }, | |
| fileUrl: { | |
| additionalLinks: "Links Adicionais", | |
| add: "Adicionar", | |
| linksAdded: "{count} link{s} adicionado", | |
| enterUrl: "Digite a URL", | |
| addAnotherLink: "Adicionar Outro Link", | |
| saveLinks: "Salvar Links", | |
| urlBadge: "URL", | |
| copyLink: "Copiar link", | |
| openLink: "Abrir link", | |
| deleteLink: "Excluir link", | |
| }, | |
| }, | |
| } |
🧰 Tools
🪛 Biome (1.9.4)
[error] 2-9: This property value named languages is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named languages
(lint/suspicious/noDuplicateObjectKeys)
[error] 9-14: This property value named language is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named language
(lint/suspicious/noDuplicateObjectKeys)
[error] 14-168: This property value named common is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named common
(lint/suspicious/noDuplicateObjectKeys)
[error] 168-183: This property value named header is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named header
(lint/suspicious/noDuplicateObjectKeys)
[error] 183-190: This property value named not_found is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named not_found
(lint/suspicious/noDuplicateObjectKeys)
[error] 190-196: This property value named theme is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named theme
(lint/suspicious/noDuplicateObjectKeys)
[error] 196-204: This property value named sidebar is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named sidebar
(lint/suspicious/noDuplicateObjectKeys)
[error] 204-226: This property value named auth is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named auth
(lint/suspicious/noDuplicateObjectKeys)
[error] 226-255: This property value named onboarding is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named onboarding
(lint/suspicious/noDuplicateObjectKeys)
[error] 256-264: This property value named overview is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named overview
(lint/suspicious/noDuplicateObjectKeys)
[error] 264-333: This property value named policies is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named policies
(lint/suspicious/noDuplicateObjectKeys)
[error] 333-337: This property value named evidence_tasks is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named evidence_tasks
(lint/suspicious/noDuplicateObjectKeys)
[error] 337-528: This property value named risk is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named risk
(lint/suspicious/noDuplicateObjectKeys)
[error] 528-571: This property value named settings is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named settings
(lint/suspicious/noDuplicateObjectKeys)
[error] 571-578: This property value named user_menu is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named user_menu
(lint/suspicious/noDuplicateObjectKeys)
[error] 578-625: This property value named frameworks is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named frameworks
(lint/suspicious/noDuplicateObjectKeys)
[error] 625-697: This property value named vendor is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named vendor
(lint/suspicious/noDuplicateObjectKeys)
[error] 697-754: This property value named people is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named people
(lint/suspicious/noDuplicateObjectKeys)
[error] 755-759: This property value named errors is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named errors
(lint/suspicious/noDuplicateObjectKeys)
[error] 759-788: This property value named sub_pages is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named sub_pages
(lint/suspicious/noDuplicateObjectKeys)
[error] 788-810: This property value named editor is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named editor
(lint/suspicious/noDuplicateObjectKeys)
[error] 810-812: This property value named evidence is later overwritten by an object member with the same name.
Overwritten with this value.
If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property value named evidence
(lint/suspicious/noDuplicateObjectKeys)
| fileUrl: { | ||
| additionalLinks: "Enlaces Adicionales", | ||
| add: "Agregar", | ||
| linksAdded: "{count} enlace{s} agregado", |
There was a problem hiding this comment.
Fix dynamic placeholder in Spanish translation.
The translation uses curly braces for the dynamic placeholder {count} and {s} but elsewhere in the file (like in line 835), double curly braces are used {{count}}. This inconsistency might cause rendering issues.
Apply this fix to ensure consistent placeholder syntax:
- linksAdded: "{count} enlace{s} agregado",
+ linksAdded: "{{count}} enlace{{s}} agregado",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| linksAdded: "{count} enlace{s} agregado", | |
| linksAdded: "{{count}} enlace{{s}} agregado", |
| upload: { | ||
| fileSection: { | ||
| filesUploaded: "{{count}} fil{{s}} lastet opp", | ||
| upload: "{{count}} fil{{s}} lastet opp" | ||
| }, | ||
| fileUpload: { | ||
| uploadingText: "Laster opp...", | ||
| dropFileHere: "Slipp fil her", | ||
| releaseToUpload: "Slipp for å laste opp", | ||
| addFiles: "Legg til filer", | ||
| uploadAdditionalEvidence: "Last opp ytterligere bevisfiler", | ||
| dragDropOrClick: "Dra og slipp eller klikk for å laste opp", | ||
| dropFileHereAlt: "Slipp filen her", | ||
| dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge", | ||
| maxFileSize: "Maks filstørrelse: {size}MB", | ||
| uploadingFile: "Laster opp fil..." | ||
| }, | ||
| fileCard: { | ||
| preview: "Forhåndsvisning", | ||
| previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.", | ||
| filePreview: "Filforhåndsvisning: {fileName}", | ||
| openFile: "Åpne fil", | ||
| deleteFile: "Slett fil", | ||
| deleteFileConfirmTitle: "Slett fil", | ||
| deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres." | ||
| }, | ||
| fileUrl: { | ||
| additionalLinks: "Ytterligere lenker", | ||
| add: "Legg til", | ||
| linksAdded: "{count} lenke{s} lagt til", | ||
| enterUrl: "Skriv inn URL", | ||
| addAnotherLink: "Legg til en annen lenke", | ||
| saveLinks: "Lagre lenker", | ||
| urlBadge: "URL", | ||
| copyLink: "Kopier lenke", | ||
| openLink: "Åpne lenke", | ||
| deleteLink: "Slett lenke" | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Inconsistent placeholder formats need standardization.
I noticed that the new upload section uses two different placeholder formats throughout the translations:
- Double braces format:
{{count}},{{s}}(infileSection) - Single braces format:
{count},{s},{size},{fileName}(infileUrland other sections)
This inconsistency could cause interpolation issues if the translation system expects a uniform format.
Apply this diff to standardize all placeholders to use the same format (double braces is used in other areas of the file):
fileSection: {
filesUploaded: "{{count}} fil{{s}} lastet opp",
upload: "{{count}} fil{{s}} lastet opp"
},
fileUpload: {
uploadingText: "Laster opp...",
dropFileHere: "Slipp fil her",
releaseToUpload: "Slipp for å laste opp",
addFiles: "Legg til filer",
uploadAdditionalEvidence: "Last opp ytterligere bevisfiler",
dragDropOrClick: "Dra og slipp eller klikk for å laste opp",
dropFileHereAlt: "Slipp filen her",
dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge",
- maxFileSize: "Maks filstørrelse: {size}MB",
+ maxFileSize: "Maks filstørrelse: {{size}}MB",
uploadingFile: "Laster opp fil..."
},
fileCard: {
preview: "Forhåndsvisning",
previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.",
- filePreview: "Filforhåndsvisning: {fileName}",
+ filePreview: "Filforhåndsvisning: {{fileName}}",
openFile: "Åpne fil",
deleteFile: "Slett fil",
deleteFileConfirmTitle: "Slett fil",
deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres."
},
fileUrl: {
additionalLinks: "Ytterligere lenker",
add: "Legg til",
- linksAdded: "{count} lenke{s} lagt til",
+ linksAdded: "{{count}} lenke{{s}} lagt til",
enterUrl: "Skriv inn URL",
addAnotherLink: "Legg til en annen lenke",
saveLinks: "Lagre lenker",
urlBadge: "URL",
copyLink: "Kopier lenke",
openLink: "Åpne lenke",
deleteLink: "Slett lenke"
}Please run the following script to check how placeholders are formatted throughout the localization files to ensure consistency:
🏁 Script executed:
#!/bin/bash
# Description: Check for placeholder formats in localization files
# This will show whether single or double braces are more commonly used
echo "Counting double-brace placeholders {{...}}:"
rg -c "\{\{[^}]+\}\}" apps/app/src/locales/
echo "\nCounting single-brace placeholders {...}:"
rg -c "\{[^{}]+\}" apps/app/src/locales/
echo "\nShowing examples of both formats:"
echo "Double-brace examples:"
rg "\{\{[^}]+\}\}" apps/app/src/locales/ --max-count 5
echo "\nSingle-brace examples:"
rg "\{[^{}]+\}" apps/app/src/locales/ --max-count 5Length of output: 4717
Standardize Placeholder Formats to Double Braces in the Upload Section
The localization file at apps/app/src/locales/no.ts has inconsistent placeholder formats within the upload section. Some keys use the double brace format (e.g., in fileSection) while others in fileUpload, fileCard, and fileUrl use the single brace format. This discrepancy can lead to interpolation issues.
Please update the following keys to use the double brace syntax for consistency:
-
fileUpload:
ChangemaxFileSize: "Maks filstørrelse: {size}MB"
→maxFileSize: "Maks filstørrelse: {{size}}MB" -
fileCard:
ChangefilePreview: "Filforhåndsvisning: {fileName}"
→filePreview: "Filforhåndsvisning: {{fileName}}" -
fileUrl:
ChangelinksAdded: "{count} lenke{s} lagt til"
→linksAdded: "{{count}} lenke{{s}} lagt til"
Here's the suggested diff:
fileUpload: {
uploadingText: "Laster opp...",
dropFileHere: "Slipp fil her",
releaseToUpload: "Slipp for å laste opp",
addFiles: "Legg til filer",
uploadAdditionalEvidence: "Last opp ytterligere bevisfiler",
dragDropOrClick: "Dra og slipp eller klikk for å laste opp",
dropFileHereAlt: "Slipp filen her",
dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge",
- maxFileSize: "Maks filstørrelse: {size}MB",
+ maxFileSize: "Maks filstørrelse: {{size}}MB",
uploadingFile: "Laster opp fil..."
},
fileCard: {
preview: "Forhåndsvisning",
previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.",
- filePreview: "Filforhåndsvisning: {fileName}",
+ filePreview: "Filforhåndsvisning: {{fileName}}",
openFile: "Åpne fil",
deleteFile: "Slett fil",
deleteFileConfirmTitle: "Slett fil",
deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres."
},
fileUrl: {
additionalLinks: "Ytterligere lenker",
add: "Legg til",
- linksAdded: "{count} lenke{s} lagt til",
+ linksAdded: "{{count}} lenke{{s}} lagt til",
enterUrl: "Skriv inn URL",
addAnotherLink: "Legg til en annen lenke",
saveLinks: "Lagre lenker",
urlBadge: "URL",
copyLink: "Kopier lenke",
openLink: "Åpne lenke",
deleteLink: "Slett lenke"
}The verification script confirms that while many localization files already use the double brace format in similar contexts, the affected keys in apps/app/src/locales/no.ts do not. Please apply these changes to avoid potential interpolation problems.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| upload: { | |
| fileSection: { | |
| filesUploaded: "{{count}} fil{{s}} lastet opp", | |
| upload: "{{count}} fil{{s}} lastet opp" | |
| }, | |
| fileUpload: { | |
| uploadingText: "Laster opp...", | |
| dropFileHere: "Slipp fil her", | |
| releaseToUpload: "Slipp for å laste opp", | |
| addFiles: "Legg til filer", | |
| uploadAdditionalEvidence: "Last opp ytterligere bevisfiler", | |
| dragDropOrClick: "Dra og slipp eller klikk for å laste opp", | |
| dropFileHereAlt: "Slipp filen her", | |
| dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge", | |
| maxFileSize: "Maks filstørrelse: {size}MB", | |
| uploadingFile: "Laster opp fil..." | |
| }, | |
| fileCard: { | |
| preview: "Forhåndsvisning", | |
| previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.", | |
| filePreview: "Filforhåndsvisning: {fileName}", | |
| openFile: "Åpne fil", | |
| deleteFile: "Slett fil", | |
| deleteFileConfirmTitle: "Slett fil", | |
| deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres." | |
| }, | |
| fileUrl: { | |
| additionalLinks: "Ytterligere lenker", | |
| add: "Legg til", | |
| linksAdded: "{count} lenke{s} lagt til", | |
| enterUrl: "Skriv inn URL", | |
| addAnotherLink: "Legg til en annen lenke", | |
| saveLinks: "Lagre lenker", | |
| urlBadge: "URL", | |
| copyLink: "Kopier lenke", | |
| openLink: "Åpne lenke", | |
| deleteLink: "Slett lenke" | |
| } | |
| } | |
| upload: { | |
| fileSection: { | |
| filesUploaded: "{{count}} fil{{s}} lastet opp", | |
| upload: "{{count}} fil{{s}} lastet opp" | |
| }, | |
| fileUpload: { | |
| uploadingText: "Laster opp...", | |
| dropFileHere: "Slipp fil her", | |
| releaseToUpload: "Slipp for å laste opp", | |
| addFiles: "Legg til filer", | |
| uploadAdditionalEvidence: "Last opp ytterligere bevisfiler", | |
| dragDropOrClick: "Dra og slipp eller klikk for å laste opp", | |
| dropFileHereAlt: "Slipp filen her", | |
| dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge", | |
| maxFileSize: "Maks filstørrelse: {{size}}MB", | |
| uploadingFile: "Laster opp fil..." | |
| }, | |
| fileCard: { | |
| preview: "Forhåndsvisning", | |
| previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.", | |
| filePreview: "Filforhåndsvisning: {{fileName}}", | |
| openFile: "Åpne fil", | |
| deleteFile: "Slett fil", | |
| deleteFileConfirmTitle: "Slett fil", | |
| deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres." | |
| }, | |
| fileUrl: { | |
| additionalLinks: "Ytterligere lenker", | |
| add: "Legg til", | |
| linksAdded: "{{count}} lenke{{s}} lagt til", | |
| enterUrl: "Skriv inn URL", | |
| addAnotherLink: "Legg til en annen lenke", | |
| saveLinks: "Lagre lenker", | |
| urlBadge: "URL", | |
| copyLink: "Kopier lenke", | |
| openLink: "Åpne lenke", | |
| deleteLink: "Slett lenke" | |
| } | |
| } |
- Remove unnecessary string interpolation for pluralization - Update translation keys for file upload and section components - Simplify translation strings for better readability
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
apps/app/src/locales/no.ts (1)
803-840: 🛠️ Refactor suggestionFix placeholder format and capitalization issues in Norwegian localization.
There are two issues in the new
uploadsection:
- Inconsistent placeholder format: The section uses single braces (e.g.,
{count}) while other parts of the file use double braces- Capitalization error in the
linksAddedproperty -{S}on line 832 should be lowercase{s}Update the placeholders to use double braces and fix the capitalization issue:
fileSection: { - filesUploaded: "{count} fil(er) lastet opp", - upload: "{count} fil(er) lastet opp" + filesUploaded: "{{count}} fil(er) lastet opp", + upload: "{{count}} fil(er) lastet opp" }, fileUpload: { uploadingText: "Laster opp...", dropFileHere: "Slipp fil her", releaseToUpload: "Slipp for å laste opp", addFiles: "Legg til filer", uploadAdditionalEvidence: "Last opp en fil", dragDropOrClick: "Dra og slipp eller klikk for å laste opp", dropFileHereAlt: "Slipp filen her", dragDropOrClickToSelect: "Dra og slipp en fil her, eller klikk for å velge", - maxFileSize: "Maks filstørrelse: {size}MB", + maxFileSize: "Maks filstørrelse: {{size}}MB", uploadingFile: "Laster opp fil..." }, fileCard: { preview: "Forhåndsvisning", previewNotAvailable: "Forhåndsvisning ikke tilgjengelig. Klikk på nedlastingsknappen for å se filen.", - filePreview: "Filforhåndsvisning: {fileName}", + filePreview: "Filforhåndsvisning: {{fileName}}", openFile: "Åpne fil", deleteFile: "Slett fil", deleteFileConfirmTitle: "Slett fil", deleteFileConfirmDescription: "Er du sikker på at du vil slette denne filen? Denne handlingen kan ikke angres." }, fileUrl: { additionalLinks: "Ytterligere lenker", add: "Legg til", - linksAdded: "{count} lenke{S} lagt til", + linksAdded: "{{count}} lenke{{s}} lagt til", enterUrl: "Skriv inn URL", addAnotherLink: "Legg til en annen lenke", saveLinks: "Lagre lenker", urlBadge: "URL", copyLink: "Kopier lenke", openLink: "Åpne lenke", deleteLink: "Slett lenke" }
🧹 Nitpick comments (6)
apps/app/src/components/upload/FileUpload.tsx (2)
64-69: Add user feedback or error handling for invalid or oversized files.Currently, the code only handles accepted files but does not convey feedback for rejections, such as invalid file types or file size exceeding the specified
maxSize. For a more user-friendly experience, you could implementonDropRejectedor display error messages when files are rejected.
130-170: Extend accessibility support to the default variant.Similarly, for the default variant, consider adding ARIA attributes and live regions to relay the upload and drag state to users relying on assistive technologies.
<div {...getRootProps()} + aria-busy={isUploading} + aria-live="polite" + aria-label={ + isUploading + ? "Uploading in progress" + : isDragActive + ? "Drop file here" + : "Drag & drop or click to select a file" + } className={cn(...)} >apps/app/src/components/upload/FileSection.tsx (2)
97-104: Consider parallelizing file preview loading.Each file preview is currently loaded sequentially in the for-loop, which can slow down loading for larger batches of files. Using a parallel approach (e.g.,
Promise.all()) can improve responsiveness:await Promise.all( filesToLoad.map(async (url) => { await handlePreviewClick(url); }) );
106-153: Add unit tests for FileSection.This component orchestrates file uploads, previews, and deletions, which merits dedicated test coverage to ensure reliability and detect regressions. Consider covering:
• Successful file uploads.
• Network or error scenarios in preview loading.
• File deletions and the “onSuccess” callback flow.Would you like me to open a new issue to track adding tests for
FileSection?apps/app/src/locales/fr.ts (1)
803-841: Ensure placeholder format consistency in French localization.The new
uploadsection uses single braces for placeholders (e.g.,{count},{size},{fileName}), while other sections in the file use double braces format (e.g.,{{current}}in line 93). This inconsistency could cause interpolation issues if the translation system expects a uniform format.Update the placeholders to use double braces format for consistency:
fileSection: { - filesUploaded: "{count} fichier(s) téléchargé(s)", - upload: "{count} fichier(s) téléchargé(s)" + filesUploaded: "{{count}} fichier(s) téléchargé(s)", + upload: "{{count}} fichier(s) téléchargé(s)" }, fileUpload: { uploadingText: "Téléchargement en cours...", dropFileHere: "Déposez le fichier ici", releaseToUpload: "Relâchez pour télécharger", addFiles: "Ajouter des fichiers", uploadAdditionalEvidence: "Télécharger un fichier", dragDropOrClick: "Glissez-déposez ou cliquez pour télécharger", dropFileHereAlt: "Déposez le fichier ici", dragDropOrClickToSelect: "Glissez-déposez un fichier ici, ou cliquez pour sélectionner", - maxFileSize: "Taille maximale du fichier : {size} Mo", + maxFileSize: "Taille maximale du fichier : {{size}} Mo", uploadingFile: "Téléchargement du fichier..." }, fileCard: { preview: "Aperçu", previewNotAvailable: "Aperçu non disponible. Cliquez sur le bouton de téléchargement pour voir le fichier.", - filePreview: "Aperçu du fichier : {fileName}", + filePreview: "Aperçu du fichier : {{fileName}}", openFile: "Ouvrir le fichier", deleteFile: "Supprimer le fichier", deleteFileConfirmTitle: "Supprimer le fichier", deleteFileConfirmDescription: "Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action ne peut pas être annulée." }, fileUrl: { additionalLinks: "Liens supplémentaires", add: "Ajouter", - linksAdded: "{count} lien{s} ajouté{s}", + linksAdded: "{{count}} lien{{s}} ajouté{{s}}", enterUrl: "Entrez l'URL", addAnotherLink: "Ajouter un autre lien", saveLinks: "Enregistrer les liens", urlBadge: "URL", copyLink: "Copier le lien", openLink: "Ouvrir le lien", deleteLink: "Supprimer le lien" }apps/app/src/locales/pt.ts (1)
773-810: Ensure placeholder format consistency in Portuguese localization.The new
uploadsection uses single braces for placeholders (e.g.,{count},{size},{fileName}), while other sections in the file use double braces format (e.g.,{{current}}in line 93). This inconsistency could cause interpolation issues if the translation system expects a uniform format.Update the placeholders to use double braces format for consistency:
fileSection: { - filesUploaded: "{count} arquivo(s) enviado(s)", - upload: "{count} arquivo(s) enviado(s)" + filesUploaded: "{{count}} arquivo(s) enviado(s)", + upload: "{{count}} arquivo(s) enviado(s)" }, fileUpload: { uploadingText: "Enviando...", dropFileHere: "Solte o arquivo aqui", releaseToUpload: "Solte para enviar", addFiles: "Adicionar Arquivos", uploadAdditionalEvidence: "Enviar um arquivo", dragDropOrClick: "Arraste e solte ou clique para enviar", dropFileHereAlt: "Solte o arquivo aqui", dragDropOrClickToSelect: "Arraste e solte um arquivo aqui ou clique para selecionar", - maxFileSize: "Tamanho máximo do arquivo: {size}MB", + maxFileSize: "Tamanho máximo do arquivo: {{size}}MB", uploadingFile: "Enviando arquivo..." }, fileCard: { preview: "Pré-visualização", previewNotAvailable: "Pré-visualização não disponível. Clique no botão de download para visualizar o arquivo.", - filePreview: "Pré-visualização do arquivo: {fileName}", + filePreview: "Pré-visualização do arquivo: {{fileName}}", openFile: "Abrir arquivo", deleteFile: "Excluir arquivo", deleteFileConfirmTitle: "Excluir Arquivo", deleteFileConfirmDescription: "Você tem certeza de que deseja excluir este arquivo? Esta ação não pode ser desfeita." }, fileUrl: { additionalLinks: "Links Adicionais", add: "Adicionar", - linksAdded: "{count} link{s} adicionado", + linksAdded: "{{count}} link{{s}} adicionado", enterUrl: "Digite a URL", addAnotherLink: "Adicionar Outro Link", saveLinks: "Salvar Links", urlBadge: "URL", copyLink: "Copiar link", openLink: "Abrir link", deleteLink: "Excluir link" }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/app/languine.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
apps/app/src/components/upload/FileSection.tsx(1 hunks)apps/app/src/components/upload/FileUpload.tsx(1 hunks)apps/app/src/locales/en.ts(1 hunks)apps/app/src/locales/es.ts(1 hunks)apps/app/src/locales/fr.ts(1 hunks)apps/app/src/locales/no.ts(1 hunks)apps/app/src/locales/pt.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/app/src/locales/en.ts
🔇 Additional comments (2)
apps/app/src/components/upload/FileUpload.tsx (1)
71-128: Improve accessibility for the card variant.Including ARIA attributes (e.g.,
aria-busy,aria-live,aria-label) can help screen readers convey the drag-and-drop and upload states to users more effectively. This enhancement was previously suggested and remains unimplemented.<Card + aria-busy={isUploading} + aria-live="polite" + aria-label={ + isUploading + ? "Uploading in progress" + : isDragActive + ? "Drop file here" + : "Add files" + } className={cn(apps/app/src/locales/es.ts (1)
803-840: Standardize placeholder format to use double braces in Spanish localization.The placeholder format uses single braces (e.g.,
{count},{size},{fileName}) in the newuploadsection, while other parts of the file use double braces (e.g.,{{current}}in line 93). This inconsistency could cause interpolation issues.Update all placeholders to use the double braces format for consistency:
fileSection: { - filesUploaded: "{count} archivo(s) subido(s)", - upload: "{count} archivo(s) subido(s)" + filesUploaded: "{{count}} archivo(s) subido(s)", + upload: "{{count}} archivo(s) subido(s)" }, fileUpload: { uploadingText: "Subiendo...", dropFileHere: "Suelta el archivo aquí", releaseToUpload: "Suelta para subir", addFiles: "Agregar Archivos", uploadAdditionalEvidence: "Subir un archivo", dragDropOrClick: "Arrastra y suelta o haz clic para subir", dropFileHereAlt: "Suelta el archivo aquí", dragDropOrClickToSelect: "Arrastra y suelta un archivo aquí, o haz clic para seleccionar", - maxFileSize: "Tamaño máximo de archivo: {size}MB", + maxFileSize: "Tamaño máximo de archivo: {{size}}MB", uploadingFile: "Subiendo archivo..." }, fileCard: { preview: "Vista previa", previewNotAvailable: "Vista previa no disponible. Haz clic en el botón de descarga para ver el archivo.", - filePreview: "Vista previa del archivo: {fileName}", + filePreview: "Vista previa del archivo: {{fileName}}", openFile: "Abrir archivo", deleteFile: "Eliminar archivo", deleteFileConfirmTitle: "Eliminar Archivo", deleteFileConfirmDescription: "¿Estás seguro de que deseas eliminar este archivo? Esta acción no se puede deshacer." }, fileUrl: { additionalLinks: "Enlaces Adicionales", add: "Agregar", - linksAdded: "{count} enlace{s} añadido{s}", + linksAdded: "{{count}} enlace{{s}} añadido{{s}}", enterUrl: "Ingresa la URL", addAnotherLink: "Agregar Otro Enlace", saveLinks: "Guardar Enlaces", urlBadge: "URL", copyLink: "Copiar enlace", openLink: "Abrir enlace", deleteLink: "Eliminar enlace" }
…mp into lewis/risk-register-uploads
Deleted the custom file upload hook as part of ongoing refactoring efforts. This hook was likely replaced by a more streamlined file upload mechanism or integrated into a different component.
Deleted the UploadThing React helpers from the uploadthing.ts file, continuing the refactoring of file upload mechanisms in the application.
Summary by CodeRabbit
New Features
Refactor & Chores