Skip to content

feat(upload): Implement comprehensive file upload and management system#91

Merged
carhartlewis merged 14 commits into
mainfrom
lewis/risk-register-uploads
Mar 4, 2025
Merged

feat(upload): Implement comprehensive file upload and management system#91
carhartlewis merged 14 commits into
mainfrom
lewis/risk-register-uploads

Conversation

@carhartlewis
Copy link
Copy Markdown
Contributor

@carhartlewis carhartlewis commented Mar 4, 2025

  • 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

Summary by CodeRabbit

  • New Features

    • Enhanced file management including streamlined upload, preview, deletion, and URL updating for evidence and task attachments.
    • Improved file display and interaction with new components for previews and file icons.
    • Integrated analytics to bolster performance tracking based on environment configuration.
    • Enhanced accessibility with updated navigation labels and system-based theme support.
    • Comprehensive updates to localization strings for improved clarity and consistency across multiple languages.
    • Added new hooks and components to manage file uploads, deletions, and previews effectively.
    • Introduced new components for managing task attachments and file sections.
    • Added new sections in localization files for file upload functionalities in multiple languages.
  • Refactor & Chores

    • Removed outdated upload integrations and cleaned up legacy components to boost reliability and clarity.
    • Refactored providers and layout components for improved structure and readability.
    • Adjusted middleware for better session handling and response management.
    • Removed unused environment variables related to file uploads.

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

vercel Bot commented Mar 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 4, 2025 4:38pm
2 Skipped Deployments
Name Status Preview Comments Updated (UTC)
comp-portal ⬜️ Skipped (Inspect) Mar 4, 2025 4:38pm
web ⬜️ Skipped (Inspect) Mar 4, 2025 4:38pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2025

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 2c85b28 and 280d89d.

📒 Files selected for processing (3)
  • apps/app/src/components/upload/FileUpload.tsx (1 hunks)
  • apps/app/src/hooks/use-upload-file.ts (0 hunks)
  • apps/app/src/lib/uploadthing.ts (0 hunks)

Walkthrough

This 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

File(s) Change Summary
.vscode/settings.json New VSCode configuration file created (empty placeholder for future settings).
apps/app/src/actions/files/delete-file.ts, get-file-url.ts, update-urls.ts, upload-file.ts Added new server-side actions for deleting files, fetching S3 signed URLs, updating evidence URLs, and handling file uploads with Zod validation and error handling.
apps/app/src/actions/types.ts Formatting adjustments and addition of the new UPLOAD_TYPE constant.
apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/actions/deleteEvidenceFile.ts, hooks/useFileDelete.ts Cosmetic formatting updates in evidence file deletion logic without affecting functionality.
apps/app/src/app/[locale]/(app)/(dashboard)/risk/[riskId]/tasks/[taskId]/actions/getTaskAttachments.ts, hooks/useTaskAttachments.ts, page.tsx New action and hook for fetching risk task attachments and updated task attachments handling in the Risk page.
apps/app/src/app/[locale]/layout.tsx, providers.tsx Removed NextSSRPlugin from layout and added scriptProps to the ThemeProvider.
apps/app/src/app/api/uploadthing/core.ts, route.ts Removed obsolete UploadThing integration including file upload routes and associated types.
apps/app/src/components/risks/tasks/task-attachment.tsx, task-attachments.tsx Deleted the old TaskAttachments component and introduced a new one for managing task attachments.
apps/app/src/components/upload/FileCard.tsx, FileIcon.tsx, FileSection.tsx, FileUpload.tsx, FileUrl.tsx Added new React components to display file previews, icons, manage file uploads and URL associations.
apps/app/src/env.mjs Removed UPLOADTHING_TOKEN and UPLOADTHING_SECRET from environment configurations.
apps/app/src/hooks/upload/use-file-delete.ts, use-file-preview.ts, use-file-upload.ts, use-url-management.ts Introduced new custom hooks to manage file deletion, preview retrieval, upload process, and URL management with error handling and state updates.
apps/web/src/app/(home)/layout.tsx, components/main-nav.tsx, layout.tsx, providers.tsx Updated web app layout: removed unused SiteFooter import, added an aria-label for accessibility, integrated conditional analytics initialization, and refactored Providers with updated theme settings.

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
Loading
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
Loading

Possibly related PRs

  • Mariano/stuff #74: Introduces additional file upload and presigned URL management functionality that directly relates to the file operations and S3 integrations modified in this PR.

