diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml index d1f4ecab8..4c085512f 100644 --- a/frontend/editor/public/locales/en-GB/translation.toml +++ b/frontend/editor/public/locales/en-GB/translation.toml @@ -12,7 +12,6 @@ cancel = "Cancel" changedCredsMessage = "Credentials changed!" chooseFile = "Choose File" close = "Close" -color = "Colour" comingSoon = "Coming soon" confirm = "Confirm" confirmClose = "Confirm Close" @@ -199,6 +198,10 @@ ssoDescription = "Two-factor authentication is managed by your identity provider ssoManaged = "Configure MFA through your identity provider." title = "Two-factor authentication" +[account.overview] +logOut = "Log out" +signedInAs = "Signed in as: {{email}}" + [add-page-numbers] tags = "paginate,label,organize,index" @@ -226,6 +229,9 @@ title = "What it does" [AddAttachmentsRequest.tooltip.header] title = "About Add Attachments" +[addFileCard] +upload = "Upload" + [addImage] applySignatures = "Apply Images" header = "Add images to PDFs" @@ -606,6 +612,7 @@ label = "Temp File Management" [admin.settings.advanced.tempFileManagement.baseTmpDir] description = "Base directory for temporary files (leave empty for default: java.io.tmpdir/stirling-pdf)" label = "Base Temp Directory" +placeholder = "Default: java.io.tmpdir/stirling-pdf" [admin.settings.advanced.tempFileManagement.cleanupIntervalMinutes] description = "How often to run cleanup (in minutes)" @@ -618,6 +625,7 @@ label = "Cleanup System Temp" [admin.settings.advanced.tempFileManagement.libreofficeDir] description = "Directory for LibreOffice temp files (leave empty for default: baseTmpDir/libreoffice)" label = "LibreOffice Temp Directory" +placeholder = "Default: baseTmpDir/libreoffice" [admin.settings.advanced.tempFileManagement.maxAgeHours] description = "Maximum age in hours before temp files are cleaned up" @@ -634,6 +642,7 @@ label = "Startup Cleanup" [admin.settings.advanced.tempFileManagement.systemTempDir] description = "System temp directory to clean (only used if cleanupSystemTemp is enabled)" label = "System Temp Directory" +placeholder = "System temp directory path" [admin.settings.advanced.tessdataDir] description = "Path to the tessdata directory for OCR language files" @@ -839,6 +848,7 @@ label = "Database Name" [admin.settings.database.password] description = "Database authentication password" label = "Password" +placeholder = "Enter database password" [admin.settings.database.port] description = "Database server port (not used if custom URL is provided)" @@ -870,10 +880,12 @@ label = "Hide unavailable tools by default" [admin.settings.endpoints.groupsToRemove] description = "Select endpoint groups to disable" label = "Disabled Endpoint Groups" +placeholder = "Select groups to disable" [admin.settings.endpoints.toRemove] description = "Select individual endpoints to disable" label = "Disabled Endpoints" +placeholder = "Select endpoints to disable" [admin.settings.enterpriseRequired] message = "An Enterprise license is required to access {{featureName}}. You are viewing demo data for reference." @@ -1088,6 +1100,7 @@ label = "SMTP Host" [admin.settings.mail.password] description = "Password for SMTP authentication" label = "SMTP Password" +placeholder = "Enter SMTP password" [admin.settings.mail.port] description = "The port number for SMTP connection (typically 25, 465, or 587)" @@ -1097,6 +1110,10 @@ label = "SMTP Port" description = "Username for SMTP authentication" label = "SMTP Username" +[admin.settings.plan.noData] +message = "Plans data is not available at the moment." +title = "No data available" + [admin.settings.premium] description = "Configure your premium or enterprise license key." license = "License Configuration" @@ -1143,6 +1160,9 @@ info = "If you have a license key or certificate file from a direct purchase, yo toggle = "Got a license key or certificate file?" [admin.settings.premium.movedFeatures] +auditLogging = "<0>Audit Logging (ENTERPRISE) - Security" +customMetadata = "<0>Custom Metadata (PRO) - General" +databaseConfiguration = "<0>Database Configuration (ENTERPRISE) - Database" message = "Premium and Enterprise features are now organized in their respective sections:" title = "Premium Features Distributed" @@ -1526,10 +1546,10 @@ section_title = "Agents" show_less = "Show less" start_chat = "Start chatting" stirling_description = "Your general-purpose PDF assistant" -stirling_running = "Running..." stirling_full_name = "Stirling General Agent" stirling_long_description = "General purpose PDF assistant that can run tools, create PDFs and extract insights from your documents." stirling_name = "Stirling" +stirling_running = "Running..." stirling_tooltip = "Stirling agent" view_all = "View all agents" @@ -2126,11 +2146,31 @@ placeholder = "Number of pages" title = "Last N Pages" [bulkSelection.operators] +text = "AND has higher precedence than comma. NOT applies within the document range." +title = "Operators" + +[bulkSelection.operators.and] +title = "Combine selections (both conditions must be true)" + +[bulkSelection.operators.descriptions] and = "AND: & or \"and\" - require both conditions (e.g., 1-50 & even)" comma = "Comma: , or | - combine selections (e.g., 1-10, 20)" not = "NOT: ! or \"not\" - exclude pages (e.g., 3n & not 30)" -text = "AND has higher precedence than comma. NOT applies within the document range." -title = "Operators" + +[bulkSelection.operators.even] +title = "Select all even-numbered pages (2, 4, 6, 8...)" + +[bulkSelection.operators.not] +title = "Exclude from selection" + +[bulkSelection.operators.odd] +title = "Select all odd-numbered pages (1, 3, 5, 7...)" + +[bulkSelection.operators.or] +title = "Add to selection (either condition can be true)" + +[bulkSelection.pageSelection] +title = "Page Selection" [bulkSelection.range] fromPlaceholder = "From" @@ -2782,6 +2822,9 @@ unknownTool = "Unknown tool" [cloudBadge] tooltip = "This operation will use your cloud credits" +[color.eyeDropper] +tooltip = "Pick colour from screen" + [colorPicker] title = "Choose colour" @@ -3006,6 +3049,10 @@ title = "Compression Method" 1 = "1-3 PDF compression,
4-6 lite image compression,
7-9 intense image compression Will dramatically reduce image quality" _value = "Compression Settings" +[compress.settings] +desiredSize = "Desired File Size" +desiredSizePlaceholder = "Enter size" + [compress.tooltip.description] text = "Compression is an easy way to reduce your file size. Pick File Size to enter a target size and have us adjust quality for you. Pick Quality to set compression strength manually." title = "Description" @@ -3496,6 +3543,13 @@ docsLink = "View installation documentation" message = "Stirling-PDF does not have permission to update itself on this machine." title = "Administrator permissions required" +[dropdownList] +searchPlaceholder = "Search..." + +[editableSecretField] +edit = "Edit" +editSecretValue = "Edit secret value" + [editTableOfContents] submit = "Apply table of contents" @@ -3583,6 +3637,9 @@ tabTitle = "Outline workspace" description = "Select the Edit Table of Contents tool to load its workspace." title = "Open the tool to start editing" +[emptyFilesState] +upload = "Upload" + [encryptedPdfUnlock] description = "This PDF is password protected. Enter the password so you can continue working with it." emptyResponse = "Password removal did not produce a file." @@ -3697,6 +3754,10 @@ title = "Settings" [extractPages.tooltip] description = "Extracts the selected pages into a new PDF, preserving order." +[fileCard] +openInFileEditor = "Open in File Editor" +viewInViewer = "View in Viewer" + [fileChooser] click = "Click" dragAndDrop = "Drag & Drop" @@ -3706,6 +3767,9 @@ extractPDF = "Extracting..." hoveredDragAndDrop = "Drag & Drop file(s) here" or = "or" +[fileDropdownMenu] +closeFile = "Close file" + [fileEditor] addFiles = "Add Files" pageCount = "{{count}} pages" @@ -3872,6 +3936,10 @@ openSettings = "Open settings" search = "Search" searchPlaceholder = "Search files..." +[fileSidebar.fileItem] +closeViewer = "Close viewer" +openInViewer = "Open in viewer" + [filesPage] addToWorkspace = "Add to workspace" addToWorkspaceCount = "Add {{count}} to workspace" @@ -4178,6 +4246,15 @@ title = "About Flattening PDFs" discord = "Discord" issues = "GitHub" +[formFill] +flattenAfterFilling = "Flatten after filling" +rescanFields = "Re-scan fields" +rescanFormFields = "Re-scan form fields" +save = "Save" + +[formFill.sidebar] +close = "Close sidebar" + [getPdfInfo] downloadJson = "Download JSON" downloads = "Downloads" @@ -4737,6 +4814,11 @@ tags = "markup,web-content,transformation,convert" title = "HTML To PDF" zoom = "Zoom level for displaying the website." +[image.upload] +hint = "Upload a PNG, JPG, SVG, or other image file to place on the PDF. SVG files will be converted to PNG for compatibility." +label = "Upload Image" +placeholder = "Select image file" + [imageToPdf] tags = "conversion,img,jpg,picture,photo" @@ -4755,6 +4837,9 @@ title = "Image to PDF" 4 = "Merge into single PDF" 5 = "Convert to separate PDFs" +[infoBanner] +dismiss = "Dismiss" + [invite] acceptError = "Failed to create account" accountFor = "Creating account for" @@ -5090,6 +5175,7 @@ failed = "OCR operation failed" [ocr.languagePicker] additionalLanguages = "Looking for additional languages?" +loading = "Loading available languages..." viewSetupGuide = "View setup guide →" [ocr.operation] @@ -5226,6 +5312,10 @@ daysRemainingSingular = "{{days}} day remaining" title = "Your 30-Day Pro Trial" trialEnds = "Trial ends {{date}}" +[onboarding.mfa] +authenticationCode = "Authentication code" +qrCodeLoading = "Generating your QR code…" + [onboarding.planOverview] adminBodyLoginDisabled = "Once you enable login mode, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge." adminBodyLoginEnabled = "As an admin, you can manage users, configure settings, and monitor server health. The first {{freeTierLimit}} people on your server get to use Stirling free of charge." @@ -5235,6 +5325,9 @@ userTitle = "Plan Overview" [onboarding.securityCheck] message = "The application has undergone significant changes recently. Your server admin's attention may be required. Please confirm your role to continue." +roleAdmin = "Admin" +rolePlaceholder = "Confirm your role" +roleUser = "User" [onboarding.serverLicense] freeBody = "Our Open-Core licensing permits up to {{freeTierLimit}} users for free per server. To scale uninterrupted, we recommend the Stirling Server plan - unlimited seats and SSO support for $99/server/mo." @@ -5360,6 +5453,17 @@ title = "Page Editor" zoomIn = "Zoom In" zoomOut = "Zoom Out" +[pageEditor.emptyState] +body = "Add files to start editing pages" +title = "No PDF files loaded" + +[pageEditor.toolbar] +delete = "Delete Selected" +redo = "Redo" +rotateLeft = "Rotate Selected Left" +rotateRight = "Rotate Selected Right" +undo = "Undo" + [pageExtracter] header = "Extract Pages" placeholder = "(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" @@ -5371,12 +5475,14 @@ addBorder = "Add Borders" borderWidth = "Border Thickness" bottom = "Bottom Margin" cols = "Columns" +colsPlaceholder = "Enter columns" header = "Multi Page Layout" innerMargin = "Inner Margin" left = "Left Margin" pagesPerSheet = "Pages per sheet:" right = "Right Margin" rows = "Rows" +rowsPlaceholder = "Enter rows" submit = "Submit" tags = "merge,composite,single-view,organize" title = "Multi Page Layout" @@ -5418,6 +5524,14 @@ innerMarginTooLarge = "Inner margin is too large for the selected layout." outerHorizontalMarginsTooLarge = "Left/Right margins are too large for this page size." outerVerticalMarginsTooLarge = "Top/Bottom margins are too large for this page size." +[pageLayout.margins] +borderThicknessPlaceholder = "Enter border thickness" +bottomPlaceholder = "Enter bottom margin" +innerPlaceholder = "Enter inner margin" +leftPlaceholder = "Enter left margin" +rightPlaceholder = "Enter right margin" +topPlaceholder = "Enter top margin" + [pageLayout.marginsBorders.tooltip.borders] text = "Enable borders to draw lines around each placed page. This can help visual separation or trimming." title = "Add Borders" @@ -6071,6 +6185,26 @@ pdfTools = "Basic PDF Tools" priority = "Priority Support" title = "Feature" +[plan.features] +allPdfOperations = "All PDF operations" +auditing = "Auditing" +communitySupport = "Community support" +customPdfMetadata = "Custom PDF metadata" +editingTextInPdfs = "Editing text in pdfs" +externalDatabase = "External Database" +googleDriveIntegration = "Google drive integration" +prometheusSupport = "Prometheus Support" +regularUpdates = "Regular updates" +saml = "SAML" +secureLoginSupport = "Secure Login Support" +selfHostedDeployment = "Self-hosted deployment" +sso = "SSO" +unlimitedUsers = "Unlimited users" +upToFiveUsers = "Up to 5 users" +upToFiveUsersLowercase = "up to 5 users" +usageTracking = "Usage tracking" +usersLimitedToSeats = "Users limited to seats" + [plan.free] forever = "Forever free" highlight1 = "Limited Tool Usage Per week" @@ -6079,6 +6213,22 @@ highlight3 = "Community support" included = "Included" name = "Free" +[plan.highlights] +advancedIntegrations = "Advanced integrations" +allBasicFeatures = "All basic features" +cancelAnytime = "Cancel anytime" +customPdfMetadata = "Custom PDF metadata" +editingTextInPdfsCaps = "Editing text in PDFs" +enterpriseFeatures = "Enterprise features (SAML, Auditing)" +perSeatLicensing = "Per-seat licensing" +saveWithAnnualBilling = "Save with annual billing" +selfHosted = "Self-hosted" +selfHostedOnInfrastructure = "Self-hosted on your infrastructure" +ssoOAuth = "SSO (OAuth2/OIDC)" +unlimitedUsers = "Unlimited users" +upToFiveUsers = "Up to 5 users" +usageTrackingPrometheus = "Usage tracking & Prometheus" + [plan.licenseWarning] body = "You have {{total}} users but the free tier only supports {{limit}} per server. Upgrade to keep Stirling PDF running smoothly." cta = "See plans" @@ -6389,6 +6539,7 @@ allSessions = "All Sessions" allTools = "Tools" automate = "Automate" back = "Back" +backToAllTools = "Back to all tools" certSign = "Certificate Sign" completedSessions = "Completed Sessions" completedTab = "Completed" @@ -7060,6 +7211,9 @@ bullet3 = "Critical for approval workflows and legal chains of custody" description = "The order you specify when creating the session determines who signs first." title = "Signature Order" +[settings] +close = "Close" + [settings.configuration] advanced = "Advanced" database = "Database" @@ -7542,6 +7696,7 @@ sharedDescription = "All users can see and use these signatures." sharedHeading = "Shared Signatures" tempStorageDescription = "Signatures are stored in your browser only. They will be lost if you clear browser data or switch browsers." tempStorageTitle = "Temporary browser storage" +use = "Use signature" [sign.saved.status] saved = "Saved" @@ -8122,9 +8277,18 @@ center = "Center" left = "Left" right = "Right" +[textInput] +clear = "Clear input" + [theme] toggle = "Toggle Theme" +[time.relative] +daysAgo = "{{count}}d ago" +hoursAgo = "{{count}}h ago" +justNow = "just now" +minutesAgo = "{{count}}m ago" + [timestampPdf] completed = "PDF timestamped successfully" desc = "Add an RFC 3161 document timestamp to your PDF using a trusted Time Stamp Authority (TSA) server." @@ -8151,11 +8315,9 @@ label = "Select a TSA server" [timestampPdf.steps] settings = "Settings" -[time.relative] -daysAgo = "{{count}}d ago" -hoursAgo = "{{count}}h ago" -justNow = "just now" -minutesAgo = "{{count}}m ago" +[toast] +dismiss = "Dismiss" +toggleDetails = "Toggle details" [tool] endpointUnavailable = "This tool is unavailable on your server." @@ -8243,6 +8405,9 @@ verification = "Verification" noSearchResults = "No tools found" noTools = "No tools available" +[tooltip] +close = "Close tooltip" + [unlockPDFForms] description = "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable." filenamePrefix = "unlocked_forms" @@ -8269,6 +8434,7 @@ breakingChangesDefault = "This version contains breaking changes." breakingChangesDetected = "Breaking Changes Detected" breakingChangesMessage = "Some versions contain breaking changes. Please review the migration guides below before updating." close = "Close" +closeModal = "Close update modal" current = "Current Version" defaultRecommendation = "This update contains important fixes and improvements." downloadLatest = "Download Latest" @@ -8520,6 +8686,9 @@ typeInsertText = "Insert Text" typeReplaceText = "Replace Text" viewComment = "View comment" +[viewer.error] +noFileProvided = "Error: No file provided to viewer" + [viewer.formBar] apply = "Apply Changes" dismiss = "Dismiss" @@ -8529,6 +8698,9 @@ title = "Form Fields" unsavedBadge = "Unsaved" unsavedDesc = "You have unsaved changes" +[viewer.link] +delete = "Delete link" + [viewer.nonPdf] columnDefault = "Column {{index}}" convertToPdf = "Convert to PDF" @@ -8544,6 +8716,19 @@ renderMarkdown = "Render markdown" sortedBy = "Sorted by: {{column}}" textStats = "{{lines}} lines · {{size}}" +[viewer.redaction] +removeMark = "Remove this mark" + +[viewer.search] +clear = "Clear search" +close = "Close search" +next = "Next result" +previous = "Previous result" +resultsOf = "of {{total}}" + +[viewer.signature] +delete = "Delete signature" + [viewPdf] header = "View PDF" tags = "view,read,annotate,text,image,highlight,edit" @@ -8792,6 +8977,37 @@ toggleSidebar = "Toggle Sidebar" toggleTheme = "Toggle Theme" viewer = "Viewer" +[workflow.participant] +accessExpired = "Your access to this document has expired." +certificateFile = "Certificate File" +certificateFilePlaceholder = "Select certificate file" +certificatePassword = "Certificate Password" +certificateType = "Certificate Type" +certTypeJks = "JKS Keystore" +certTypeP12 = "P12/PKCS12 Certificate" +certTypeServer = "Server Certificate (if available)" +completedDeclined = "You have declined this document." +completedSigned = "You have signed this document." +decline = "Decline" +downloadDocument = "Download Document" +dueDate = "Due Date: {{date}}" +errorTitle = "Error" +from = "From: {{name}}" +loadingSession = "Loading session..." +location = "Location" +locationPlaceholder = "e.g., San Francisco, CA" +pageNumber = "Page Number (optional)" +reason = "Reason" +reasonPlaceholder = "e.g., Document approval" +sessionNotFound = "Session not found or access denied." +signDocument = "Sign Document" +statusDeclined = "Declined" +statusNotified = "Notified" +statusPending = "Pending" +statusSigned = "Signed" +statusViewed = "Viewed" +submitSignature = "Submit Signature" + [workspace] title = "Workspace" diff --git a/frontend/editor/src/core/components/annotation/shared/ColorControl.tsx b/frontend/editor/src/core/components/annotation/shared/ColorControl.tsx index ff2a3ff45..ca41f0f6a 100644 --- a/frontend/editor/src/core/components/annotation/shared/ColorControl.tsx +++ b/frontend/editor/src/core/components/annotation/shared/ColorControl.tsx @@ -8,6 +8,7 @@ import { Group, } from "@mantine/core"; import { useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import ColorizeIcon from "@mui/icons-material/Colorize"; // safari and firefox do not support the eye dropper API, only edge, chrome and opera do. @@ -33,6 +34,7 @@ export function ColorControl({ label, disabled = false, }: ColorControlProps) { + const { t } = useTranslation(); const [opened, setOpened] = useState(false); // Buffer the colour locally so the picker stays responsive during drag. // Only propagate to the parent (which triggers expensive annotation updates) @@ -111,7 +113,9 @@ export function ColorControl({ /> {supportsEyeDropper && ( - + = ({ onImageChange, disabled = false, }) => { + const { t } = useTranslation(); const [, setImageData] = useState(null); const handleImageUpload = async (file: File | null) => { @@ -57,9 +59,12 @@ export const ImageTool: React.FC = ({ diff --git a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx index 5fc62680b..a7aae35f2 100644 --- a/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx +++ b/frontend/editor/src/core/components/fileEditor/AddFileCard.tsx @@ -134,7 +134,7 @@ const AddFileCard = ({ )} @@ -442,14 +511,24 @@ const ParticipantView: React.FC = ({ token }) => { {participant.hasCompleted && ( } color="green"> - You have {participant.status === "SIGNED" ? "signed" : "declined"}{" "} - this document. + {participant.status === "SIGNED" + ? t( + "workflow.participant.completedSigned", + "You have signed this document.", + ) + : t( + "workflow.participant.completedDeclined", + "You have declined this document.", + )} )} {participant.isExpired && ( } color="orange"> - Your access to this document has expired. + {t( + "workflow.participant.accessExpired", + "Your access to this document has expired.", + )} )} diff --git a/frontend/editor/src/proprietary/constants/planConstants.ts b/frontend/editor/src/proprietary/constants/planConstants.ts index 6a03a72b7..bb8a66ac4 100644 --- a/frontend/editor/src/proprietary/constants/planConstants.ts +++ b/frontend/editor/src/proprietary/constants/planConstants.ts @@ -1,100 +1,219 @@ +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import type { PlanFeature } from "@app/types/license"; /** * Shared plan feature definitions for Stirling PDF Self-Hosted - * Used by both dynamic (Stripe) and static (fallback) plan displays + * Used by both dynamic (Stripe) and static (fallback) plan displays. + * + * These are exposed as hooks so that every feature/highlight string can be + * localized via `t()` while preserving the original shape. */ -export const PLAN_FEATURES = { - FREE: [ - { name: "Self-hosted deployment", included: true }, - { name: "All PDF operations", included: true }, - { name: "Secure Login Support", included: true }, - { name: "Community support", included: true }, - { name: "Regular updates", included: true }, - { name: "up to 5 users", included: true }, - { name: "Unlimited users", included: false }, - { name: "Google drive integration", included: false }, - { name: "External Database", included: false }, - { name: "Editing text in pdfs", included: false }, - { name: "Users limited to seats", included: false }, - { name: "SSO", included: false }, - { name: "SAML", included: false }, - { name: "Auditing", included: false }, - { name: "Usage tracking", included: false }, - { name: "Prometheus Support", included: false }, - { name: "Custom PDF metadata", included: false }, - ] as PlanFeature[], +export interface PlanFeaturesMap { + FREE: PlanFeature[]; + SERVER: PlanFeature[]; + ENTERPRISE: PlanFeature[]; +} - SERVER: [ - { name: "Self-hosted deployment", included: true }, - { name: "All PDF operations", included: true }, - { name: "Secure Login Support", included: true }, - { name: "Community support", included: true }, - { name: "Regular updates", included: true }, - { name: "Up to 5 users", included: false }, - { name: "Unlimited users", included: true }, - { name: "Google drive integration", included: true }, - { name: "External Database", included: true }, - { name: "Editing text in pdfs", included: true }, - { name: "Users limited to seats", included: false }, - { name: "SSO", included: true }, - { name: "SAML", included: false }, - { name: "Auditing", included: false }, - { name: "Usage tracking", included: false }, - { name: "Prometheus Support", included: false }, - { name: "Custom PDF metadata", included: false }, - ] as PlanFeature[], +export interface PlanHighlightsMap { + FREE: string[]; + SERVER_MONTHLY: string[]; + SERVER_YEARLY: string[]; + ENTERPRISE_MONTHLY: string[]; + ENTERPRISE_YEARLY: string[]; +} - ENTERPRISE: [ - { name: "Self-hosted deployment", included: true }, - { name: "All PDF operations", included: true }, - { name: "Secure Login Support", included: true }, - { name: "Community support", included: true }, - { name: "Regular updates", included: true }, - { name: "up to 5 users", included: false }, - { name: "Unlimited users", included: false }, - { name: "Google drive integration", included: true }, - { name: "External Database", included: true }, - { name: "Editing text in pdfs", included: true }, - { name: "Users limited to seats", included: true }, - { name: "SSO", included: true }, - { name: "SAML", included: true }, - { name: "Auditing", included: true }, - { name: "Usage tracking", included: true }, - { name: "Prometheus Support", included: true }, - { name: "Custom PDF metadata", included: true }, - ] as PlanFeature[], -} as const; +export const usePlanFeatures = (): PlanFeaturesMap => { + const { t } = useTranslation(); -export const PLAN_HIGHLIGHTS = { - FREE: ["Up to 5 users", "Self-hosted", "All basic features"], - SERVER_MONTHLY: [ - "Self-hosted on your infrastructure", - "Unlimited users", - "Advanced integrations", - "SSO (OAuth2/OIDC)", - "Editing text in PDFs", - "Cancel anytime", - ], - SERVER_YEARLY: [ - "Self-hosted on your infrastructure", - "Unlimited users", - "Advanced integrations", - "SSO (OAuth2/OIDC)", - "Editing text in PDFs", - "Save with annual billing", - ], - ENTERPRISE_MONTHLY: [ - "Enterprise features (SAML, Auditing)", - "Usage tracking & Prometheus", - "Custom PDF metadata", - "Per-seat licensing", - ], - ENTERPRISE_YEARLY: [ - "Enterprise features (SAML, Auditing)", - "Usage tracking & Prometheus", - "Custom PDF metadata", - "Save with annual billing", - ], -} as const; + return useMemo(() => { + const selfHostedDeployment = t( + "plan.features.selfHostedDeployment", + "Self-hosted deployment", + ); + const allPdfOperations = t( + "plan.features.allPdfOperations", + "All PDF operations", + ); + const secureLoginSupport = t( + "plan.features.secureLoginSupport", + "Secure Login Support", + ); + const communitySupport = t( + "plan.features.communitySupport", + "Community support", + ); + const regularUpdates = t("plan.features.regularUpdates", "Regular updates"); + const upToFiveUsersLowercase = t( + "plan.features.upToFiveUsersLowercase", + "up to 5 users", + ); + const upToFiveUsers = t("plan.features.upToFiveUsers", "Up to 5 users"); + const unlimitedUsers = t("plan.features.unlimitedUsers", "Unlimited users"); + const googleDriveIntegration = t( + "plan.features.googleDriveIntegration", + "Google drive integration", + ); + const externalDatabase = t( + "plan.features.externalDatabase", + "External Database", + ); + const editingTextInPdfs = t( + "plan.features.editingTextInPdfs", + "Editing text in pdfs", + ); + const usersLimitedToSeats = t( + "plan.features.usersLimitedToSeats", + "Users limited to seats", + ); + const sso = t("plan.features.sso", "SSO"); + const saml = t("plan.features.saml", "SAML"); + const auditing = t("plan.features.auditing", "Auditing"); + const usageTracking = t("plan.features.usageTracking", "Usage tracking"); + const prometheusSupport = t( + "plan.features.prometheusSupport", + "Prometheus Support", + ); + const customPdfMetadata = t( + "plan.features.customPdfMetadata", + "Custom PDF metadata", + ); + + return { + FREE: [ + { name: selfHostedDeployment, included: true }, + { name: allPdfOperations, included: true }, + { name: secureLoginSupport, included: true }, + { name: communitySupport, included: true }, + { name: regularUpdates, included: true }, + { name: upToFiveUsersLowercase, included: true }, + { name: unlimitedUsers, included: false }, + { name: googleDriveIntegration, included: false }, + { name: externalDatabase, included: false }, + { name: editingTextInPdfs, included: false }, + { name: usersLimitedToSeats, included: false }, + { name: sso, included: false }, + { name: saml, included: false }, + { name: auditing, included: false }, + { name: usageTracking, included: false }, + { name: prometheusSupport, included: false }, + { name: customPdfMetadata, included: false }, + ], + SERVER: [ + { name: selfHostedDeployment, included: true }, + { name: allPdfOperations, included: true }, + { name: secureLoginSupport, included: true }, + { name: communitySupport, included: true }, + { name: regularUpdates, included: true }, + { name: upToFiveUsers, included: false }, + { name: unlimitedUsers, included: true }, + { name: googleDriveIntegration, included: true }, + { name: externalDatabase, included: true }, + { name: editingTextInPdfs, included: true }, + { name: usersLimitedToSeats, included: false }, + { name: sso, included: true }, + { name: saml, included: false }, + { name: auditing, included: false }, + { name: usageTracking, included: false }, + { name: prometheusSupport, included: false }, + { name: customPdfMetadata, included: false }, + ], + ENTERPRISE: [ + { name: selfHostedDeployment, included: true }, + { name: allPdfOperations, included: true }, + { name: secureLoginSupport, included: true }, + { name: communitySupport, included: true }, + { name: regularUpdates, included: true }, + { name: upToFiveUsersLowercase, included: false }, + { name: unlimitedUsers, included: false }, + { name: googleDriveIntegration, included: true }, + { name: externalDatabase, included: true }, + { name: editingTextInPdfs, included: true }, + { name: usersLimitedToSeats, included: true }, + { name: sso, included: true }, + { name: saml, included: true }, + { name: auditing, included: true }, + { name: usageTracking, included: true }, + { name: prometheusSupport, included: true }, + { name: customPdfMetadata, included: true }, + ], + }; + }, [t]); +}; + +export const usePlanHighlights = (): PlanHighlightsMap => { + const { t } = useTranslation(); + + return useMemo(() => { + const selfHostedOnInfrastructure = t( + "plan.highlights.selfHostedOnInfrastructure", + "Self-hosted on your infrastructure", + ); + const unlimitedUsers = t( + "plan.highlights.unlimitedUsers", + "Unlimited users", + ); + const advancedIntegrations = t( + "plan.highlights.advancedIntegrations", + "Advanced integrations", + ); + const ssoOAuth = t("plan.highlights.ssoOAuth", "SSO (OAuth2/OIDC)"); + const editingTextInPdfsCaps = t( + "plan.highlights.editingTextInPdfsCaps", + "Editing text in PDFs", + ); + const enterpriseFeatures = t( + "plan.highlights.enterpriseFeatures", + "Enterprise features (SAML, Auditing)", + ); + const usageTrackingPrometheus = t( + "plan.highlights.usageTrackingPrometheus", + "Usage tracking & Prometheus", + ); + const customPdfMetadata = t( + "plan.highlights.customPdfMetadata", + "Custom PDF metadata", + ); + const saveWithAnnualBilling = t( + "plan.highlights.saveWithAnnualBilling", + "Save with annual billing", + ); + + return { + FREE: [ + t("plan.highlights.upToFiveUsers", "Up to 5 users"), + t("plan.highlights.selfHosted", "Self-hosted"), + t("plan.highlights.allBasicFeatures", "All basic features"), + ], + SERVER_MONTHLY: [ + selfHostedOnInfrastructure, + unlimitedUsers, + advancedIntegrations, + ssoOAuth, + editingTextInPdfsCaps, + t("plan.highlights.cancelAnytime", "Cancel anytime"), + ], + SERVER_YEARLY: [ + selfHostedOnInfrastructure, + unlimitedUsers, + advancedIntegrations, + ssoOAuth, + editingTextInPdfsCaps, + saveWithAnnualBilling, + ], + ENTERPRISE_MONTHLY: [ + enterpriseFeatures, + usageTrackingPrometheus, + customPdfMetadata, + t("plan.highlights.perSeatLicensing", "Per-seat licensing"), + ], + ENTERPRISE_YEARLY: [ + enterpriseFeatures, + usageTrackingPrometheus, + customPdfMetadata, + saveWithAnnualBilling, + ], + }; + }, [t]); +}; diff --git a/frontend/editor/src/proprietary/contexts/CheckoutContext.tsx b/frontend/editor/src/proprietary/contexts/CheckoutContext.tsx index 5c6406639..45912667f 100644 --- a/frontend/editor/src/proprietary/contexts/CheckoutContext.tsx +++ b/frontend/editor/src/proprietary/contexts/CheckoutContext.tsx @@ -24,6 +24,10 @@ import { import { useLicense } from "@app/contexts/LicenseContext"; import { isSupabaseConfigured } from "@app/services/supabaseClient"; import { getPreferredCurrency } from "@app/utils/currencyDetection"; +import { + usePlanFeatures, + usePlanHighlights, +} from "@app/constants/planConstants"; export interface CheckoutOptions { minimumSeats?: number; // Override calculated seats for enterprise @@ -57,6 +61,8 @@ export const CheckoutProvider: React.FC = ({ }) => { const { t, i18n } = useTranslation(); const { refetchLicense } = useLicense(); + const planFeatures = usePlanFeatures(); + const planHighlights = usePlanHighlights(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedPlanGroup, setSelectedPlanGroup] = @@ -85,7 +91,11 @@ export const CheckoutProvider: React.FC = ({ try { setPlansLoading(true); - const response = await licenseService.getPlans(currency); + const response = await licenseService.getPlans( + planFeatures, + planHighlights, + currency, + ); setPlans(response.plans); setPlansLoaded(true); } catch (error) { @@ -95,7 +105,7 @@ export const CheckoutProvider: React.FC = ({ setPlansLoading(false); } }, - [plansLoading], + [plansLoading, planFeatures, planHighlights], ); const refetchPlans = useCallback(() => { diff --git a/frontend/editor/src/proprietary/hooks/usePlans.ts b/frontend/editor/src/proprietary/hooks/usePlans.ts index 8a9d0c13f..45b7d8988 100644 --- a/frontend/editor/src/proprietary/hooks/usePlans.ts +++ b/frontend/editor/src/proprietary/hooks/usePlans.ts @@ -1,8 +1,12 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import licenseService, { PlanTier, PlansResponse, } from "@app/services/licenseService"; +import { + usePlanFeatures, + usePlanHighlights, +} from "@app/constants/planConstants"; export interface UsePlansReturn { plans: PlanTier[]; @@ -12,16 +16,22 @@ export interface UsePlansReturn { } export const usePlans = (currency: string = "gbp"): UsePlansReturn => { + const planFeatures = usePlanFeatures(); + const planHighlights = usePlanHighlights(); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const fetchPlans = async () => { + const fetchPlans = useCallback(async () => { try { setLoading(true); setError(null); - const data: PlansResponse = await licenseService.getPlans(currency); + const data: PlansResponse = await licenseService.getPlans( + planFeatures, + planHighlights, + currency, + ); setPlans(data.plans); } catch (err) { console.error("Error fetching plans:", err); @@ -29,11 +39,11 @@ export const usePlans = (currency: string = "gbp"): UsePlansReturn => { } finally { setLoading(false); } - }; + }, [currency, planFeatures, planHighlights]); useEffect(() => { fetchPlans(); - }, [currency]); + }, [fetchPlans]); return { plans, diff --git a/frontend/editor/src/proprietary/services/licenseService.ts b/frontend/editor/src/proprietary/services/licenseService.ts index 600498b33..e87f304eb 100644 --- a/frontend/editor/src/proprietary/services/licenseService.ts +++ b/frontend/editor/src/proprietary/services/licenseService.ts @@ -1,7 +1,10 @@ import apiClient from "@app/services/apiClient"; import { supabase, isSupabaseConfigured } from "@app/services/supabaseClient"; import { getCheckoutMode } from "@app/utils/protocolDetection"; -import { PLAN_FEATURES, PLAN_HIGHLIGHTS } from "@app/constants/planConstants"; +import type { + PlanFeaturesMap, + PlanHighlightsMap, +} from "@app/constants/planConstants"; import type { LicenseInfo, PlanFeature } from "@app/types/license"; export interface PlanTier { @@ -102,9 +105,18 @@ const SELF_HOSTED_LOOKUP_KEYS = [ const licenseService = { /** - * Get available plans with pricing for the specified currency + * Get available plans with pricing for the specified currency. + * + * Feature and highlight labels are passed in by the caller (a React hook / + * context that resolves them via `usePlanFeatures` / `usePlanHighlights`), + * which keeps this service free of React/i18n dependencies while still + * returning fully localized plan content. */ - async getPlans(currency: string = "usd"): Promise { + async getPlans( + planFeatures: PlanFeaturesMap, + planHighlights: PlanHighlightsMap, + currency: string = "usd", + ): Promise { try { // Check if Supabase is configured if (!isSupabaseConfigured || !supabase) { @@ -179,8 +191,8 @@ const licenseService = { currency: currencySymbol, period: "/month", popular: false, - features: PLAN_FEATURES.SERVER, - highlights: PLAN_HIGHLIGHTS.SERVER_MONTHLY, + features: planFeatures.SERVER, + highlights: planHighlights.SERVER_MONTHLY, }, { id: "selfhosted:server:yearly", @@ -190,8 +202,8 @@ const licenseService = { currency: currencySymbol, period: "/year", popular: true, - features: PLAN_FEATURES.SERVER, - highlights: PLAN_HIGHLIGHTS.SERVER_YEARLY, + features: planFeatures.SERVER, + highlights: planHighlights.SERVER_YEARLY, }, { id: "selfhosted:enterprise:monthly", @@ -203,8 +215,8 @@ const licenseService = { period: "/month", popular: false, requiresSeats: true, - features: PLAN_FEATURES.ENTERPRISE, - highlights: PLAN_HIGHLIGHTS.ENTERPRISE_MONTHLY, + features: planFeatures.ENTERPRISE, + highlights: planHighlights.ENTERPRISE_MONTHLY, }, { id: "selfhosted:enterprise:yearly", @@ -216,8 +228,8 @@ const licenseService = { period: "/year", popular: false, requiresSeats: true, - features: PLAN_FEATURES.ENTERPRISE, - highlights: PLAN_HIGHLIGHTS.ENTERPRISE_YEARLY, + features: planFeatures.ENTERPRISE, + highlights: planHighlights.ENTERPRISE_YEARLY, }, ]; @@ -240,8 +252,8 @@ const licenseService = { currency: currencySymbol, period: "", popular: false, - features: PLAN_FEATURES.FREE, - highlights: PLAN_HIGHLIGHTS.FREE, + features: planFeatures.FREE, + highlights: planHighlights.FREE, }; const allPlans = [freePlan, ...validPlans];