Poem

I'm a rabbit with hops so free,
In code meadows, changes bloom for me.
Files now dance in S3's light,
Hooks and actions set things right.
With every line and gentle fix,
My heart leaps like clever tricks.
🐇💻 Hip, hop—code magic in the mix!


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?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 FileIcon component 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:

  1. 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} />
  1. 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} />;
+ }
  1. 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 evidence and riskTask share 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?.data is empty, the component returns null. 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.serverError is 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 handlePreviewClick dependency array includes previewStates, 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 identical DialogTrigger and 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 specifying ServerSideEncryption and 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 structure

The nested data structure in ServerResponse seems unnecessarily complex with multiple layers of data objects. 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 IDs

When evidenceId or taskId are 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 UI

The 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 variant

The 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 contextual

The 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 readability

The 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 query

The task query includes TaskAttachment but 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 found

When 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2f5e05c and 205dd8e.

📒 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

  1. Prevent Rocket Loader Interference
    Adding data-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].
  2. Resolve Script Errors
    Rocket Loader modifies script tags (e.g., appending hashes to type attributes like type="ea10e272b0-text/javascript"), which can break HTML validation or Content Security Policies (CSP)[4]. Excluding scripts prevents such modifications.

  3. 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 src attribute 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.php to 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:


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 Providers component. The changes:

  1. Simplify the code by removing the separate AnalyticsWrapper component
  2. Make the analytics integration more straightforward
  3. Improve user experience by using defaultTheme="system" and enableSystem to respect user preferences
  4. Add the data-cfasync="false" attribute for consistent Cloudflare script handling across the application

This 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 in apps/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" to defaultTheme="system" with enableSystem improves 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 initializeServer function from @bubba/analytics is 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 DraftUrl interface for managing unsaved URLs and the response type UpdateUrlsActionResponse extending 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 isValidUrl function correctly uses a try-catch block to handle URL parsing, and formatUrl adds 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.
Because hasLoadedPreview and previewState.isLoading are 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 if url or props change unexpectedly.

apps/app/src/hooks/upload/use-file-upload.ts (4)

3-7: Good imports and type usage

The 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 good

The 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 upload

The 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 handling

The 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 support

The 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 implementation

The onDrop callback correctly handles the first file from the dropped files array, which aligns with the multiple: false setting in the dropzone configuration.


59-71: Good dropzone configuration

The 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 good

The import for TaskAttachments has been correctly updated.


36-42: Simplified TaskAttachments implementation

The 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 params prop 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 ts

Length of output: 3438


Validation of params Type Usage

The usage of params as 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.

Comment on lines +43 to +48
if (!attachments) {
return {
success: false,
error: "Task attachments not found",
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
if (!attachments) {
return {
success: false,
error: "Task attachments not found",
};
}
if (attachments.length === 0) {
return {
success: false,
error: "Task attachments not found",
};
}

Comment on lines +59 to +60
[evidenceId, onSuccess, toast],
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
[evidenceId, onSuccess, toast],
);
[evidenceId, taskId, uploadType, onSuccess, toast],

Comment on lines +78 to +86
const evidence = await db.organizationEvidence.findFirst({
where: {
id: parsedInput.evidenceId,
},
});

if (!evidence) {
throw new Error("Evidence or file not found");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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");
}

Comment on lines +106 to +115
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");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
"ue client"
"use client"

setIsUploading(false);
}
},
[evidenceId, onSuccess, toast],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
[evidenceId, onSuccess, toast],
[evidenceId, taskId, uploadType, onSuccess, toast],

Comment on lines +9 to +28
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;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 14:48 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 14:48 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 14:49 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 14:49 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (6)
apps/app/src/components/upload/FileUrl.tsx (3)

43-50: Consider enhancing URL validation

The getDomainName function 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 operations

The copyToClipboard function 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 accessibility

While 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 feedback

The onDrop callback 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 option

To improve user experience, consider adding the preventDropOnDocument option 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 localization

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 205dd8e and 8560657.

⛔ Files ignored due to path filters (1)
  • apps/app/languine.lock is 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 upload section is well-organized with four logical subsections that cover different aspects of the file upload process:

  • fileSection: Displays counts of uploaded files
  • fileUpload: Provides prompts and status messages for the upload process
  • fileCard: Handles file preview and management
  • fileUrl: Manages additional link functionalities

The 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 displays
  • fileUpload: For upload interface text
  • fileCard: For file preview and management
  • fileUrl: For link management

All 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 imports

The imports are well-structured with UI components grouped together and utility functions separated appropriately.


16-20: Clear interface definition with appropriate types

The interface is well-defined with descriptive property names and appropriate types. The onSuccess callback returning a Promise is a good practice for async operations.

apps/app/src/components/upload/FileUpload.tsx (3)

10-29: Remove unused onClick prop

The component accepts an onClick prop 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 support

The 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 CSS

The component effectively uses the cn utility to conditionally apply classes based on dropzone states, which results in clean, maintainable code.

Comment on lines +90 to +102
<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();
}
}}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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 useUrlManagement hook for consistency.
  • Provide immediate feedback to the user when an invalid URL is detected rather than proceeding with the submission.

Comment on lines +169 to +229
<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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
<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>

Comment on lines +81 to +89
<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",
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
<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
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 14:58 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 14:58 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 14:59 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 14:59 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 evidenceId and taskId. This might lead to API errors if the backend expects valid IDs when calling deleteFile.

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, extractS3KeyFromUrl relies on amazonaws.com being 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.
Both UPLOAD_TYPE.evidence and UPLOAD_TYPE.riskTask share 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 hardcoded expiresIn: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8560657 and fa013ac.

⛔ Files ignored due to path filters (1)
  • apps/app/languine.lock is 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_TYPE enum 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 onSuccess callback 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, and toast. 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 upload section 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.ts

Length 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 both filesUploaded and linksAdded). 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}} subido and {{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:

  1. "dragDropOrClick": "Dra og slipp eller klikk for å laste opp"
  2. "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 opp provides 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.ts

Length 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.

Comment on lines +78 to +84
const evidence = await db.organizationEvidence.findFirst({
where: {
id: parsedInput.evidenceId,
organizationId: user.organizationId,
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 16:08 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 16:08 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 16:09 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 16:09 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 mount

This assumes the url doesn'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}} subido might 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:

  1. dropFileHere and dropFileHereAlt have similar translations
  2. dragDropOrClick and dragDropOrClickToSelect convey similar concepts
  3. uploadingText and uploadingFile are nearly identical

These 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa013ac and 02f23e0.

📒 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-pathname header 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 FilePreviewState interface 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 upload section 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 upload section 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 upload section 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 maxFileSize translation 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 filesUploaded and upload are 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.

Comment on lines +159 to +188
<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>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
<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>
)}
</>
);
}

Comment thread apps/app/src/locales/pt.ts Outdated
Comment on lines +1 to +844
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: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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)

Comment thread apps/app/src/locales/es.ts Outdated
fileUrl: {
additionalLinks: "Enlaces Adicionales",
add: "Agregar",
linksAdded: "{count} enlace{s} agregado",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
linksAdded: "{count} enlace{s} agregado",
linksAdded: "{{count}} enlace{{s}} agregado",

Comment on lines +803 to 841
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"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Inconsistent placeholder formats need standardization.

I noticed that the new upload section uses two different placeholder formats throughout the translations:

  1. Double braces format: {{count}}, {{s}} (in fileSection)
  2. Single braces format: {count}, {s}, {size}, {fileName} (in fileUrl and 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 5

Length 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:
    Change maxFileSize: "Maks filstørrelse: {size}MB"
    maxFileSize: "Maks filstørrelse: {{size}}MB"

  • fileCard:
    Change filePreview: "Filforhåndsvisning: {fileName}"
    filePreview: "Filforhåndsvisning: {{fileName}}"

  • fileUrl:
    Change linksAdded: "{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.

Suggested change
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
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 16:19 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 16:19 Inactive
@vercel vercel Bot temporarily deployed to Preview – comp-portal March 4, 2025 16:20 Inactive
@vercel vercel Bot temporarily deployed to Preview – web March 4, 2025 16:20 Inactive
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/app/src/locales/no.ts (1)

803-840: 🛠️ Refactor suggestion

Fix placeholder format and capitalization issues in Norwegian localization.

There are two issues in the new upload section:

  1. Inconsistent placeholder format: The section uses single braces (e.g., {count}) while other parts of the file use double braces
  2. Capitalization error in the linksAdded property - {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 implement onDropRejected or 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 upload section 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 upload section 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2513837 and 2c85b28.

⛔ Files ignored due to path filters (1)
  • apps/app/languine.lock is 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 new upload section, 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"
    }

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants