diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e0a269b..f58cb41 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -7,7 +7,30 @@ "continue": "Continue", "back": "Back", "save": "Save", - "cancel": "Cancel" + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "next": "Next", + "dismiss": "Dismiss", + "or": "Or", + "and": "and", + "close": "Close", + "redirecting": "Redirecting…", + "readOnlyAccess": "Read-only access for this organization.", + "errorGeneric": "Something went wrong", + "loadingEllipsis": "Loading...", + "search": "Search", + "action": "Action", + "status": "Status", + "name": "Name", + "email": "Email", + "date": "Date", + "organization": "Organization", + "backToApp": "← Back to app", + "copied": "Copied", + "copyLink": "Copy link", + "none": "None", + "preview": "Preview" }, "language": { "label": "Language", @@ -67,7 +90,47 @@ "signedIn": "Signed in", "switchOrganization": "Switch organization", "subscriptions": "Subscriptions", - "account": "Account" + "account": "Account", + "emailPlaceholder": "you@example.com", + "passwordPlaceholder": "••••••••", + "namePlaceholder": "John Doe", + "termsIntro": "By signing up, you agree to our", + "errorRegistrationFailed": "Registration failed", + "errorLoginFailed": "Login failed", + "errorCreateOrganization": "Failed to create organization", + "acceptInviteTitle": "Accept invitation", + "loadingInvitation": "Loading invitation...", + "invalidInvitationLink": "Invalid invitation link", + "invitationAlreadyAccepted": "This invitation is already accepted. You can log in now.", + "errorLoadInvitation": "Could not load invitation", + "organizationLabel": "Organization:", + "emailLabel": "Email:", + "nameRequired": "Name is required", + "passwordMinLength8": "Password must be at least 8 characters", + "passwordsDoNotMatch": "Passwords do not match", + "labelName": "Name", + "labelCreatePassword": "Create password", + "labelConfirmPassword": "Confirm password", + "activateAccount": "Activate account", + "invitationAcceptedRedirect": "Invitation Accepted. Redirecting to login...", + "errorAcceptInvitation": "Could not accept invitation", + "alreadyHaveAccess": "Already have access?", + "goToLogin": "Go to login", + "acceptOrganizationTitle": "Accept organization invitation", + "alreadyHaveAccount": "Already have an account?", + "invitedBy": "Invited by:", + "ownerEmail": "Owner email", + "activateOrganization": "Activate organization", + "organizationAcceptedRedirect": "Invitation accepted. Redirecting to login...", + "stepAccount": "Account", + "stepOrganization": "Organization", + "organizationName": "Organization name", + "organizationNamePlaceholder": "Sunshine Dental Clinic", + "organizationEmail": "Organization email", + "organizationEmailPlaceholder": "contact@sunshineclinic.com", + "organizationType": "Organization type", + "dentalClinic": "Dental Clinic", + "dentalLab": "Dental Lab" }, "landing": { "heroTitle": "Connect Dental Clinics & Labs", @@ -106,5 +169,398 @@ "organizationEmailInvalid": "Please enter a valid organization email", "organizationTypeRequired": "Please select organization type", "passwordsDoNotMatch": "Passwords don't match" + }, + "today": { + "welcomeBack": "Welcome back!!", + "noSubscriptionNotice": "This organization does not have an active subscription yet.", + "choosePlanLink": "Choose a plan", + "noSubscriptionCta": "to start the purchase process.", + "cardTodaysAppointments": "Today's Appointments", + "cardActivePatients": "Active Patients", + "cardNewLabCase": "New Lab Case", + "cardTodayInvoices": "Today invoices" + }, + "staff": { + "redirecting": "Redirecting…", + "title": "Staff Management", + "subtitle": "Invite teammates, set tab access, and stay within your plan seat limit.", + "inviteMember": "Invite member", + "seatsLabel": "Seats:", + "unlimitedPlan": "(unlimited plan)", + "seatLimitReached": "Plan seat limit reached for this organization.", + "noActivePlan": "No active plan selected for this organization. Choose a subscription plan to invite members.", + "invitedPending": "Invitation is pending until they open the link, set a password, and log in.", + "invitedAccepted": "Invitation was accepted immediately.", + "inviteLinkHeading": "Invite link", + "shareLinkHint": "Share this link manually via SMS or email. A new link is generated if the previous one expired or was lost.", + "loadingTeam": "Loading team…", + "tableName": "Name", + "tableEmail": "Email", + "tableRole": "Role", + "tableStatus": "Status", + "tableAccess": "Access", + "tableAction": "Action", + "roleOwner": "Owner", + "roleStaff": "Staff", + "statusActive": "Active", + "statusPending": "Pending", + "statusDisabled": "Disabled", + "statusExpired": "Expired", + "allFeatures": "All features", + "inviteModalTitle": "Invite team member", + "stepOf": "Step {step} of 2", + "permissionView": "View", + "permissionEdit": "Edit", + "labelEmail": "Email", + "labelDisplayName": "Display name", + "tabAccess": "Tab access", + "sendInvite": "Send invite", + "skipForNow": "Skip for now", + "enableModalTitle": "Enable team member", + "enableConfirm": "Enable {name} ({email})?", + "enableBullet1": "They can sign in to this organization again with their existing account.", + "enableBullet2": "No new invitation is sent and no data was removed while they were disabled.", + "enableBullet3": "Enabling uses one seat on your plan.", + "noSeatsAvailable": "No seats are available. Disable another member or upgrade your plan before enabling this person.", + "enableMemberButton": "Enable member", + "disableModalTitle": "Disable team member", + "disableConfirm": "Disable {name} ({email})?", + "disableBullet1": "They will not be able to sign in to this organization.", + "disableBullet2": "No data will be removed.", + "disableBullet3": "Disabling frees one seat on your plan so you can invite someone else.", + "disableMemberButton": "Disable member", + "editModalTitle": "Edit member", + "loadingWorkingHours": "Loading working hours…", + "errorLoadStaff": "Failed to load staff.", + "errorCopyInvite": "Could not copy invitation link.", + "errorSendInvite": "Failed to send invitation.", + "errorLoadWorkingHours": "Failed to load working hours.", + "successMemberUpdated": "Member updated.", + "errorUpdateMember": "Failed to update member.", + "errorDeleteNotImplemented": "Delete is not implemented yet.", + "successMemberDisabled": "{name} was disabled. A seat is now available.", + "errorDisableMember": "Failed to disable member.", + "successMemberEnabled": "{name} was enabled and can sign in again.", + "errorEnableMember": "Failed to enable member.", + "successInvited": "{name} ({email}) was invited.", + "copyInviteLink": "Copy invitation link", + "copyInviteLinkTitle": "Copy invitation link (generates a new link if needed)", + "enableMemberAria": "Enable member", + "enableMemberTitle": "Enable member (uses a seat)", + "disableMemberAria": "Disable member", + "disableMemberTitle": "Disable member (frees a seat)", + "editMemberAria": "Edit member", + "deleteMemberAria": "Delete member", + "deleteMemberTitle": "Delete member (not implemented)", + "features": { + "featureToday": "Today", + "featureStaff": "Staff", + "featureOrganizations": "Organizations", + "featureClinics": "Clinics", + "featureLabs": "Labs", + "featurePatients": "Patients", + "featureAppointment": "Appointment", + "featureTreatment": "Treatment", + "featureBilling": "Billing", + "featureReports": "Reports", + "noTabAccess": "No tab access", + "readOnlySuffix": "(Read only)" + }, + "workingHours": { + "recommendedTitle": "Working hours recommended", + "recommendedBody": "Staff with treatment edit access appear as provider columns in Appointments. Set their weekly hours so the schedule grid shows the right bookable times.", + "intro": "Set weekly working hours for this provider. The appointments grid uses these hours to show bookable time slots.", + "workingDay": "Working day", + "start": "Start", + "end": "End", + "removeShift": "Remove shift", + "addShift": "Add shift", + "autoRepeatWeekly": "Repeat these hours at the start of each week (copy forward on Monday)", + "weekdayMon": "Mon", + "weekdayTue": "Tue", + "weekdayWed": "Wed", + "weekdayThu": "Thu", + "weekdayFri": "Fri", + "weekdaySat": "Sat", + "weekdaySun": "Sun", + "validationNeedsShift": "{day} needs at least one shift or should be marked off.", + "validationEndAfterStart": "{day} shift end time must be after start time.", + "validationOverlap": "{day} shifts cannot overlap." + } + }, + "patients": { + "title": "Patients", + "newPatient": "New Patient", + "errorLoadPatients": "Failed to load patients.", + "successPatientSaved": "Patient {firstName} {lastName} was saved successfully.", + "errorSavePatient": "Failed to save patient.", + "firstName": "First name", + "lastName": "Last name", + "phone": "Phone", + "savePatient": "Save Patient", + "dialogTitle": "New patient", + "searchPlaceholder": "Search patients by name, phone, email", + "loadingPatients": "Loading patients...", + "noResults": "No patients found for this search.", + "noContact": "No contact", + "selectPatient": "Select a patient to view details.", + "phoneLabel": "Phone:", + "emailLabel": "Email:", + "statusLabel": "Status:", + "statusActive": "Active", + "statusInactive": "Inactive", + "emptyValue": "-" + }, + "appointments": { + "title": "Appointments", + "subtitle": "Search a patient, pick a date, then click a time slot under a provider to book.", + "loadingSchedule": "Loading schedule…", + "infoPastViewOnly": "Past appointments are view-only.", + "infoSelectPatient": "Select a patient before booking.", + "errorOutsideHours": "This appointment falls outside the provider's current working hours and cannot be edited.", + "successUpdated": "Appointment updated.", + "successSaved": "Appointment saved.", + "errorUpdate": "Could not update appointment.", + "errorSave": "Could not save appointment.", + "confirmRemove": "Remove this appointment?", + "successRemoved": "Appointment removed.", + "errorDelete": "Could not delete appointment.", + "errorLoadSchedule": "Failed to load schedule.", + "successPatientSaved": "Patient {firstName} {lastName} was saved.", + "searchPlaceholder": "Search existing patients", + "searching": "Searching…", + "searchHint": "Type to search patients by name, phone, or email.", + "noPermissionAdd": "You do not have permission to add patients.", + "editTitle": "Edit appointment", + "newTitle": "New appointment", + "providerLabel": "Provider:", + "patientLabel": "Patient", + "startLabel": "Start", + "endLabel": "End", + "purposeLabel": "Purpose", + "errorSelectPatient": "Select a patient first.", + "errorEndAfterStart": "End time must be after start time.", + "errorPastSchedule": "Cannot schedule in the past.", + "errorPastViewOnly": "Past appointments are view-only.", + "errorMissingDetails": "Missing appointment details.", + "noProviders": "No providers available. Add staff with treatment edit access to see columns here.", + "noWorkingHours": "No working hours are configured for this day. Set provider working hours in Staff management.", + "noHoursSet": "No hours set", + "offToday": "Off today", + "slotOffToday": "Provider is off today", + "slotHoursNotConfigured": "Working hours not configured", + "slotOutsideHours": "Outside working hours", + "slotCannotCreate": "You cannot create appointments", + "slotBookAt": "Book {time}", + "outsideHoursBlocked": "Outside working hours — editing blocked", + "overlappingChoose": "{count} overlapping — click to choose", + "overlapping": "{count} overlapping", + "legend": "Legend", + "overlappingTitle": "Overlapping appointments ({count})", + "purposeConsultation": "Consultation", + "purposeFilling": "Filling", + "purposeEndo": "Endo", + "purposeVisit": "Visit", + "purposeHygiene": "Hygiene" + }, + "treatment": { + "loading": "Loading…", + "noPermissionTitle": "Treatment workspace", + "noPermissionBody": "You do not have permission to view the Treatment tab for this organization.", + "title": "Treatment", + "subtitleEdit": "Document cases for your appointments, save drafts, and send work to linked organizations.", + "subtitleReadOnly": "View-only access — you can review appointments and treatment history but cannot edit.", + "pastDayNotice": "Past days are view-only. You can review appointments and history, but treatment cases cannot be added or changed.", + "selectedPatient": "Selected patient", + "purposeLabel": "Purpose:", + "loadingAppointments": "Loading appointments…", + "selectDayWithAppointment": "Select a day with at least one appointment.", + "confirmDiscard": "You have unsaved changes. Discard them and continue?", + "successDraftSaved": "Treatment draft saved.", + "errorChooseOrg": "Choose at least one active organization to send this case.", + "successCaseSent": "Case sent to selected organizations.", + "successFilesUploaded": "{count} file(s) uploaded successfully.", + "errorLoadAppointments": "Failed to load appointments.", + "errorLoadOrgs": "Failed to load linked organizations.", + "errorLoadHistory": "Failed to load treatment history.", + "errorLoadDraft": "Failed to load treatment draft.", + "errorUpload": "Failed to upload attachments.", + "errorSaveDraft": "Failed to save treatment draft.", + "errorSendCase": "Failed to send case.", + "errorCaseMustSave": "Case must be saved before sending.", + "draftTitle": "Draft · {patientName}", + "hiddenMessage": "Appointments are hidden.", + "showAppointments": "Show appointments", + "appointmentsTitle": "My appointments", + "hideAppointments": "Hide appointments", + "emptyDay": "No appointments assigned to you on this day.", + "casesTitle": "Treatment cases", + "casesSubtitle": "Each case has its own teeth, notes, attachments, and destinations for send.", + "addCase": "Add case", + "caseLabel": "Case {n}", + "comments": "Comments", + "commentsPlaceholder": "Write clinical notes for this case…", + "treatmentType": "Treatment type", + "typeConsultation": "consultation", + "typeFilling": "filling", + "typeEndo": "endo", + "typeVisit": "visit", + "typeHygiene": "hygiene", + "attachments": "Attachments", + "attachFiles": "Attach files for this treatment case", + "chooseFiles": "Choose files", + "sendToOrgs": "Send this case to linked organizations", + "searchOrgsPlaceholder": "Search active organizations...", + "recent": "Recent:", + "noOrgMatch": "No active organization matches your search.", + "sendThisCase": "Send this case", + "saveDraft": "Save treatment draft", + "unsavedChanges": "Unsaved changes", + "draftSaved": "Draft saved", + "sendSavesFirst": "Sending is per case and saves first automatically.", + "historyTitle": "Previous treatments", + "historySubtitle": "Completed treatments for this patient. Each case is listed separately.", + "loadingHistory": "Loading history…", + "historyEmpty": "No prior treatments for this patient.", + "statusLabel": "Status:", + "historyCaseLabel": "Case {n} · {type}", + "teethLabel": "Teeth:", + "teethNone": "None selected", + "reviewDetails": "Review details", + "previewTitle": "Treatment preview", + "previewDraft": "Preview current draft", + "selectAppointment": "Select an appointment to preview its draft.", + "caseCount": "{n} case(s)", + "attachmentCount": "{n} attachment(s)", + "caseSummary": "Case {n}: {type}", + "teethPrefix": "· Teeth", + "moreCases": "+ {n} more case(s)", + "previewDialogTitle": "Treatment preview", + "previewDialogSubtitle": "Review cases, attachments, and send destinations.", + "noCases": "No cases in this treatment.", + "typeLabel": "Type:", + "commentsLabel": "Comments:", + "commentsEmpty": "Comments: —", + "attachFilesShort": "Attach files", + "sendCase": "Send this case", + "sendToLinkedOrgs": "Send to linked organizations", + "noActiveOrgs": "No active linked organizations.", + "confirmSend": "Confirm send", + "toothChartTitle": "FDI tooth chart", + "toothChartHint": "Tap teeth to multi-select. Applies to the active case.", + "selectedLabel": "Selected:", + "selectedEmpty": "—", + "upperArch": "Upper arch", + "lowerArch": "Lower arch", + "toothAria": "FDI tooth {fdi}", + "toothSelectedSuffix": ", selected", + "sentToAt": "Sent to {orgName} at {datetime}", + "fallbackOrgName": "organization" + }, + "organizations": { + "loadingOrganization": "Loading organization...", + "subtitle": "Search organizations, send connection requests to existing accounts, or invitation links when they are not on DyoLink yet.", + "invitationHistory": "Invitation History", + "searchPlaceholder": "Search {counterpart} by name, email, or phone...", + "backToList": "Back to list", + "tableOrganization": "Organization", + "tableOwnerEmail": "Owner email", + "tableDate": "Date", + "tableStatus": "Status", + "tableAction": "Action", + "emptyConnections": "No connections yet. Search to send a connection request or an invitation link.", + "statusInvitationPending": "Invitation pending", + "statusConnectionPending": "Connection request pending", + "statusConnected": "Connected", + "statusDeclined": "Connection request declined", + "statusFound": "Found", + "statusToday": "Today", + "acceptRequest": "Accept connection request", + "declineRequest": "Decline connection request", + "removeConnection": "Remove connection", + "sendRequest": "Send connection request", + "noDirectoryResults": "No organization found in directory search.", + "hideInvitationFields": "Hide invitation fields", + "sendInvitationLink": "Send invitation link", + "counterpartNameLabel": "{counterpart} name", + "ownerEmailLabel": "Owner email", + "sendInvitation": "Send invitation", + "successConnectionSent": "{counterpart} connection request sent.", + "successInviteCreated": "Invitation link created for {email}", + "successLinkCopied": "Invitation link copied to clipboard.", + "successAccepted": "Connection request accepted.", + "successDeclined": "Connection request declined.", + "successRemoved": "Connection removed.", + "historyTitle": "Invitation History", + "loadingHistory": "Loading invitation history...", + "historyEmpty": "No invitations yet.", + "tableInvitationLink": "Invitation link", + "statusPending": "Invitation pending", + "statusAccepted": "Invitation accepted", + "statusRejected": "Invitation rejected", + "statusExpired": "Invitation expired", + "copyInvitationLink": "Copy invitation link", + "copyInvitationLinkTitle": "Copy invitation link (generates a new link if needed)", + "selectorTitle": "Organizations", + "selectorSubtitleWithCreate": "Select an organization to continue, or create a new one.", + "selectorSubtitleSelectOnly": "Select an organization to continue.", + "createOrganization": "Create Organization", + "createAndContinue": "Create and Continue", + "emptyCanCreate": "No organizations found. Create your first one to continue.", + "emptyAskOwner": "No organizations found. Ask an organization owner to invite you.", + "continueArrow": "Continue →", + "planLabel": "Plan: {name} • {maxUsers} users", + "counterpartClinic": "Clinic", + "counterpartLab": "Lab" + }, + "settings": { + "accountTitle": "Account", + "accountSubtitle": "Profile and security settings for your login.", + "accountPlaceholder": "Password change and profile editing will be wired here next (e.g. invite flow, reset password).", + "subscriptionsTitle": "Subscriptions", + "subscriptionsSubtitle": "Your DyoLink workspace plan and seats for {orgName}. Clinic and lab income tracking stays under the sidebar Billing tab.", + "noSubscriptionNotice": "This organization has no active subscription. Select a plan below to start the purchase process.", + "currentPlan": "Current plan", + "planPrice": "Plan price", + "seatsUsed": "Seats used", + "seatsRemaining": "Seats remaining", + "daysRemaining": "Days remaining", + "unlimited": "Unlimited", + "unlimitedSeats": "Unlimited seats", + "seatsCount": "{n} seats", + "pricePerMonth": "${price} / month", + "noActiveSubscription": "No active subscription for this organization.", + "trialEnded": "Trial period has ended. Choose a plan when checkout is available.", + "trialEndsIn": "Trial ends in {days} day(s).", + "seatsLow": "Seat usage is high for this organization.", + "choosePlanIntro": "Choose a plan to continue. Purchase integration is not active yet, so this currently prepares the selection step only.", + "planSolo": "Solo", + "planSmall": "Small", + "planMedium": "Medium", + "planLarge": "Large", + "planEnterprise": "Enterprise", + "startPurchase": "Start purchase process", + "purchaseNotice": "Purchase flow will be enabled soon. {plan} is selected and ready for checkout setup." + }, + "schedule": { + "defaultLabel": "Schedule date", + "previousDay": "Previous day", + "nextDay": "Next day", + "chooseDate": "Choose schedule date", + "year": "Year", + "month": "Month", + "day": "Day", + "monthJanuary": "January", + "monthFebruary": "February", + "monthMarch": "March", + "monthApril": "April", + "monthMay": "May", + "monthJune": "June", + "monthJuly": "July", + "monthAugust": "August", + "monthSeptember": "September", + "monthOctober": "October", + "monthNovember": "November", + "monthDecember": "December" } } diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index e0a269b..e87d189 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -1,110 +1,566 @@ { "common": { "appName": "DyoLink", - "loading": "Loading...", - "loadingApp": "Loading app...", - "loadingWorkspace": "Loading workspace...", - "continue": "Continue", - "back": "Back", - "save": "Save", - "cancel": "Cancel" + "loading": "در حال بارگذاری...", + "loadingApp": "در حال بارگذاری برنامه...", + "loadingWorkspace": "در حال بارگذاری فضای کاری...", + "continue": "ادامه", + "back": "بازگشت", + "save": "ذخیره", + "cancel": "لغو", + "delete": "حذف", + "edit": "ویرایش", + "next": "بعدی", + "dismiss": "رد کردن", + "or": "یا", + "and": "و", + "close": "بستن", + "redirecting": "در حال انتقال...", + "readOnlyAccess": "دسترسی فقط خواندنی برای این سازمان.", + "errorGeneric": "خطایی رخ داده است", + "loadingEllipsis": "در حال بارگذاری...", + "search": "جستجو", + "action": "عملیات", + "status": "وضعیت", + "name": "نام", + "email": "ایمیل", + "date": "تاریخ", + "organization": "سازمان", + "backToApp": "← بازگشت به برنامه", + "copied": "کپی شد", + "copyLink": "کپی لینک", + "none": "هیچکدام", + "preview": "پیشنمایش" }, "language": { - "label": "Language", - "selectLanguage": "Select language", - "en": "English", - "fa": "Persian", - "nl": "Dutch" + "label": "زبان", + "selectLanguage": "انتخاب زبان", + "en": "انگلیسی", + "fa": "فارسی", + "nl": "هلندی" }, "theme": { - "switchToLight": "Switch to light mode", - "switchToDark": "Switch to dark mode", - "lightMode": "Light mode", - "darkMode": "Dark mode" + "switchToLight": "تغییر به حالت روشن", + "switchToDark": "تغییر به حالت تیره", + "lightMode": "حالت روشن", + "darkMode": "حالت تیره" }, "nav": { - "dashboard": "Dashboard", - "staff": "Staff", - "patients": "Patients", - "appointment": "Appointment", - "treatment": "Treatment", - "billing": "Billing", - "reports": "Reports", - "clinics": "Clinics", - "labs": "Labs" + "dashboard": "داشبورد", + "staff": "کارکنان", + "patients": "بیماران", + "appointment": "نوبتها", + "treatment": "درمان", + "billing": "صورتحساب", + "reports": "گزارشها", + "clinics": "کلینیکها", + "labs": "لابراتوارها" }, "auth": { - "login": "Login", - "signIn": "Sign in", - "signOut": "Log out", - "register": "Register", - "startTrial": "Start Trial", - "startFreeTrial": "Start Free Trial", - "dashboard": "Dashboard", - "signInTitle": "Sign in to your account", - "signInPrompt": "Or {link}", - "startTrialLink": "start your free trial", - "registerTitle": "Start your 30-day free trial", - "registerPrompt": "Already have an account?", - "signInLink": "Sign in", - "email": "Email address", - "password": "Password", - "confirmPassword": "Confirm password", - "fullName": "Full name", - "rememberMe": "Remember me", - "forgotPassword": "Forgot your password?", - "invalidCredentials": "Invalid email or password", - "loginFailed": "Login failed", - "registrationFailed": "Registration failed. Please try again.", - "startMyFreeTrial": "Start my free trial", - "trialIncludes": "Your trial includes:", - "trialTeamMembers": "Up to 5 team members", - "trialFullAccess": "Full access to all features", - "trialNoCard": "30 days free, no credit card required", - "termsAgreement": "By signing up, you agree to our {terms} and {privacy}", - "termsOfService": "Terms of Service", - "privacyPolicy": "Privacy Policy", - "signedIn": "Signed in", - "switchOrganization": "Switch organization", - "subscriptions": "Subscriptions", - "account": "Account" + "login": "ورود", + "signIn": "ورود به حساب", + "signOut": "خروج", + "register": "ثبتنام", + "startTrial": "شروع دوره آزمایشی", + "startFreeTrial": "شروع دوره آزمایشی رایگان", + "dashboard": "داشبورد", + "signInTitle": "به حساب کاربری خود وارد شوید", + "signInPrompt": "یا {link}", + "startTrialLink": "دوره آزمایشی رایگان خود را شروع کنید", + "registerTitle": "دوره آزمایشی رایگان ۳۰ روزه خود را آغاز کنید", + "registerPrompt": "از قبل حساب کاربری دارید؟", + "signInLink": "وارد شوید", + "email": "آدرس ایمیل", + "password": "رمز عبور", + "confirmPassword": "تأیید رمز عبور", + "fullName": "نام و نام خانوادگی", + "rememberMe": "مرا به خاطر بسپار", + "forgotPassword": "رمز عبور خود را فراموش کردهاید؟", + "invalidCredentials": "ایمیل یا رمز عبور نامعتبر است", + "loginFailed": "ورود ناموفق بود", + "registrationFailed": "ثبتنام ناموفق بود. لطفاً دوباره تلاش کنید.", + "startMyFreeTrial": "دوره آزمایشی رایگان من را شروع کن", + "trialIncludes": "دوره آزمایشی شما شامل موارد زیر است:", + "trialTeamMembers": "تا ۵ عضو تیم", + "trialFullAccess": "دسترسی کامل به تمام امکانات", + "trialNoCard": "۳۰ روز رایگان، بدون نیاز به کارت اعتباری", + "termsAgreement": "با ثبتنام، شما با {terms} و {privacy} ما موافقت میکنید", + "termsOfService": "شرایط استفاده از خدمات", + "privacyPolicy": "سیاست حفظ حریم خصوصی", + "signedIn": "وارد شدهاید", + "switchOrganization": "تغییر سازمان", + "subscriptions": "اشتراکها", + "account": "حساب کاربری", + "emailPlaceholder": "you@example.com", + "passwordPlaceholder": "••••••••", + "namePlaceholder": "John Doe", + "termsIntro": "با ثبتنام، شما با", + "errorRegistrationFailed": "ثبتنام ناموفق بود", + "errorLoginFailed": "ورود ناموفق بود", + "errorCreateOrganization": "ایجاد سازمان ناموفق بود", + "acceptInviteTitle": "پذیرش دعوتنامه", + "loadingInvitation": "در حال بارگذاری دعوتنامه...", + "invalidInvitationLink": "لینک دعوتنامه نامعتبر است", + "invitationAlreadyAccepted": "این دعوتنامه قبلاً پذیرفته شده است. اکنون میتوانید وارد شوید.", + "errorLoadInvitation": "بارگذاری دعوتنامه امکانپذیر نبود", + "organizationLabel": "سازمان:", + "emailLabel": "ایمیل:", + "nameRequired": "نام الزامی است", + "passwordMinLength8": "رمز عبور باید حداقل ۸ کاراکتر باشد", + "passwordsDoNotMatch": "رمزهای عبور مطابقت ندارند", + "labelName": "نام", + "labelCreatePassword": "ایجاد رمز عبور", + "labelConfirmPassword": "تأیید رمز عبور", + "activateAccount": "فعالسازی حساب", + "invitationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال انتقال به صفحه ورود...", + "errorAcceptInvitation": "پذیرش دعوتنامه امکانپذیر نبود", + "alreadyHaveAccess": "از قبل دسترسی دارید؟", + "goToLogin": "رفتن به ورود", + "acceptOrganizationTitle": "پذیرش دعوتنامه سازمان", + "alreadyHaveAccount": "از قبل حساب کاربری دارید؟", + "invitedBy": "دعوتکننده:", + "ownerEmail": "ایمیل مالک", + "activateOrganization": "فعالسازی سازمان", + "organizationAcceptedRedirect": "دعوتنامه پذیرفته شد. در حال انتقال به صفحه ورود...", + "stepAccount": "حساب", + "stepOrganization": "سازمان", + "organizationName": "نام سازمان", + "organizationNamePlaceholder": "کلینیک دندانپزشکی خورشید", + "organizationEmail": "ایمیل سازمان", + "organizationEmailPlaceholder": "contact@sunshineclinic.com", + "organizationType": "نوع سازمان", + "dentalClinic": "کلینیک دندانپزشکی", + "dentalLab": "لابراتوار دندانپزشکی" }, "landing": { - "heroTitle": "Connect Dental Clinics & Labs", - "heroHighlight": "Seamlessly", - "heroSubtitle": "Streamline communication between dental professionals. Start with a 30-day free trial, no credit card required.", - "featureClinicsTitle": "For Clinics", - "featureClinicsDescription": "Manage patients, appointments, and send cases to labs instantly.", - "featureLabsTitle": "For Labs", - "featureLabsDescription": "Receive cases, track progress, and communicate with clinics.", - "featureTeamTitle": "Team Management", - "featureTeamDescription": "Add up to 5 team members during trial. Scale as you grow.", - "featureTrialTitle": "30-Day Trial", - "featureTrialDescription": "Full access to all features. No credit card required.", - "featureRealtimeTitle": "Real-time Updates", - "featureRealtimeDescription": "Get instant notifications on case status changes.", - "featureSecurityTitle": "Secure & Compliant", - "featureSecurityDescription": "HIPAA-compliant with enterprise-grade security.", - "footerCopyright": "© 2026 DyoLink. All rights reserved.", - "termsAndConditions": "Terms & Conditions" + "heroTitle": "اتصال کلینیکها و لابراتوارهای دندانپزشکی", + "heroHighlight": "بهصورت یکپارچه", + "heroSubtitle": "ارتباط بین متخصصان دندانپزشکی را ساده و سریع کنید. با یک دوره آزمایشی رایگان ۳۰ روزه، بدون نیاز به کارت اعتباری، شروع کنید.", + "featureClinicsTitle": "برای کلینیکها", + "featureClinicsDescription": "بیماران و نوبتها را مدیریت کنید و پروندهها را فوراً به لابراتوارها ارسال کنید.", + "featureLabsTitle": "برای لابراتوارها", + "featureLabsDescription": "پروندهها را دریافت کنید، روند پیشرفت را پیگیری کنید و با کلینیکها در ارتباط باشید.", + "featureTeamTitle": "مدیریت تیم", + "featureTeamDescription": "در طول دوره آزمایشی تا ۵ عضو تیم اضافه کنید. همگام با رشد خود مقیاسپذیر باشید.", + "featureTrialTitle": "دوره آزمایشی ۳۰ روزه", + "featureTrialDescription": "دسترسی کامل به تمامی امکانات. بدون نیاز به کارت اعتباری.", + "featureRealtimeTitle": "بهروزرسانیهای لحظهای", + "featureRealtimeDescription": "اعلانهای فوری درباره تغییر وضعیت پروندهها دریافت کنید.", + "featureSecurityTitle": "امن و منطبق با استانداردها", + "featureSecurityDescription": "منطبق با HIPAA و دارای امنیت در سطح سازمانی.", + "footerCopyright": "© ۲۰۲۶ DyoLink. تمامی حقوق محفوظ است.", + "termsAndConditions": "شرایط و ضوابط" }, "accountMenu": { - "noActiveSubscription": "No active subscription — review Subscriptions", - "trialEnded": "Trial ended — review Subscriptions", - "trialEndingSoon": "Trial ending soon — review Subscriptions", - "seatsLow": "Seats running low — review Subscriptions", - "reviewSubscriptions": "Review Subscriptions" + "noActiveSubscription": "اشتراک فعالی وجود ندارد — اشتراکها را بررسی کنید", + "trialEnded": "دوره آزمایشی به پایان رسیده است — اشتراکها را بررسی کنید", + "trialEndingSoon": "دوره آزمایشی بهزودی به پایان میرسد — اشتراکها را بررسی کنید", + "seatsLow": "ظرفیت کاربران رو به اتمام است — اشتراکها را بررسی کنید", + "reviewSubscriptions": "بررسی اشتراکها" }, "validation": { - "emailInvalid": "Please enter a valid email address", - "passwordRequired": "Password is required", - "nameMinLength": "Name must be at least 2 characters", - "passwordMinLength": "Password must be at least 8 characters", - "passwordUppercase": "Password must contain at least one uppercase letter", - "passwordNumber": "Password must contain at least one number", - "organizationNameMinLength": "Organization name must be at least 2 characters", - "organizationEmailInvalid": "Please enter a valid organization email", - "organizationTypeRequired": "Please select organization type", - "passwordsDoNotMatch": "Passwords don't match" + "emailInvalid": "لطفاً یک آدرس ایمیل معتبر وارد کنید", + "passwordRequired": "رمز عبور الزامی است", + "nameMinLength": "نام باید حداقل ۲ کاراکتر باشد", + "passwordMinLength": "رمز عبور باید حداقل ۸ کاراکتر باشد", + "passwordUppercase": "رمز عبور باید حداقل شامل یک حرف بزرگ باشد", + "passwordNumber": "رمز عبور باید حداقل شامل یک عدد باشد", + "organizationNameMinLength": "نام سازمان باید حداقل ۲ کاراکتر باشد", + "organizationEmailInvalid": "لطفاً یک ایمیل سازمانی معتبر وارد کنید", + "organizationTypeRequired": "لطفاً نوع سازمان را انتخاب کنید", + "passwordsDoNotMatch": "رمزهای عبور مطابقت ندارند" + }, + "today": { + "welcomeBack": "خوش آمدید!!", + "noSubscriptionNotice": "این سازمان هنوز اشتراک فعالی ندارد.", + "choosePlanLink": "انتخاب طرح", + "noSubscriptionCta": "برای شروع فرآیند خرید.", + "cardTodaysAppointments": "نوبتهای امروز", + "cardActivePatients": "بیماران فعال", + "cardNewLabCase": "پرونده جدید لابراتوار", + "cardTodayInvoices": "صورتحسابهای امروز" + }, + "staff": { + "redirecting": "در حال انتقال...", + "title": "مدیریت کارکنان", + "subtitle": "از همکاران خود دعوت کنید، دسترسی به برگهها را تنظیم کنید و در محدوده طرح خود باقی بمانید.", + "inviteMember": "دعوت از عضو", + "seatsLabel": "مجوزها:", + "unlimitedPlan": "(طرح نامحدود)", + "seatLimitReached": "ظرفیت مجوزهای طرح برای این سازمان تکمیل شده است.", + "noActivePlan": "هیچ طرح فعالی برای این سازمان انتخاب نشده است. برای دعوت از اعضا، یک طرح اشتراک انتخاب کنید.", + "invitedPending": "دعوتنامه تا زمانی که لینک را باز کنند، رمز عبور تنظیم کنند و وارد شوند، در حالت انتظار است.", + "invitedAccepted": "دعوتنامه بلافاصله پذیرفته شد.", + "inviteLinkHeading": "لینک دعوت", + "shareLinkHint": "این لینک را به صورت دستی از طریق پیامک یا ایمیل به اشتراک بگذارید. در صورت منقضی شدن یا گم شدن لینک قبلی، لینک جدیدی تولید میشود.", + "loadingTeam": "در حال بارگذاری تیم...", + "tableName": "نام", + "tableEmail": "ایمیل", + "tableRole": "نقش", + "tableStatus": "وضعیت", + "tableAccess": "دسترسی", + "tableAction": "عملیات", + "roleOwner": "مالک", + "roleStaff": "کارمند", + "statusActive": "فعال", + "statusPending": "در انتظار", + "statusDisabled": "غیرفعال", + "statusExpired": "منقضی شده", + "allFeatures": "همه امکانات", + "inviteModalTitle": "دعوت از عضو تیم", + "stepOf": "مرحله {step} از ۲", + "permissionView": "نمایش", + "permissionEdit": "ویرایش", + "labelEmail": "ایمیل", + "labelDisplayName": "نام نمایشی", + "tabAccess": "دسترسی به برگهها", + "sendInvite": "ارسال دعوتنامه", + "skipForNow": "فعلاً رد کردن", + "enableModalTitle": "فعالسازی عضو تیم", + "enableConfirm": "{name} ({email}) فعال شود؟", + "enableBullet1": "آنها میتوانند دوباره با حساب موجود خود وارد این سازمان شوند.", + "enableBullet2": "هیچ دعوتنامه جدیدی ارسال نمیشود و در زمان غیرفعال بودن هیچ دادهای حذف نشده است.", + "enableBullet3": "فعالسازی از یک مجوز در طرح شما استفاده میکند.", + "noSeatsAvailable": "هیچ مجوزی در دسترس نیست. قبل از فعالسازی این شخص، عضو دیگری را غیرفعال کنید یا طرح خود را ارتقا دهید.", + "enableMemberButton": "فعالسازی عضو", + "disableModalTitle": "غیرفعالسازی عضو تیم", + "disableConfirm": "{name} ({email}) غیرفعال شود؟", + "disableBullet1": "آنها نمیتوانند وارد این سازمان شوند.", + "disableBullet2": "هیچ دادهای حذف نمیشود.", + "disableBullet3": "غیرفعالسازی یک مجوز در طرح شما را آزاد میکند تا بتوانید شخص دیگری را دعوت کنید.", + "disableMemberButton": "غیرفعالسازی عضو", + "editModalTitle": "ویرایش عضو", + "loadingWorkingHours": "در حال بارگذاری ساعات کاری...", + "errorLoadStaff": "بارگذاری کارکنان ناموفق بود.", + "errorCopyInvite": "کپی لینک دعوتنامه امکانپذیر نبود.", + "errorSendInvite": "ارسال دعوتنامه ناموفق بود.", + "errorLoadWorkingHours": "بارگذاری ساعات کاری ناموفق بود.", + "successMemberUpdated": "عضو بهروزرسانی شد.", + "errorUpdateMember": "بهروزرسانی عضو ناموفق بود.", + "errorDeleteNotImplemented": "حذف هنوز پیادهسازی نشده است.", + "successMemberDisabled": "{name} غیرفعال شد. یک مجوز در دسترس قرار گرفت.", + "errorDisableMember": "غیرفعالسازی عضو ناموفق بود.", + "successMemberEnabled": "{name} فعال شد و میتواند دوباره وارد شود.", + "errorEnableMember": "فعالسازی عضو ناموفق بود.", + "successInvited": "{name} ({email}) دعوت شد.", + "copyInviteLink": "کپی لینک دعوتنامه", + "copyInviteLinkTitle": "کپی لینک دعوتنامه (در صورت نیاز لینک جدیدی تولید میکند)", + "enableMemberAria": "فعالسازی عضو", + "enableMemberTitle": "فعالسازی عضو (از یک مجوز استفاده میکند)", + "disableMemberAria": "غیرفعالسازی عضو", + "disableMemberTitle": "غیرفعالسازی عضو (یک مجوز را آزاد میکند)", + "editMemberAria": "ویرایش عضو", + "deleteMemberAria": "حذف عضو", + "deleteMemberTitle": "حذف عضو (پیادهسازی نشده)", + "features": { + "featureToday": "امروز", + "featureStaff": "کارکنان", + "featureOrganizations": "سازمانها", + "featureClinics": "کلینیکها", + "featureLabs": "لابراتوارها", + "featurePatients": "بیماران", + "featureAppointment": "نوبتها", + "featureTreatment": "درمان", + "featureBilling": "صورتحساب", + "featureReports": "گزارشها", + "noTabAccess": "دسترسی به برگهها وجود ندارد", + "readOnlySuffix": "(فقط خواندنی)" + }, + "workingHours": { + "recommendedTitle": "ساعات کاری توصیه میشود", + "recommendedBody": "کارکنانی که دسترسی ویرایش درمان دارند، به عنوان ستونهای ارائهدهنده در نوبتها ظاهر میشوند. ساعات هفتگی آنها را تنظیم کنید تا جدول زمانبندی زمانهای قابل رزرو صحیح را نشان دهد.", + "intro": "ساعات کاری هفتگی این ارائهدهنده را تنظیم کنید. جدول نوبتها از این ساعات برای نمایش زمانهای قابل رزرو استفاده میکند.", + "workingDay": "روز کاری", + "start": "شروع", + "end": "پایان", + "removeShift": "حذف شیفت", + "addShift": "افزودن شیفت", + "autoRepeatWeekly": "این ساعات را در ابتدای هر هفته تکرار کنید (کپی به جلو در روز دوشنبه)", + "weekdayMon": "دوشنبه", + "weekdayTue": "سهشنبه", + "weekdayWed": "چهارشنبه", + "weekdayThu": "پنجشنبه", + "weekdayFri": "جمعه", + "weekdaySat": "شنبه", + "weekdaySun": "یکشنبه", + "validationNeedsShift": "{day} حداقل به یک شیفت نیاز دارد یا باید به عنوان تعطیل علامتگذاری شود.", + "validationEndAfterStart": "زمان پایان شیفت {day} باید بعد از زمان شروع باشد.", + "validationOverlap": "شیفتهای {day} نمیتوانند همپوشانی داشته باشند." + } + }, + "patients": { + "title": "بیماران", + "newPatient": "بیمار جدید", + "errorLoadPatients": "بارگذاری بیماران ناموفق بود.", + "successPatientSaved": "بیمار {firstName} {lastName} با موفقیت ذخیره شد.", + "errorSavePatient": "ذخیره بیمار ناموفق بود.", + "firstName": "نام", + "lastName": "نام خانوادگی", + "phone": "تلفن", + "savePatient": "ذخیره بیمار", + "dialogTitle": "بیمار جدید", + "searchPlaceholder": "جستجوی بیماران بر اساس نام، تلفن، ایمیل", + "loadingPatients": "در حال بارگذاری بیماران...", + "noResults": "هیچ بیماری برای این جستجو یافت نشد.", + "noContact": "بدون اطلاعات تماس", + "selectPatient": "برای مشاهده جزئیات، یک بیمار را انتخاب کنید.", + "phoneLabel": "تلفن:", + "emailLabel": "ایمیل:", + "statusLabel": "وضعیت:", + "statusActive": "فعال", + "statusInactive": "غیرفعال", + "emptyValue": "-" + }, + "appointments": { + "title": "نوبتها", + "subtitle": "یک بیمار را جستجو کنید، تاریخ را انتخاب کنید، سپس روی یک زمان در زیر ارائهدهنده کلیک کنید تا رزرو کنید.", + "loadingSchedule": "در حال بارگذاری برنامه...", + "infoPastViewOnly": "نوبتهای گذشته فقط قابل مشاهده هستند.", + "infoSelectPatient": "قبل از رزرو، یک بیمار را انتخاب کنید.", + "errorOutsideHours": "این نوبت خارج از ساعات کاری فعلی ارائهدهنده است و قابل ویرایش نیست.", + "successUpdated": "نوبت بهروزرسانی شد.", + "successSaved": "نوبت ذخیره شد.", + "errorUpdate": "بهروزرسانی نوبت امکانپذیر نبود.", + "errorSave": "ذخیره نوبت امکانپذیر نبود.", + "confirmRemove": "این نوبت حذف شود؟", + "successRemoved": "نوبت حذف شد.", + "errorDelete": "حذف نوبت امکانپذیر نبود.", + "errorLoadSchedule": "بارگذاری برنامه ناموفق بود.", + "successPatientSaved": "بیمار {firstName} {lastName} ذخیره شد.", + "searchPlaceholder": "جستجوی بیماران موجود", + "searching": "در حال جستجو...", + "searchHint": "برای جستجوی بیماران بر اساس نام، تلفن یا ایمیل، تایپ کنید.", + "noPermissionAdd": "شما مجوز اضافه کردن بیمار را ندارید.", + "editTitle": "ویرایش نوبت", + "newTitle": "نوبت جدید", + "providerLabel": "ارائهدهنده:", + "patientLabel": "بیمار", + "startLabel": "شروع", + "endLabel": "پایان", + "purposeLabel": "هدف", + "errorSelectPatient": "ابتدا یک بیمار را انتخاب کنید.", + "errorEndAfterStart": "زمان پایان باید بعد از زمان شروع باشد.", + "errorPastSchedule": "نمیتوان در گذشته زمانبندی کرد.", + "errorPastViewOnly": "نوبتهای گذشته فقط قابل مشاهده هستند.", + "errorMissingDetails": "جزئیات نوبت وجود ندارد.", + "noProviders": "هیچ ارائهدهندهای در دسترس نیست. برای مشاهده ستونها در اینجا، کارکنانی با دسترسی ویرایش درمان اضافه کنید.", + "noWorkingHours": "ساعات کاری برای این روز پیکربندی نشده است. ساعات کاری ارائهدهنده را در مدیریت کارکنان تنظیم کنید.", + "noHoursSet": "ساعتی تنظیم نشده", + "offToday": "امروز تعطیل است", + "slotOffToday": "ارائهدهنده امروز تعطیل است", + "slotHoursNotConfigured": "ساعات کاری پیکربندی نشده است", + "slotOutsideHours": "خارج از ساعات کاری", + "slotCannotCreate": "شما نمیتوانید نوبت ایجاد کنید", + "slotBookAt": "رزرو {time}", + "outsideHoursBlocked": "خارج از ساعات کاری — ویرایش مسدود شده است", + "overlappingChoose": "{count} همپوشانی — برای انتخاب کلیک کنید", + "overlapping": "{count} همپوشانی", + "legend": "راهنما", + "overlappingTitle": "نوبتهای همپوشانی ({count})", + "purposeConsultation": "مشاوره", + "purposeFilling": "پر کردن", + "purposeEndo": "درمان ریشه", + "purposeVisit": "ویزیت", + "purposeHygiene": "بهداشت" + }, + "treatment": { + "loading": "در حال بارگذاری...", + "noPermissionTitle": "فضای کاری درمان", + "noPermissionBody": "شما مجوز مشاهده برگه درمان برای این سازمان را ندارید.", + "title": "درمان", + "subtitleEdit": "پروندههای نوبتهای خود را مستند کنید، پیشنویسها را ذخیره کنید و کار را به سازمانهای مرتبط ارسال کنید.", + "subtitleReadOnly": "دسترسی فقط خواندنی — میتوانید نوبتها و تاریخچه درمان را بررسی کنید اما نمیتوانید ویرایش کنید.", + "pastDayNotice": "روزهای گذشته فقط قابل مشاهده هستند. میتوانید نوبتها و تاریخچه را بررسی کنید، اما پروندههای درمانی قابل اضافه یا تغییر نیستند.", + "selectedPatient": "بیمار انتخاب شده", + "purposeLabel": "هدف:", + "loadingAppointments": "در حال بارگذاری نوبتها...", + "selectDayWithAppointment": "روزی را انتخاب کنید که حداقل یک نوبت داشته باشد.", + "confirmDiscard": "تغییرات ذخیرهنشده دارید. آنها را کنار بگذارید و ادامه دهید؟", + "successDraftSaved": "پیشنویس درمان ذخیره شد.", + "errorChooseOrg": "حداقل یک سازمان فعال را برای ارسال این پرونده انتخاب کنید.", + "successCaseSent": "پرونده به سازمانهای انتخاب شده ارسال شد.", + "successFilesUploaded": "{count} فایل با موفقیت بارگذاری شد.", + "errorLoadAppointments": "بارگذاری نوبتها ناموفق بود.", + "errorLoadOrgs": "بارگذاری سازمانهای مرتبط ناموفق بود.", + "errorLoadHistory": "بارگذاری تاریخچه درمان ناموفق بود.", + "errorLoadDraft": "بارگذاری پیشنویس درمان ناموفق بود.", + "errorUpload": "بارگذاری پیوستها ناموفق بود.", + "errorSaveDraft": "ذخیره پیشنویس درمان ناموفق بود.", + "errorSendCase": "ارسال پرونده ناموفق بود.", + "errorCaseMustSave": "پرونده باید قبل از ارسال ذخیره شود.", + "draftTitle": "پیشنویس · {patientName}", + "hiddenMessage": "نوبتها پنهان هستند.", + "showAppointments": "نمایش نوبتها", + "appointmentsTitle": "نوبتهای من", + "hideAppointments": "پنهان کردن نوبتها", + "emptyDay": "هیچ نوبتی به شما در این روز اختصاص داده نشده است.", + "casesTitle": "پروندههای درمانی", + "casesSubtitle": "هر پرونده دارای دندانها، یادداشتها، پیوستها و مقصدهای ارسال خود است.", + "addCase": "افزودن پرونده", + "caseLabel": "پرونده {n}", + "comments": "نظرات", + "commentsPlaceholder": "یادداشتهای بالینی این پرونده را بنویسید...", + "treatmentType": "نوع درمان", + "typeConsultation": "مشاوره", + "typeFilling": "پر کردن", + "typeEndo": "درمان ریشه", + "typeVisit": "ویزیت", + "typeHygiene": "بهداشت", + "attachments": "پیوستها", + "attachFiles": "فایلها را برای این پرونده درمانی پیوست کنید", + "chooseFiles": "انتخاب فایلها", + "sendToOrgs": "ارسال این پرونده به سازمانهای مرتبط", + "searchOrgsPlaceholder": "جستجوی سازمانهای فعال...", + "recent": "اخیر:", + "noOrgMatch": "هیچ سازمان فعالی با جستجوی شما مطابقت ندارد.", + "sendThisCase": "ارسال این پرونده", + "saveDraft": "ذخیره پیشنویس درمان", + "unsavedChanges": "تغییرات ذخیرهنشده", + "draftSaved": "پیشنویس ذخیره شد", + "sendSavesFirst": "ارسال برای هر پرونده به صورت جداگانه است و ابتدا به طور خودکار ذخیره میکند.", + "historyTitle": "درمانهای قبلی", + "historySubtitle": "درمانهای تکمیل شده برای این بیمار. هر پرونده به طور جداگانه فهرست شده است.", + "loadingHistory": "در حال بارگذاری تاریخچه...", + "historyEmpty": "هیچ درمان قبلی برای این بیمار وجود ندارد.", + "statusLabel": "وضعیت:", + "historyCaseLabel": "پرونده {n} · {type}", + "teethLabel": "دندانها:", + "teethNone": "هیچکدام انتخاب نشده", + "reviewDetails": "بررسی جزئیات", + "previewTitle": "پیشنمایش درمان", + "previewDraft": "پیشنمایش پیشنویس فعلی", + "selectAppointment": "یک نوبت را برای پیشنمایش پیشنویس آن انتخاب کنید.", + "caseCount": "{n} پرونده", + "attachmentCount": "{n} پیوست", + "caseSummary": "پرونده {n}: {type}", + "teethPrefix": "· دندانها", + "moreCases": "+ {n} پرونده دیگر", + "previewDialogTitle": "پیشنمایش درمان", + "previewDialogSubtitle": "بررسی پروندهها، پیوستها و مقصدهای ارسال.", + "noCases": "هیچ پروندهای در این درمان وجود ندارد.", + "typeLabel": "نوع:", + "commentsLabel": "نظرات:", + "commentsEmpty": "نظرات: —", + "attachFilesShort": "پیوست فایلها", + "sendCase": "ارسال این پرونده", + "sendToLinkedOrgs": "ارسال به سازمانهای مرتبط", + "noActiveOrgs": "هیچ سازمان مرتبط فعالی وجود ندارد.", + "confirmSend": "تأیید ارسال", + "toothChartTitle": "نمودار دندانها FDI", + "toothChartHint": "برای انتخاب چندگانه روی دندانها ضربه بزنید. برای پرونده فعال اعمال میشود.", + "selectedLabel": "انتخاب شده:", + "selectedEmpty": "—", + "upperArch": "قوس بالا", + "lowerArch": "قوس پایین", + "toothAria": "دندان FDI {fdi}", + "toothSelectedSuffix": "، انتخاب شده", + "sentToAt": "ارسال به {orgName} در {datetime}", + "fallbackOrgName": "سازمان" + }, + "organizations": { + "loadingOrganization": "در حال بارگذاری سازمان...", + "subtitle": "سازمانها را جستجو کنید، درخواست اتصال به حسابهای موجود ارسال کنید، یا اگر هنوز در DyoLink نیستند، لینک دعوت ارسال کنید.", + "invitationHistory": "تاریخچه دعوتنامهها", + "searchPlaceholder": "جستجوی {counterpart} بر اساس نام، ایمیل یا تلفن...", + "backToList": "بازگشت به لیست", + "tableOrganization": "سازمان", + "tableOwnerEmail": "ایمیل مالک", + "tableDate": "تاریخ", + "tableStatus": "وضعیت", + "tableAction": "عملیات", + "emptyConnections": "هنوز هیچ اتصالی وجود ندارد. برای ارسال درخواست اتصال یا لینک دعوت، جستجو کنید.", + "statusInvitationPending": "دعوتنامه در انتظار", + "statusConnectionPending": "درخواست اتصال در انتظار", + "statusConnected": "متصل", + "statusDeclined": "درخواست اتصال رد شد", + "statusFound": "یافت شد", + "statusToday": "امروز", + "acceptRequest": "پذیرش درخواست اتصال", + "declineRequest": "رد درخواست اتصال", + "removeConnection": "قطع اتصال", + "sendRequest": "ارسال درخواست اتصال", + "noDirectoryResults": "هیچ سازمانی در جستجوی دایرکتوری یافت نشد.", + "hideInvitationFields": "پنهان کردن فیلدهای دعوت", + "sendInvitationLink": "ارسال لینک دعوت", + "counterpartNameLabel": "نام {counterpart}", + "ownerEmailLabel": "ایمیل مالک", + "sendInvitation": "ارسال دعوتنامه", + "successConnectionSent": "درخواست اتصال {counterpart} ارسال شد.", + "successInviteCreated": "لینک دعوت برای {email} ایجاد شد", + "successLinkCopied": "لینک دعوت در کلیپبورد کپی شد.", + "successAccepted": "درخواست اتصال پذیرفته شد.", + "successDeclined": "درخواست اتصال رد شد.", + "successRemoved": "اتصال قطع شد.", + "historyTitle": "تاریخچه دعوتنامهها", + "loadingHistory": "در حال بارگذاری تاریخچه دعوتنامهها...", + "historyEmpty": "هنوز هیچ دعوتنامهای وجود ندارد.", + "tableInvitationLink": "لینک دعوت", + "statusPending": "دعوتنامه در انتظار", + "statusAccepted": "دعوتنامه پذیرفته شد", + "statusRejected": "دعوتنامه رد شد", + "statusExpired": "دعوتنامه منقضی شد", + "copyInvitationLink": "کپی لینک دعوتنامه", + "copyInvitationLinkTitle": "کپی لینک دعوتنامه (در صورت نیاز لینک جدیدی تولید میکند)", + "selectorTitle": "سازمانها", + "selectorSubtitleWithCreate": "یک سازمان را برای ادامه انتخاب کنید، یا یک سازمان جدید ایجاد کنید.", + "selectorSubtitleSelectOnly": "یک سازمان را برای ادامه انتخاب کنید.", + "createOrganization": "ایجاد سازمان", + "createAndContinue": "ایجاد و ادامه", + "emptyCanCreate": "هیچ سازمانی یافت نشد. اولین سازمان خود را ایجاد کنید تا ادامه دهید.", + "emptyAskOwner": "هیچ سازمانی یافت نشد. از مالک سازمان بخواهید تا شما را دعوت کند.", + "continueArrow": "ادامه →", + "planLabel": "طرح: {name} • {maxUsers} کاربر", + "counterpartClinic": "کلینیک", + "counterpartLab": "لابراتوار" + }, + "settings": { + "accountTitle": "حساب کاربری", + "accountSubtitle": "تنظیمات پروفایل و امنیت برای ورود شما.", + "accountPlaceholder": "تغییر رمز عبور و ویرایش پروفایل در مرحله بعدی در اینجا قرار میگیرند (مثلاً فرآیند دعوت، بازنشانی رمز عبور).", + "subscriptionsTitle": "اشتراکها", + "subscriptionsSubtitle": "طرح و مجوزهای فضای کاری DyoLink شما برای {orgName}. پیگیری درآمد کلینیک و لابراتوار در برگه صورتحساب در نوار کناری قرار دارد.", + "noSubscriptionNotice": "این سازمان اشتراک فعالی ندارد. برای شروع فرآیند خرید، یک طرح زیر را انتخاب کنید.", + "currentPlan": "طرح فعلی", + "planPrice": "قیمت طرح", + "seatsUsed": "مجوزهای استفاده شده", + "seatsRemaining": "مجوزهای باقیمانده", + "daysRemaining": "روزهای باقیمانده", + "unlimited": "نامحدود", + "unlimitedSeats": "مجوزهای نامحدود", + "seatsCount": "{n} مجوز", + "pricePerMonth": "${price} / ماه", + "noActiveSubscription": "هیچ اشتراک فعالی برای این سازمان وجود ندارد.", + "trialEnded": "دوره آزمایشی به پایان رسیده است. زمانی که تسویه حساب در دسترس قرار گرفت، یک طرح انتخاب کنید.", + "trialEndsIn": "دوره آزمایشی در {days} روز به پایان میرسد.", + "seatsLow": "استفاده از مجوزها برای این سازمان بالا است.", + "choosePlanIntro": "یک طرح را برای ادامه انتخاب کنید. یکپارچهسازی خرید هنوز فعال نیست، بنابراین این مرحله فقط مرحله انتخاب را آماده میکند.", + "planSolo": "انفرادی", + "planSmall": "کوچک", + "planMedium": "متوسط", + "planLarge": "بزرگ", + "planEnterprise": "سازمانی", + "startPurchase": "شروع فرآیند خرید", + "purchaseNotice": "جریان خرید به زودی فعال میشود. {plan} انتخاب شده و برای راهاندازی تسویه حساب آماده است." + }, + "schedule": { + "defaultLabel": "تاریخ برنامه", + "previousDay": "روز قبل", + "nextDay": "روز بعد", + "chooseDate": "انتخاب تاریخ برنامه", + "year": "سال", + "month": "ماه", + "day": "روز", + "monthJanuary": "ژانویه", + "monthFebruary": "فوریه", + "monthMarch": "مارس", + "monthApril": "آوریل", + "monthMay": "مه", + "monthJune": "ژوئن", + "monthJuly": "ژوئیه", + "monthAugust": "اوت", + "monthSeptember": "سپتامبر", + "monthOctober": "اکتبر", + "monthNovember": "نوامبر", + "monthDecember": "دسامبر" } -} +} \ No newline at end of file diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index e0a269b..517a742 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -1,110 +1,566 @@ { "common": { "appName": "DyoLink", - "loading": "Loading...", - "loadingApp": "Loading app...", - "loadingWorkspace": "Loading workspace...", - "continue": "Continue", - "back": "Back", - "save": "Save", - "cancel": "Cancel" + "loading": "Laden...", + "loadingApp": "Applicatie laden...", + "loadingWorkspace": "Werkruimte laden...", + "continue": "Doorgaan", + "back": "Terug", + "save": "Opslaan", + "cancel": "Annuleren", + "delete": "Verwijderen", + "edit": "Bewerken", + "next": "Volgende", + "dismiss": "Afwijzen", + "or": "Of", + "and": "en", + "close": "Sluiten", + "redirecting": "Bezig met doorsturen...", + "readOnlyAccess": "Alleen-lezen toegang voor deze organisatie.", + "errorGeneric": "Er is iets misgegaan", + "loadingEllipsis": "Laden...", + "search": "Zoeken", + "action": "Actie", + "status": "Status", + "name": "Naam", + "email": "E-mail", + "date": "Datum", + "organization": "Organisatie", + "backToApp": "← Terug naar app", + "copied": "Gekopieerd", + "copyLink": "Link kopiëren", + "none": "Geen", + "preview": "Voorbeeld" }, "language": { - "label": "Language", - "selectLanguage": "Select language", - "en": "English", - "fa": "Persian", - "nl": "Dutch" + "label": "Taal", + "selectLanguage": "Selecteer taal", + "en": "Engels", + "fa": "Perzisch", + "nl": "Nederlands" }, "theme": { - "switchToLight": "Switch to light mode", - "switchToDark": "Switch to dark mode", - "lightMode": "Light mode", - "darkMode": "Dark mode" + "switchToLight": "Overschakelen naar lichte modus", + "switchToDark": "Overschakelen naar donkere modus", + "lightMode": "Lichte modus", + "darkMode": "Donkere modus" }, "nav": { "dashboard": "Dashboard", - "staff": "Staff", - "patients": "Patients", - "appointment": "Appointment", - "treatment": "Treatment", - "billing": "Billing", - "reports": "Reports", - "clinics": "Clinics", - "labs": "Labs" + "staff": "Medewerkers", + "patients": "Patiënten", + "appointment": "Afspraak", + "treatment": "Behandeling", + "billing": "Facturatie", + "reports": "Rapporten", + "clinics": "Klinieken", + "labs": "Laboratoria" }, "auth": { - "login": "Login", - "signIn": "Sign in", - "signOut": "Log out", - "register": "Register", - "startTrial": "Start Trial", - "startFreeTrial": "Start Free Trial", + "login": "Inloggen", + "signIn": "Aanmelden", + "signOut": "Uitloggen", + "register": "Registreren", + "startTrial": "Proefperiode starten", + "startFreeTrial": "Gratis proefperiode starten", "dashboard": "Dashboard", - "signInTitle": "Sign in to your account", - "signInPrompt": "Or {link}", - "startTrialLink": "start your free trial", - "registerTitle": "Start your 30-day free trial", - "registerPrompt": "Already have an account?", - "signInLink": "Sign in", - "email": "Email address", - "password": "Password", - "confirmPassword": "Confirm password", - "fullName": "Full name", - "rememberMe": "Remember me", - "forgotPassword": "Forgot your password?", - "invalidCredentials": "Invalid email or password", - "loginFailed": "Login failed", - "registrationFailed": "Registration failed. Please try again.", - "startMyFreeTrial": "Start my free trial", - "trialIncludes": "Your trial includes:", - "trialTeamMembers": "Up to 5 team members", - "trialFullAccess": "Full access to all features", - "trialNoCard": "30 days free, no credit card required", - "termsAgreement": "By signing up, you agree to our {terms} and {privacy}", - "termsOfService": "Terms of Service", - "privacyPolicy": "Privacy Policy", - "signedIn": "Signed in", - "switchOrganization": "Switch organization", - "subscriptions": "Subscriptions", - "account": "Account" + "signInTitle": "Meld u aan bij uw account", + "signInPrompt": "Of {link}", + "startTrialLink": "start uw gratis proefperiode", + "registerTitle": "Start uw gratis proefperiode van 30 dagen", + "registerPrompt": "Heeft u al een account?", + "signInLink": "Aanmelden", + "email": "E-mailadres", + "password": "Wachtwoord", + "confirmPassword": "Bevestig wachtwoord", + "fullName": "Volledige naam", + "rememberMe": "Onthoud mij", + "forgotPassword": "Wachtwoord vergeten?", + "invalidCredentials": "Ongeldig e-mailadres of wachtwoord", + "loginFailed": "Inloggen mislukt", + "registrationFailed": "Registratie mislukt. Probeer het opnieuw.", + "startMyFreeTrial": "Start mijn gratis proefperiode", + "trialIncludes": "Uw proefperiode omvat:", + "trialTeamMembers": "Tot 5 teamleden", + "trialFullAccess": "Volledige toegang tot alle functies", + "trialNoCard": "30 dagen gratis, geen creditcard vereist", + "termsAgreement": "Door u aan te melden gaat u akkoord met onze {terms} en {privacy}", + "termsOfService": "Servicevoorwaarden", + "privacyPolicy": "Privacybeleid", + "signedIn": "Aangemeld", + "switchOrganization": "Organisatie wisselen", + "subscriptions": "Abonnementen", + "account": "Account", + "emailPlaceholder": "you@example.com", + "passwordPlaceholder": "••••••••", + "namePlaceholder": "John Doe", + "termsIntro": "Door u te registreren gaat u akkoord met onze", + "errorRegistrationFailed": "Registratie mislukt", + "errorLoginFailed": "Inloggen mislukt", + "errorCreateOrganization": "Organisatie aanmaken mislukt", + "acceptInviteTitle": "Uitnodiging accepteren", + "loadingInvitation": "Uitnodiging laden...", + "invalidInvitationLink": "Ongeldige uitnodigingslink", + "invitationAlreadyAccepted": "Deze uitnodiging is al geaccepteerd. U kunt nu inloggen.", + "errorLoadInvitation": "Kon uitnodiging niet laden", + "organizationLabel": "Organisatie:", + "emailLabel": "E-mail:", + "nameRequired": "Naam is verplicht", + "passwordMinLength8": "Wachtwoord moet minimaal 8 tekens bevatten", + "passwordsDoNotMatch": "Wachtwoorden komen niet overeen", + "labelName": "Naam", + "labelCreatePassword": "Wachtwoord aanmaken", + "labelConfirmPassword": "Bevestig wachtwoord", + "activateAccount": "Account activeren", + "invitationAcceptedRedirect": "Uitnodiging geaccepteerd. Doorsturen naar inloggen...", + "errorAcceptInvitation": "Kon uitnodiging niet accepteren", + "alreadyHaveAccess": "Heeft u al toegang?", + "goToLogin": "Ga naar inloggen", + "acceptOrganizationTitle": "Organisatie-uitnodiging accepteren", + "alreadyHaveAccount": "Heeft u al een account?", + "invitedBy": "Uitgenodigd door:", + "ownerEmail": "E-mail eigenaar", + "activateOrganization": "Organisatie activeren", + "organizationAcceptedRedirect": "Uitnodiging geaccepteerd. Doorsturen naar inloggen...", + "stepAccount": "Account", + "stepOrganization": "Organisatie", + "organizationName": "Organisatienaam", + "organizationNamePlaceholder": "Sunshine Tandartspraktijk", + "organizationEmail": "Organisatie e-mail", + "organizationEmailPlaceholder": "contact@sunshineclinic.com", + "organizationType": "Organisatietype", + "dentalClinic": "Tandartspraktijk", + "dentalLab": "Tandtechnisch Laboratorium" }, "landing": { - "heroTitle": "Connect Dental Clinics & Labs", - "heroHighlight": "Seamlessly", - "heroSubtitle": "Streamline communication between dental professionals. Start with a 30-day free trial, no credit card required.", - "featureClinicsTitle": "For Clinics", - "featureClinicsDescription": "Manage patients, appointments, and send cases to labs instantly.", - "featureLabsTitle": "For Labs", - "featureLabsDescription": "Receive cases, track progress, and communicate with clinics.", - "featureTeamTitle": "Team Management", - "featureTeamDescription": "Add up to 5 team members during trial. Scale as you grow.", - "featureTrialTitle": "30-Day Trial", - "featureTrialDescription": "Full access to all features. No credit card required.", - "featureRealtimeTitle": "Real-time Updates", - "featureRealtimeDescription": "Get instant notifications on case status changes.", - "featureSecurityTitle": "Secure & Compliant", - "featureSecurityDescription": "HIPAA-compliant with enterprise-grade security.", - "footerCopyright": "© 2026 DyoLink. All rights reserved.", - "termsAndConditions": "Terms & Conditions" + "heroTitle": "Verbind Tandheelkundige Klinieken & Laboratoria", + "heroHighlight": "Naadloos", + "heroSubtitle": "Stroomlijn de communicatie tussen tandheelkundige professionals. Start met een gratis proefperiode van 30 dagen, zonder creditcard.", + "featureClinicsTitle": "Voor Klinieken", + "featureClinicsDescription": "Beheer patiënten, afspraken en stuur casussen direct naar laboratoria.", + "featureLabsTitle": "Voor Laboratoria", + "featureLabsDescription": "Ontvang casussen, volg de voortgang en communiceer met klinieken.", + "featureTeamTitle": "Teambeheer", + "featureTeamDescription": "Voeg tijdens de proefperiode maximaal 5 teamleden toe. Schaal mee met uw groei.", + "featureTrialTitle": "Proefperiode van 30 dagen", + "featureTrialDescription": "Volledige toegang tot alle functies. Geen creditcard vereist.", + "featureRealtimeTitle": "Realtime Updates", + "featureRealtimeDescription": "Ontvang direct meldingen bij statuswijzigingen van casussen.", + "featureSecurityTitle": "Veilig & Conform", + "featureSecurityDescription": "HIPAA-conform met beveiliging op ondernemingsniveau.", + "footerCopyright": "© 2026 DyoLink. Alle rechten voorbehouden.", + "termsAndConditions": "Algemene Voorwaarden" }, "accountMenu": { - "noActiveSubscription": "No active subscription — review Subscriptions", - "trialEnded": "Trial ended — review Subscriptions", - "trialEndingSoon": "Trial ending soon — review Subscriptions", - "seatsLow": "Seats running low — review Subscriptions", - "reviewSubscriptions": "Review Subscriptions" + "noActiveSubscription": "Geen actief abonnement — bekijk Abonnementen", + "trialEnded": "Proefperiode beëindigd — bekijk Abonnementen", + "trialEndingSoon": "Proefperiode eindigt binnenkort — bekijk Abonnementen", + "seatsLow": "Aantal beschikbare plaatsen raakt op — bekijk Abonnementen", + "reviewSubscriptions": "Abonnementen bekijken" }, "validation": { - "emailInvalid": "Please enter a valid email address", - "passwordRequired": "Password is required", - "nameMinLength": "Name must be at least 2 characters", - "passwordMinLength": "Password must be at least 8 characters", - "passwordUppercase": "Password must contain at least one uppercase letter", - "passwordNumber": "Password must contain at least one number", - "organizationNameMinLength": "Organization name must be at least 2 characters", - "organizationEmailInvalid": "Please enter a valid organization email", - "organizationTypeRequired": "Please select organization type", - "passwordsDoNotMatch": "Passwords don't match" + "emailInvalid": "Voer een geldig e-mailadres in", + "passwordRequired": "Wachtwoord is verplicht", + "nameMinLength": "Naam moet minimaal 2 tekens bevatten", + "passwordMinLength": "Wachtwoord moet minimaal 8 tekens bevatten", + "passwordUppercase": "Wachtwoord moet minimaal één hoofdletter bevatten", + "passwordNumber": "Wachtwoord moet minimaal één cijfer bevatten", + "organizationNameMinLength": "Organisatienaam moet minimaal 2 tekens bevatten", + "organizationEmailInvalid": "Voer een geldig organisatie-e-mailadres in", + "organizationTypeRequired": "Selecteer een organisatietype", + "passwordsDoNotMatch": "Wachtwoorden komen niet overeen" + }, + "today": { + "welcomeBack": "Welkom terug!!", + "noSubscriptionNotice": "Deze organisatie heeft nog geen actief abonnement.", + "choosePlanLink": "Kies een abonnement", + "noSubscriptionCta": "om het aankoopproces te starten.", + "cardTodaysAppointments": "Afspraken van vandaag", + "cardActivePatients": "Actieve patiënten", + "cardNewLabCase": "Nieuwe laboratoriumcase", + "cardTodayInvoices": "Facturen van vandaag" + }, + "staff": { + "redirecting": "Bezig met doorsturen...", + "title": "Medewerkersbeheer", + "subtitle": "Nodig teamleden uit, stel tabbladtoegang in en blijf binnen het limiet van uw abonnementsplaatsen.", + "inviteMember": "Lid uitnodigen", + "seatsLabel": "Plaatsen:", + "unlimitedPlan": "(onbeperkt abonnement)", + "seatLimitReached": "Plaatslimiet van het abonnement voor deze organisatie is bereikt.", + "noActivePlan": "Geen actief abonnement geselecteerd voor deze organisatie. Kies een abonnementsplan om leden uit te nodigen.", + "invitedPending": "Uitnodiging is in afwachting totdat ze de link openen, een wachtwoord instellen en inloggen.", + "invitedAccepted": "Uitnodiging is onmiddellijk geaccepteerd.", + "inviteLinkHeading": "Uitnodigingslink", + "shareLinkHint": "Deel deze link handmatig via SMS of e-mail. Er wordt een nieuwe link gegenereerd als de vorige is verlopen of verloren is gegaan.", + "loadingTeam": "Team laden...", + "tableName": "Naam", + "tableEmail": "E-mail", + "tableRole": "Rol", + "tableStatus": "Status", + "tableAccess": "Toegang", + "tableAction": "Actie", + "roleOwner": "Eigenaar", + "roleStaff": "Medewerker", + "statusActive": "Actief", + "statusPending": "In afwachting", + "statusDisabled": "Uitgeschakeld", + "statusExpired": "Verlopen", + "allFeatures": "Alle functies", + "inviteModalTitle": "Teamlid uitnodigen", + "stepOf": "Stap {step} van 2", + "permissionView": "Bekijken", + "permissionEdit": "Bewerken", + "labelEmail": "E-mail", + "labelDisplayName": "Weergavenaam", + "tabAccess": "Tabbladtoegang", + "sendInvite": "Uitnodiging verzenden", + "skipForNow": "Nu overslaan", + "enableModalTitle": "Teamlid inschakelen", + "enableConfirm": "{name} ({email}) inschakelen?", + "enableBullet1": "Ze kunnen opnieuw inloggen op deze organisatie met hun bestaande account.", + "enableBullet2": "Er wordt geen nieuwe uitnodiging verzonden en er zijn geen gegevens verwijderd terwijl ze waren uitgeschakeld.", + "enableBullet3": "Inschakelen gebruikt één plaats in uw abonnement.", + "noSeatsAvailable": "Geen plaatsen beschikbaar. Schakel een ander lid uit of upgrade uw abonnement voordat u deze persoon inschakelt.", + "enableMemberButton": "Lid inschakelen", + "disableModalTitle": "Teamlid uitschakelen", + "disableConfirm": "{name} ({email}) uitschakelen?", + "disableBullet1": "Ze kunnen niet inloggen op deze organisatie.", + "disableBullet2": "Er worden geen gegevens verwijderd.", + "disableBullet3": "Uitschakelen maakt één plaats vrij in uw abonnement, zodat u iemand anders kunt uitnodigen.", + "disableMemberButton": "Lid uitschakelen", + "editModalTitle": "Lid bewerken", + "loadingWorkingHours": "Werktijden laden...", + "errorLoadStaff": "Medewerkers laden mislukt.", + "errorCopyInvite": "Kon uitnodigingslink niet kopiëren.", + "errorSendInvite": "Uitnodiging verzenden mislukt.", + "errorLoadWorkingHours": "Werktijden laden mislukt.", + "successMemberUpdated": "Lid bijgewerkt.", + "errorUpdateMember": "Lid bijwerken mislukt.", + "errorDeleteNotImplemented": "Verwijderen is nog niet geïmplementeerd.", + "successMemberDisabled": "{name} is uitgeschakeld. Er is nu een plaats beschikbaar.", + "errorDisableMember": "Lid uitschakelen mislukt.", + "successMemberEnabled": "{name} is ingeschakeld en kan opnieuw inloggen.", + "errorEnableMember": "Lid inschakelen mislukt.", + "successInvited": "{name} ({email}) is uitgenodigd.", + "copyInviteLink": "Uitnodigingslink kopiëren", + "copyInviteLinkTitle": "Uitnodigingslink kopiëren (genereert indien nodig een nieuwe link)", + "enableMemberAria": "Lid inschakelen", + "enableMemberTitle": "Lid inschakelen (gebruikt een plaats)", + "disableMemberAria": "Lid uitschakelen", + "disableMemberTitle": "Lid uitschakelen (maakt een plaats vrij)", + "editMemberAria": "Lid bewerken", + "deleteMemberAria": "Lid verwijderen", + "deleteMemberTitle": "Lid verwijderen (niet geïmplementeerd)", + "features": { + "featureToday": "Vandaag", + "featureStaff": "Medewerkers", + "featureOrganizations": "Organisaties", + "featureClinics": "Klinieken", + "featureLabs": "Laboratoria", + "featurePatients": "Patiënten", + "featureAppointment": "Afspraak", + "featureTreatment": "Behandeling", + "featureBilling": "Facturatie", + "featureReports": "Rapporten", + "noTabAccess": "Geen tabbladtoegang", + "readOnlySuffix": "(Alleen lezen)" + }, + "workingHours": { + "recommendedTitle": "Werktijden aanbevolen", + "recommendedBody": "Medewerkers met bewerkingsrechten voor behandelingen verschijnen als aanbiederkolommen in Afspraken. Stel hun wekelijkse uren in, zodat het rooster de juiste boekbare tijden weergeeft.", + "intro": "Stel wekelijkse werktijden in voor deze aanbieder. Het afsprakenrooster gebruikt deze uren om boekbare tijdsloten weer te geven.", + "workingDay": "Werkdag", + "start": "Start", + "end": "Einde", + "removeShift": "Dienst verwijderen", + "addShift": "Dienst toevoegen", + "autoRepeatWeekly": "Deze uren wekelijks herhalen aan het begin van elke week (kopieer door op maandag)", + "weekdayMon": "Ma", + "weekdayTue": "Di", + "weekdayWed": "Wo", + "weekdayThu": "Do", + "weekdayFri": "Vr", + "weekdaySat": "Za", + "weekdaySun": "Zo", + "validationNeedsShift": "{day} heeft minstens één dienst nodig of moet worden gemarkeerd als vrij.", + "validationEndAfterStart": "Eindtijd van dienst op {day} moet na de starttijd liggen.", + "validationOverlap": "Diensten op {day} mogen niet overlappen." + } + }, + "patients": { + "title": "Patiënten", + "newPatient": "Nieuwe patiënt", + "errorLoadPatients": "Patiënten laden mislukt.", + "successPatientSaved": "Patiënt {firstName} {lastName} is succesvol opgeslagen.", + "errorSavePatient": "Patiënt opslaan mislukt.", + "firstName": "Voornaam", + "lastName": "Achternaam", + "phone": "Telefoon", + "savePatient": "Patiënt opslaan", + "dialogTitle": "Nieuwe patiënt", + "searchPlaceholder": "Zoek patiënten op naam, telefoon, e-mail", + "loadingPatients": "Patiënten laden...", + "noResults": "Geen patiënten gevonden voor deze zoekopdracht.", + "noContact": "Geen contact", + "selectPatient": "Selecteer een patiënt om details te bekijken.", + "phoneLabel": "Telefoon:", + "emailLabel": "E-mail:", + "statusLabel": "Status:", + "statusActive": "Actief", + "statusInactive": "Inactief", + "emptyValue": "-" + }, + "appointments": { + "title": "Afspraken", + "subtitle": "Zoek een patiënt, kies een datum en klik vervolgens op een tijdslot onder een aanbieder om te boeken.", + "loadingSchedule": "Rooster laden...", + "infoPastViewOnly": "Afspraken uit het verleden zijn alleen-lezen.", + "infoSelectPatient": "Selecteer een patiënt voor het boeken.", + "errorOutsideHours": "Deze afspraak valt buiten de huidige werktijden van de aanbieder en kan niet worden bewerkt.", + "successUpdated": "Afspraak bijgewerkt.", + "successSaved": "Afspraak opgeslagen.", + "errorUpdate": "Kon afspraak niet bijwerken.", + "errorSave": "Kon afspraak niet opslaan.", + "confirmRemove": "Deze afspraak verwijderen?", + "successRemoved": "Afspraak verwijderd.", + "errorDelete": "Kon afspraak niet verwijderen.", + "errorLoadSchedule": "Rooster laden mislukt.", + "successPatientSaved": "Patiënt {firstName} {lastName} is opgeslagen.", + "searchPlaceholder": "Bestaande patiënten zoeken", + "searching": "Zoeken...", + "searchHint": "Typ om te zoeken naar patiënten op naam, telefoon of e-mail.", + "noPermissionAdd": "U heeft geen toestemming om patiënten toe te voegen.", + "editTitle": "Afspraak bewerken", + "newTitle": "Nieuwe afspraak", + "providerLabel": "Aanbieder:", + "patientLabel": "Patiënt", + "startLabel": "Start", + "endLabel": "Einde", + "purposeLabel": "Doel", + "errorSelectPatient": "Selecteer eerst een patiënt.", + "errorEndAfterStart": "Eindtijd moet na de starttijd liggen.", + "errorPastSchedule": "Kan niet in het verleden plannen.", + "errorPastViewOnly": "Afspraken uit het verleden zijn alleen-lezen.", + "errorMissingDetails": "Ontbrekende afspraakgegevens.", + "noProviders": "Geen aanbieders beschikbaar. Voeg medewerkers met bewerkingsrechten voor behandelingen toe om hier kolommen te zien.", + "noWorkingHours": "Er zijn geen werktijden geconfigureerd voor deze dag. Stel werktijden van de aanbieder in bij Medewerkersbeheer.", + "noHoursSet": "Geen uren ingesteld", + "offToday": "Vandaag vrij", + "slotOffToday": "Aanbieder is vandaag vrij", + "slotHoursNotConfigured": "Werktijden niet geconfigureerd", + "slotOutsideHours": "Buiten werktijden", + "slotCannotCreate": "U kunt geen afspraken maken", + "slotBookAt": "Boeken om {time}", + "outsideHoursBlocked": "Buiten werktijden — bewerken geblokkeerd", + "overlappingChoose": "{count} overlappend — klik om te kiezen", + "overlapping": "{count} overlappend", + "legend": "Legenda", + "overlappingTitle": "Overlappende afspraken ({count})", + "purposeConsultation": "Consult", + "purposeFilling": "Vulling", + "purposeEndo": "Endo", + "purposeVisit": "Bezoek", + "purposeHygiene": "Hygiëne" + }, + "treatment": { + "loading": "Laden...", + "noPermissionTitle": "Behandelwerkruimte", + "noPermissionBody": "U heeft geen toestemming om het tabblad Behandeling voor deze organisatie te bekijken.", + "title": "Behandeling", + "subtitleEdit": "Documenteer casussen voor uw afspraken, sla concepten op en stuur werk naar gekoppelde organisaties.", + "subtitleReadOnly": "Alleen-lezen toegang — u kunt afspraken en behandelgeschiedenis bekijken, maar niet bewerken.", + "pastDayNotice": "Dagen in het verleden zijn alleen-lezen. U kunt afspraken en geschiedenis bekijken, maar behandelcasussen kunnen niet worden toegevoegd of gewijzigd.", + "selectedPatient": "Geselecteerde patiënt", + "purposeLabel": "Doel:", + "loadingAppointments": "Afspraken laden...", + "selectDayWithAppointment": "Selecteer een dag met ten minste één afspraak.", + "confirmDiscard": "U heeft niet-opgeslagen wijzigingen. Wilt u deze negeren en doorgaan?", + "successDraftSaved": "Behandelconcept opgeslagen.", + "errorChooseOrg": "Kies ten minste één actieve organisatie om deze case te verzenden.", + "successCaseSent": "Case verzonden naar geselecteerde organisaties.", + "successFilesUploaded": "{count} bestand(en) succesvol geüpload.", + "errorLoadAppointments": "Afspraken laden mislukt.", + "errorLoadOrgs": "Gekoppelde organisaties laden mislukt.", + "errorLoadHistory": "Behandelgeschiedenis laden mislukt.", + "errorLoadDraft": "Behandelconcept laden mislukt.", + "errorUpload": "Bijlagen uploaden mislukt.", + "errorSaveDraft": "Behandelconcept opslaan mislukt.", + "errorSendCase": "Case verzenden mislukt.", + "errorCaseMustSave": "Case moet worden opgeslagen voor verzending.", + "draftTitle": "Concept · {patientName}", + "hiddenMessage": "Afspraken zijn verborgen.", + "showAppointments": "Afspraken tonen", + "appointmentsTitle": "Mijn afspraken", + "hideAppointments": "Afspraken verbergen", + "emptyDay": "Geen afspraken aan u toegewezen op deze dag.", + "casesTitle": "Behandelcasussen", + "casesSubtitle": "Elke case heeft zijn eigen tanden, notities, bijlagen en verzendbestemmingen.", + "addCase": "Case toevoegen", + "caseLabel": "Case {n}", + "comments": "Opmerkingen", + "commentsPlaceholder": "Schrijf klinische notities voor deze case...", + "treatmentType": "Behandeltype", + "typeConsultation": "consult", + "typeFilling": "vulling", + "typeEndo": "endo", + "typeVisit": "bezoek", + "typeHygiene": "hygiëne", + "attachments": "Bijlagen", + "attachFiles": "Voeg bestanden toe voor deze behandelscase", + "chooseFiles": "Kies bestanden", + "sendToOrgs": "Verzend deze case naar gekoppelde organisaties", + "searchOrgsPlaceholder": "Zoek actieve organisaties...", + "recent": "Recent:", + "noOrgMatch": "Geen actieve organisatie komt overeen met uw zoekopdracht.", + "sendThisCase": "Verzend deze case", + "saveDraft": "Behandelconcept opslaan", + "unsavedChanges": "Niet-opgeslagen wijzigingen", + "draftSaved": "Concept opgeslagen", + "sendSavesFirst": "Verzenden is per case en slaat eerst automatisch op.", + "historyTitle": "Eerdere behandelingen", + "historySubtitle": "Voltooide behandelingen voor deze patiënt. Elke case wordt afzonderlijk weergegeven.", + "loadingHistory": "Geschiedenis laden...", + "historyEmpty": "Geen eerdere behandelingen voor deze patiënt.", + "statusLabel": "Status:", + "historyCaseLabel": "Case {n} · {type}", + "teethLabel": "Tanden:", + "teethNone": "Geen geselecteerd", + "reviewDetails": "Details bekijken", + "previewTitle": "Behandelvoorbeeld", + "previewDraft": "Bekijk huidig concept", + "selectAppointment": "Selecteer een afspraak om het concept te bekijken.", + "caseCount": "{n} case(s)", + "attachmentCount": "{n} bijlage(n)", + "caseSummary": "Case {n}: {type}", + "teethPrefix": "· Tanden", + "moreCases": "+ {n} meer case(s)", + "previewDialogTitle": "Behandelvoorbeeld", + "previewDialogSubtitle": "Bekijk casussen, bijlagen en verzendbestemmingen.", + "noCases": "Geen casussen in deze behandeling.", + "typeLabel": "Type:", + "commentsLabel": "Opmerkingen:", + "commentsEmpty": "Opmerkingen: —", + "attachFilesShort": "Bestanden bijvoegen", + "sendCase": "Verzend deze case", + "sendToLinkedOrgs": "Verzenden naar gekoppelde organisaties", + "noActiveOrgs": "Geen actieve gekoppelde organisaties.", + "confirmSend": "Bevestig verzending", + "toothChartTitle": "FDI-tanddiagram", + "toothChartHint": "Tik op tanden om meerdere te selecteren. Geldt voor de actieve case.", + "selectedLabel": "Geselecteerd:", + "selectedEmpty": "—", + "upperArch": "Bovenboog", + "lowerArch": "Onderboog", + "toothAria": "FDI-tand {fdi}", + "toothSelectedSuffix": ", geselecteerd", + "sentToAt": "Verzonden naar {orgName} op {datetime}", + "fallbackOrgName": "organisatie" + }, + "organizations": { + "loadingOrganization": "Organisatie laden...", + "subtitle": "Zoek organisaties, stuur verbindingsverzoeken naar bestaande accounts, of uitnodigingslinks wanneer ze nog niet op DyoLink staan.", + "invitationHistory": "Uitnodigingsgeschiedenis", + "searchPlaceholder": "Zoek {counterpart} op naam, e-mail of telefoon...", + "backToList": "Terug naar lijst", + "tableOrganization": "Organisatie", + "tableOwnerEmail": "E-mail eigenaar", + "tableDate": "Datum", + "tableStatus": "Status", + "tableAction": "Actie", + "emptyConnections": "Nog geen verbindingen. Zoek om een verbindingsverzoek of een uitnodigingslink te verzenden.", + "statusInvitationPending": "Uitnodiging in afwachting", + "statusConnectionPending": "Verbindingsverzoek in afwachting", + "statusConnected": "Verbonden", + "statusDeclined": "Verbindingsverzoek afgewezen", + "statusFound": "Gevonden", + "statusToday": "Vandaag", + "acceptRequest": "Verbindingsverzoek accepteren", + "declineRequest": "Verbindingsverzoek afwijzen", + "removeConnection": "Verbinding verwijderen", + "sendRequest": "Verbindingsverzoek verzenden", + "noDirectoryResults": "Geen organisatie gevonden in directoryzoekopdracht.", + "hideInvitationFields": "Uitnodigingsvelden verbergen", + "sendInvitationLink": "Uitnodigingslink verzenden", + "counterpartNameLabel": "Naam {counterpart}", + "ownerEmailLabel": "E-mail eigenaar", + "sendInvitation": "Uitnodiging verzenden", + "successConnectionSent": "Verbindingsverzoek voor {counterpart} verzonden.", + "successInviteCreated": "Uitnodigingslink aangemaakt voor {email}", + "successLinkCopied": "Uitnodigingslink gekopieerd naar klembord.", + "successAccepted": "Verbindingsverzoek geaccepteerd.", + "successDeclined": "Verbindingsverzoek afgewezen.", + "successRemoved": "Verbinding verwijderd.", + "historyTitle": "Uitnodigingsgeschiedenis", + "loadingHistory": "Uitnodigingsgeschiedenis laden...", + "historyEmpty": "Nog geen uitnodigingen.", + "tableInvitationLink": "Uitnodigingslink", + "statusPending": "Uitnodiging in afwachting", + "statusAccepted": "Uitnodiging geaccepteerd", + "statusRejected": "Uitnodiging afgewezen", + "statusExpired": "Uitnodiging verlopen", + "copyInvitationLink": "Uitnodigingslink kopiëren", + "copyInvitationLinkTitle": "Uitnodigingslink kopiëren (genereert indien nodig een nieuwe link)", + "selectorTitle": "Organisaties", + "selectorSubtitleWithCreate": "Selecteer een organisatie om door te gaan, of maak een nieuwe aan.", + "selectorSubtitleSelectOnly": "Selecteer een organisatie om door te gaan.", + "createOrganization": "Organisatie aanmaken", + "createAndContinue": "Aanmaken en doorgaan", + "emptyCanCreate": "Geen organisaties gevonden. Maak uw eerste organisatie aan om door te gaan.", + "emptyAskOwner": "Geen organisaties gevonden. Vraag een organisatie-eigenaar om u uit te nodigen.", + "continueArrow": "Doorgaan →", + "planLabel": "Plan: {name} • {maxUsers} gebruikers", + "counterpartClinic": "Kliniek", + "counterpartLab": "Laboratorium" + }, + "settings": { + "accountTitle": "Account", + "accountSubtitle": "Profiel- en beveiligingsinstellingen voor uw login.", + "accountPlaceholder": "Wachtwoordwijziging en profielbewerking worden hierna hier aangesloten (bijv. uitnodigingsflow, wachtwoord herstellen).", + "subscriptionsTitle": "Abonnementen", + "subscriptionsSubtitle": "Uw DyoLink-werkruimteplan en plaatsen voor {orgName}. Kliniek- en laboratoriuminkomsten blijven onder het tabblad Facturatie in de zijbalk.", + "noSubscriptionNotice": "Deze organisatie heeft geen actief abonnement. Selecteer hieronder een abonnement om het aankoopproces te starten.", + "currentPlan": "Huidig abonnement", + "planPrice": "Abonnementsprijs", + "seatsUsed": "Plaatsen gebruikt", + "seatsRemaining": "Plaatsen over", + "daysRemaining": "Dagen over", + "unlimited": "Onbeperkt", + "unlimitedSeats": "Onbeperkte plaatsen", + "seatsCount": "{n} plaatsen", + "pricePerMonth": "${price} / maand", + "noActiveSubscription": "Geen actief abonnement voor deze organisatie.", + "trialEnded": "Proefperiode is beëindigd. Kies een abonnement wanneer afrekenen beschikbaar is.", + "trialEndsIn": "Proefperiode eindigt over {days} dag(en).", + "seatsLow": "Plaatsgebruik is hoog voor deze organisatie.", + "choosePlanIntro": "Kies een abonnement om door te gaan. Aankoopintegratie is nog niet actief, dus dit bereidt momenteel alleen de selectiestap voor.", + "planSolo": "Solo", + "planSmall": "Klein", + "planMedium": "Middel", + "planLarge": "Groot", + "planEnterprise": "Onderneming", + "startPurchase": "Aankoopproces starten", + "purchaseNotice": "Aankoopstroom wordt binnenkort ingeschakeld. {plan} is geselecteerd en klaar voor afrekenconfiguratie." + }, + "schedule": { + "defaultLabel": "Roosterdatum", + "previousDay": "Vorige dag", + "nextDay": "Volgende dag", + "chooseDate": "Kies roosterdatum", + "year": "Jaar", + "month": "Maand", + "day": "Dag", + "monthJanuary": "Januari", + "monthFebruary": "Februari", + "monthMarch": "Maart", + "monthApril": "April", + "monthMay": "Mei", + "monthJune": "Juni", + "monthJuly": "Juli", + "monthAugust": "Augustus", + "monthSeptember": "September", + "monthOctober": "Oktober", + "monthNovember": "November", + "monthDecember": "December" } -} +} \ No newline at end of file diff --git a/frontend/scripts/build-messages.mjs b/frontend/scripts/build-messages.mjs new file mode 100644 index 0000000..d832eee --- /dev/null +++ b/frontend/scripts/build-messages.mjs @@ -0,0 +1,619 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const messagesDir = path.join(__dirname, '..', 'messages'); + +const en = { + common: { + appName: 'DyoLink', + loading: 'Loading...', + loadingApp: 'Loading app...', + loadingWorkspace: 'Loading workspace...', + continue: 'Continue', + back: 'Back', + save: 'Save', + cancel: 'Cancel', + delete: 'Delete', + edit: 'Edit', + next: 'Next', + dismiss: 'Dismiss', + or: 'Or', + and: 'and', + close: 'Close', + redirecting: 'Redirecting…', + readOnlyAccess: 'Read-only access for this organization.', + errorGeneric: 'Something went wrong', + loadingEllipsis: 'Loading...', + search: 'Search', + action: 'Action', + status: 'Status', + name: 'Name', + email: 'Email', + date: 'Date', + organization: 'Organization', + backToApp: '← Back to app', + copied: 'Copied', + copyLink: 'Copy link', + none: 'None', + preview: 'Preview', + }, + language: { + label: 'Language', + selectLanguage: 'Select language', + en: 'English', + fa: 'Persian', + nl: 'Dutch', + }, + theme: { + switchToLight: 'Switch to light mode', + switchToDark: 'Switch to dark mode', + lightMode: 'Light mode', + darkMode: 'Dark mode', + }, + nav: { + dashboard: 'Dashboard', + staff: 'Staff', + patients: 'Patients', + appointment: 'Appointment', + treatment: 'Treatment', + billing: 'Billing', + reports: 'Reports', + clinics: 'Clinics', + labs: 'Labs', + }, + auth: { + login: 'Login', + signIn: 'Sign in', + signOut: 'Log out', + register: 'Register', + startTrial: 'Start Trial', + startFreeTrial: 'Start Free Trial', + dashboard: 'Dashboard', + signInTitle: 'Sign in to your account', + signInPrompt: 'Or {link}', + startTrialLink: 'start your free trial', + registerTitle: 'Start your 30-day free trial', + registerPrompt: 'Already have an account?', + signInLink: 'Sign in', + email: 'Email address', + password: 'Password', + confirmPassword: 'Confirm password', + fullName: 'Full name', + rememberMe: 'Remember me', + forgotPassword: 'Forgot your password?', + invalidCredentials: 'Invalid email or password', + loginFailed: 'Login failed', + registrationFailed: 'Registration failed. Please try again.', + startMyFreeTrial: 'Start my free trial', + trialIncludes: 'Your trial includes:', + trialTeamMembers: 'Up to 5 team members', + trialFullAccess: 'Full access to all features', + trialNoCard: '30 days free, no credit card required', + termsAgreement: 'By signing up, you agree to our {terms} and {privacy}', + termsOfService: 'Terms of Service', + privacyPolicy: 'Privacy Policy', + signedIn: 'Signed in', + switchOrganization: 'Switch organization', + subscriptions: 'Subscriptions', + account: 'Account', + emailPlaceholder: 'you@example.com', + passwordPlaceholder: '••••••••', + namePlaceholder: 'John Doe', + termsIntro: 'By signing up, you agree to our', + errorRegistrationFailed: 'Registration failed', + errorLoginFailed: 'Login failed', + errorCreateOrganization: 'Failed to create organization', + acceptInviteTitle: 'Accept invitation', + loadingInvitation: 'Loading invitation...', + invalidInvitationLink: 'Invalid invitation link', + invitationAlreadyAccepted: 'This invitation is already accepted. You can log in now.', + errorLoadInvitation: 'Could not load invitation', + organizationLabel: 'Organization:', + emailLabel: 'Email:', + nameRequired: 'Name is required', + passwordMinLength8: 'Password must be at least 8 characters', + passwordsDoNotMatch: 'Passwords do not match', + labelName: 'Name', + labelCreatePassword: 'Create password', + labelConfirmPassword: 'Confirm password', + activateAccount: 'Activate account', + invitationAcceptedRedirect: 'Invitation Accepted. Redirecting to login...', + errorAcceptInvitation: 'Could not accept invitation', + alreadyHaveAccess: 'Already have access?', + goToLogin: 'Go to login', + acceptOrganizationTitle: 'Accept organization invitation', + alreadyHaveAccount: 'Already have an account?', + invitedBy: 'Invited by:', + ownerEmail: 'Owner email', + activateOrganization: 'Activate organization', + organizationAcceptedRedirect: 'Invitation accepted. Redirecting to login...', + stepAccount: 'Account', + stepOrganization: 'Organization', + organizationName: 'Organization name', + organizationNamePlaceholder: 'Sunshine Dental Clinic', + organizationEmail: 'Organization email', + organizationEmailPlaceholder: 'contact@sunshineclinic.com', + organizationType: 'Organization type', + dentalClinic: 'Dental Clinic', + dentalLab: 'Dental Lab', + }, + landing: { + heroTitle: 'Connect Dental Clinics & Labs', + heroHighlight: 'Seamlessly', + heroSubtitle: + 'Streamline communication between dental professionals. Start with a 30-day free trial, no credit card required.', + featureClinicsTitle: 'For Clinics', + featureClinicsDescription: + 'Manage patients, appointments, and send cases to labs instantly.', + featureLabsTitle: 'For Labs', + featureLabsDescription: 'Receive cases, track progress, and communicate with clinics.', + featureTeamTitle: 'Team Management', + featureTeamDescription: 'Add up to 5 team members during trial. Scale as you grow.', + featureTrialTitle: '30-Day Trial', + featureTrialDescription: 'Full access to all features. No credit card required.', + featureRealtimeTitle: 'Real-time Updates', + featureRealtimeDescription: 'Get instant notifications on case status changes.', + featureSecurityTitle: 'Secure & Compliant', + featureSecurityDescription: 'HIPAA-compliant with enterprise-grade security.', + footerCopyright: '© 2026 DyoLink. All rights reserved.', + termsAndConditions: 'Terms & Conditions', + }, + accountMenu: { + noActiveSubscription: 'No active subscription — review Subscriptions', + trialEnded: 'Trial ended — review Subscriptions', + trialEndingSoon: 'Trial ending soon — review Subscriptions', + seatsLow: 'Seats running low — review Subscriptions', + reviewSubscriptions: 'Review Subscriptions', + }, + validation: { + emailInvalid: 'Please enter a valid email address', + passwordRequired: 'Password is required', + nameMinLength: 'Name must be at least 2 characters', + passwordMinLength: 'Password must be at least 8 characters', + passwordUppercase: 'Password must contain at least one uppercase letter', + passwordNumber: 'Password must contain at least one number', + organizationNameMinLength: 'Organization name must be at least 2 characters', + organizationEmailInvalid: 'Please enter a valid organization email', + organizationTypeRequired: 'Please select organization type', + passwordsDoNotMatch: "Passwords don't match", + }, + today: { + welcomeBack: 'Welcome back!!', + noSubscriptionNotice: 'This organization does not have an active subscription yet.', + choosePlanLink: 'Choose a plan', + noSubscriptionCta: 'to start the purchase process.', + cardTodaysAppointments: "Today's Appointments", + cardActivePatients: 'Active Patients', + cardNewLabCase: 'New Lab Case', + cardTodayInvoices: 'Today invoices', + }, + staff: { + redirecting: 'Redirecting…', + title: 'Staff Management', + subtitle: 'Invite teammates, set tab access, and stay within your plan seat limit.', + inviteMember: 'Invite member', + seatsLabel: 'Seats:', + unlimitedPlan: '(unlimited plan)', + seatLimitReached: 'Plan seat limit reached for this organization.', + noActivePlan: + 'No active plan selected for this organization. Choose a subscription plan to invite members.', + invitedPending: + 'Invitation is pending until they open the link, set a password, and log in.', + invitedAccepted: 'Invitation was accepted immediately.', + inviteLinkHeading: 'Invite link', + shareLinkHint: + 'Share this link manually via SMS or email. A new link is generated if the previous one expired or was lost.', + loadingTeam: 'Loading team…', + tableName: 'Name', + tableEmail: 'Email', + tableRole: 'Role', + tableStatus: 'Status', + tableAccess: 'Access', + tableAction: 'Action', + roleOwner: 'Owner', + roleStaff: 'Staff', + statusActive: 'Active', + statusPending: 'Pending', + statusDisabled: 'Disabled', + statusExpired: 'Expired', + allFeatures: 'All features', + inviteModalTitle: 'Invite team member', + stepOf: 'Step {step} of 2', + permissionView: 'View', + permissionEdit: 'Edit', + labelEmail: 'Email', + labelDisplayName: 'Display name', + tabAccess: 'Tab access', + sendInvite: 'Send invite', + skipForNow: 'Skip for now', + enableModalTitle: 'Enable team member', + enableConfirm: 'Enable {name} ({email})?', + enableBullet1: 'They can sign in to this organization again with their existing account.', + enableBullet2: 'No new invitation is sent and no data was removed while they were disabled.', + enableBullet3: 'Enabling uses one seat on your plan.', + noSeatsAvailable: + 'No seats are available. Disable another member or upgrade your plan before enabling this person.', + enableMemberButton: 'Enable member', + disableModalTitle: 'Disable team member', + disableConfirm: 'Disable {name} ({email})?', + disableBullet1: 'They will not be able to sign in to this organization.', + disableBullet2: 'No data will be removed.', + disableBullet3: 'Disabling frees one seat on your plan so you can invite someone else.', + disableMemberButton: 'Disable member', + editModalTitle: 'Edit member', + loadingWorkingHours: 'Loading working hours…', + errorLoadStaff: 'Failed to load staff.', + errorCopyInvite: 'Could not copy invitation link.', + errorSendInvite: 'Failed to send invitation.', + errorLoadWorkingHours: 'Failed to load working hours.', + successMemberUpdated: 'Member updated.', + errorUpdateMember: 'Failed to update member.', + errorDeleteNotImplemented: 'Delete is not implemented yet.', + successMemberDisabled: '{name} was disabled. A seat is now available.', + errorDisableMember: 'Failed to disable member.', + successMemberEnabled: '{name} was enabled and can sign in again.', + errorEnableMember: 'Failed to enable member.', + successInvited: '{name} ({email}) was invited.', + copyInviteLink: 'Copy invitation link', + copyInviteLinkTitle: 'Copy invitation link (generates a new link if needed)', + enableMemberAria: 'Enable member', + enableMemberTitle: 'Enable member (uses a seat)', + disableMemberAria: 'Disable member', + disableMemberTitle: 'Disable member (frees a seat)', + editMemberAria: 'Edit member', + deleteMemberAria: 'Delete member', + deleteMemberTitle: 'Delete member (not implemented)', + features: { + featureToday: 'Today', + featureStaff: 'Staff', + featureOrganizations: 'Organizations', + featureClinics: 'Clinics', + featureLabs: 'Labs', + featurePatients: 'Patients', + featureAppointment: 'Appointment', + featureTreatment: 'Treatment', + featureBilling: 'Billing', + featureReports: 'Reports', + noTabAccess: 'No tab access', + readOnlySuffix: '(Read only)', + }, + workingHours: { + recommendedTitle: 'Working hours recommended', + recommendedBody: + 'Staff with treatment edit access appear as provider columns in Appointments. Set their weekly hours so the schedule grid shows the right bookable times.', + intro: + 'Set weekly working hours for this provider. The appointments grid uses these hours to show bookable time slots.', + workingDay: 'Working day', + start: 'Start', + end: 'End', + removeShift: 'Remove shift', + addShift: 'Add shift', + autoRepeatWeekly: 'Repeat these hours at the start of each week (copy forward on Monday)', + weekdayMon: 'Mon', + weekdayTue: 'Tue', + weekdayWed: 'Wed', + weekdayThu: 'Thu', + weekdayFri: 'Fri', + weekdaySat: 'Sat', + weekdaySun: 'Sun', + validationNeedsShift: '{day} needs at least one shift or should be marked off.', + validationEndAfterStart: '{day} shift end time must be after start time.', + validationOverlap: '{day} shifts cannot overlap.', + }, + }, + patients: { + title: 'Patients', + newPatient: 'New Patient', + errorLoadPatients: 'Failed to load patients.', + successPatientSaved: 'Patient {firstName} {lastName} was saved successfully.', + errorSavePatient: 'Failed to save patient.', + firstName: 'First name', + lastName: 'Last name', + phone: 'Phone', + savePatient: 'Save Patient', + dialogTitle: 'New patient', + searchPlaceholder: 'Search patients by name, phone, email', + loadingPatients: 'Loading patients...', + noResults: 'No patients found for this search.', + noContact: 'No contact', + selectPatient: 'Select a patient to view details.', + phoneLabel: 'Phone:', + emailLabel: 'Email:', + statusLabel: 'Status:', + statusActive: 'Active', + statusInactive: 'Inactive', + emptyValue: '-', + }, + appointments: { + title: 'Appointments', + subtitle: 'Search a patient, pick a date, then click a time slot under a provider to book.', + loadingSchedule: 'Loading schedule…', + infoPastViewOnly: 'Past appointments are view-only.', + infoSelectPatient: 'Select a patient before booking.', + errorOutsideHours: + "This appointment falls outside the provider's current working hours and cannot be edited.", + successUpdated: 'Appointment updated.', + successSaved: 'Appointment saved.', + errorUpdate: 'Could not update appointment.', + errorSave: 'Could not save appointment.', + confirmRemove: 'Remove this appointment?', + successRemoved: 'Appointment removed.', + errorDelete: 'Could not delete appointment.', + errorLoadSchedule: 'Failed to load schedule.', + successPatientSaved: 'Patient {firstName} {lastName} was saved.', + searchPlaceholder: 'Search existing patients', + searching: 'Searching…', + searchHint: 'Type to search patients by name, phone, or email.', + noPermissionAdd: 'You do not have permission to add patients.', + editTitle: 'Edit appointment', + newTitle: 'New appointment', + providerLabel: 'Provider:', + patientLabel: 'Patient', + startLabel: 'Start', + endLabel: 'End', + purposeLabel: 'Purpose', + errorSelectPatient: 'Select a patient first.', + errorEndAfterStart: 'End time must be after start time.', + errorPastSchedule: 'Cannot schedule in the past.', + errorPastViewOnly: 'Past appointments are view-only.', + errorMissingDetails: 'Missing appointment details.', + noProviders: + 'No providers available. Add staff with treatment edit access to see columns here.', + noWorkingHours: + 'No working hours are configured for this day. Set provider working hours in Staff management.', + noHoursSet: 'No hours set', + offToday: 'Off today', + slotOffToday: 'Provider is off today', + slotHoursNotConfigured: 'Working hours not configured', + slotOutsideHours: 'Outside working hours', + slotCannotCreate: 'You cannot create appointments', + slotBookAt: 'Book {time}', + outsideHoursBlocked: 'Outside working hours — editing blocked', + overlappingChoose: '{count} overlapping — click to choose', + overlapping: '{count} overlapping', + legend: 'Legend', + overlappingTitle: 'Overlapping appointments ({count})', + purposeConsultation: 'Consultation', + purposeFilling: 'Filling', + purposeEndo: 'Endo', + purposeVisit: 'Visit', + purposeHygiene: 'Hygiene', + }, + treatment: { + loading: 'Loading…', + noPermissionTitle: 'Treatment workspace', + noPermissionBody: 'You do not have permission to view the Treatment tab for this organization.', + title: 'Treatment', + subtitleEdit: + 'Document cases for your appointments, save drafts, and send work to linked organizations.', + subtitleReadOnly: + 'View-only access — you can review appointments and treatment history but cannot edit.', + pastDayNotice: + 'Past days are view-only. You can review appointments and history, but treatment cases cannot be added or changed.', + selectedPatient: 'Selected patient', + purposeLabel: 'Purpose:', + loadingAppointments: 'Loading appointments…', + selectDayWithAppointment: 'Select a day with at least one appointment.', + confirmDiscard: 'You have unsaved changes. Discard them and continue?', + successDraftSaved: 'Treatment draft saved.', + errorChooseOrg: 'Choose at least one active organization to send this case.', + successCaseSent: 'Case sent to selected organizations.', + successFilesUploaded: '{count} file(s) uploaded successfully.', + errorLoadAppointments: 'Failed to load appointments.', + errorLoadOrgs: 'Failed to load linked organizations.', + errorLoadHistory: 'Failed to load treatment history.', + errorLoadDraft: 'Failed to load treatment draft.', + errorUpload: 'Failed to upload attachments.', + errorSaveDraft: 'Failed to save treatment draft.', + errorSendCase: 'Failed to send case.', + errorCaseMustSave: 'Case must be saved before sending.', + draftTitle: 'Draft · {patientName}', + hiddenMessage: 'Appointments are hidden.', + showAppointments: 'Show appointments', + appointmentsTitle: 'My appointments', + hideAppointments: 'Hide appointments', + emptyDay: 'No appointments assigned to you on this day.', + casesTitle: 'Treatment cases', + casesSubtitle: 'Each case has its own teeth, notes, attachments, and destinations for send.', + addCase: 'Add case', + caseLabel: 'Case {n}', + comments: 'Comments', + commentsPlaceholder: 'Write clinical notes for this case…', + treatmentType: 'Treatment type', + typeConsultation: 'consultation', + typeFilling: 'filling', + typeEndo: 'endo', + typeVisit: 'visit', + typeHygiene: 'hygiene', + attachments: 'Attachments', + attachFiles: 'Attach files for this treatment case', + chooseFiles: 'Choose files', + sendToOrgs: 'Send this case to linked organizations', + searchOrgsPlaceholder: 'Search active organizations...', + recent: 'Recent:', + noOrgMatch: 'No active organization matches your search.', + sendThisCase: 'Send this case', + saveDraft: 'Save treatment draft', + unsavedChanges: 'Unsaved changes', + draftSaved: 'Draft saved', + sendSavesFirst: 'Sending is per case and saves first automatically.', + historyTitle: 'Previous treatments', + historySubtitle: 'Completed treatments for this patient. Each case is listed separately.', + loadingHistory: 'Loading history…', + historyEmpty: 'No prior treatments for this patient.', + statusLabel: 'Status:', + historyCaseLabel: 'Case {n} · {type}', + teethLabel: 'Teeth:', + teethNone: 'None selected', + reviewDetails: 'Review details', + previewTitle: 'Treatment preview', + previewDraft: 'Preview current draft', + selectAppointment: 'Select an appointment to preview its draft.', + caseCount: '{n} case(s)', + attachmentCount: '{n} attachment(s)', + caseSummary: 'Case {n}: {type}', + teethPrefix: '· Teeth', + moreCases: '+ {n} more case(s)', + previewDialogTitle: 'Treatment preview', + previewDialogSubtitle: 'Review cases, attachments, and send destinations.', + noCases: 'No cases in this treatment.', + typeLabel: 'Type:', + commentsLabel: 'Comments:', + commentsEmpty: 'Comments: —', + attachFilesShort: 'Attach files', + sendCase: 'Send this case', + sendToLinkedOrgs: 'Send to linked organizations', + noActiveOrgs: 'No active linked organizations.', + confirmSend: 'Confirm send', + toothChartTitle: 'FDI tooth chart', + toothChartHint: 'Tap teeth to multi-select. Applies to the active case.', + selectedLabel: 'Selected:', + selectedEmpty: '—', + upperArch: 'Upper arch', + lowerArch: 'Lower arch', + toothAria: 'FDI tooth {fdi}', + toothSelectedSuffix: ', selected', + sentToAt: 'Sent to {orgName} at {datetime}', + fallbackOrgName: 'organization', + }, + organizations: { + loadingOrganization: 'Loading organization...', + subtitle: + 'Search organizations, send connection requests to existing accounts, or invitation links when they are not on DyoLink yet.', + invitationHistory: 'Invitation History', + searchPlaceholder: 'Search {counterpart} by name, email, or phone...', + backToList: 'Back to list', + tableOrganization: 'Organization', + tableOwnerEmail: 'Owner email', + tableDate: 'Date', + tableStatus: 'Status', + tableAction: 'Action', + emptyConnections: 'No connections yet. Search to send a connection request or an invitation link.', + statusInvitationPending: 'Invitation pending', + statusConnectionPending: 'Connection request pending', + statusConnected: 'Connected', + statusDeclined: 'Connection request declined', + statusFound: 'Found', + statusToday: 'Today', + acceptRequest: 'Accept connection request', + declineRequest: 'Decline connection request', + removeConnection: 'Remove connection', + sendRequest: 'Send connection request', + noDirectoryResults: 'No organization found in directory search.', + hideInvitationFields: 'Hide invitation fields', + sendInvitationLink: 'Send invitation link', + counterpartNameLabel: '{counterpart} name', + ownerEmailLabel: 'Owner email', + sendInvitation: 'Send invitation', + successConnectionSent: '{counterpart} connection request sent.', + successInviteCreated: 'Invitation link created for {email}', + successLinkCopied: 'Invitation link copied to clipboard.', + successAccepted: 'Connection request accepted.', + successDeclined: 'Connection request declined.', + successRemoved: 'Connection removed.', + historyTitle: 'Invitation History', + loadingHistory: 'Loading invitation history...', + historyEmpty: 'No invitations yet.', + tableInvitationLink: 'Invitation link', + statusPending: 'Invitation pending', + statusAccepted: 'Invitation accepted', + statusRejected: 'Invitation rejected', + statusExpired: 'Invitation expired', + copyInvitationLink: 'Copy invitation link', + copyInvitationLinkTitle: 'Copy invitation link (generates a new link if needed)', + selectorTitle: 'Organizations', + selectorSubtitleWithCreate: 'Select an organization to continue, or create a new one.', + selectorSubtitleSelectOnly: 'Select an organization to continue.', + createOrganization: 'Create Organization', + createAndContinue: 'Create and Continue', + emptyCanCreate: 'No organizations found. Create your first one to continue.', + emptyAskOwner: 'No organizations found. Ask an organization owner to invite you.', + continueArrow: 'Continue →', + planLabel: 'Plan: {name} • {maxUsers} users', + }, + settings: { + accountTitle: 'Account', + accountSubtitle: 'Profile and security settings for your login.', + accountPlaceholder: + 'Password change and profile editing will be wired here next (e.g. invite flow, reset password).', + subscriptionsTitle: 'Subscriptions', + subscriptionsSubtitle: + 'Your DyoLink workspace plan and seats for {orgName}. Clinic and lab income tracking stays under the sidebar Billing tab.', + noSubscriptionNotice: + 'This organization has no active subscription. Select a plan below to start the purchase process.', + currentPlan: 'Current plan', + planPrice: 'Plan price', + seatsUsed: 'Seats used', + seatsRemaining: 'Seats remaining', + daysRemaining: 'Days remaining', + unlimited: 'Unlimited', + unlimitedSeats: 'Unlimited seats', + seatsCount: '{n} seats', + pricePerMonth: '${price} / month', + noActiveSubscription: 'No active subscription for this organization.', + trialEnded: 'Trial period has ended. Choose a plan when checkout is available.', + trialEndsIn: 'Trial ends in {days} day(s).', + seatsLow: 'Seat usage is high for this organization.', + choosePlanIntro: + 'Choose a plan to continue. Purchase integration is not active yet, so this currently prepares the selection step only.', + planSolo: 'Solo', + planSmall: 'Small', + planMedium: 'Medium', + planLarge: 'Large', + planEnterprise: 'Enterprise', + startPurchase: 'Start purchase process', + purchaseNotice: + 'Purchase flow will be enabled soon. {plan} is selected and ready for checkout setup.', + }, + schedule: { + defaultLabel: 'Schedule date', + previousDay: 'Previous day', + nextDay: 'Next day', + chooseDate: 'Choose schedule date', + year: 'Year', + month: 'Month', + day: 'Day', + monthJanuary: 'January', + monthFebruary: 'February', + monthMarch: 'March', + monthApril: 'April', + monthMay: 'May', + monthJune: 'June', + monthJuly: 'July', + monthAugust: 'August', + monthSeptember: 'September', + monthOctober: 'October', + monthNovember: 'November', + monthDecember: 'December', + }, +}; + +function deepMerge(base, overlay) { + const result = { ...base }; + for (const key of Object.keys(base)) { + const baseVal = base[key]; + const overlayVal = overlay?.[key]; + if (baseVal && typeof baseVal === 'object' && !Array.isArray(baseVal)) { + result[key] = deepMerge(baseVal, overlayVal ?? {}); + } else if (overlayVal !== undefined) { + result[key] = overlayVal; + } + } + return result; +} + +function writeJson(file, data) { + fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); +} + +writeJson(path.join(messagesDir, 'en.json'), en); + +for (const locale of ['fa', 'nl']) { + const file = path.join(messagesDir, `${locale}.json`); + const existing = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : {}; + writeJson(file, deepMerge(en, existing)); +} + +console.log('Messages built: en.json updated; fa.json and nl.json merged with existing translations.'); diff --git a/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx b/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx index f7ec1ad..10207e0 100644 --- a/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx +++ b/frontend/src/app/[locale]/(dashboard)/appointments/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { appointmentsApi } from '@/lib/api/appointments'; import { patientsApi } from '@/lib/api/patients'; import { useAuth } from '@/lib/hooks/useAuth'; @@ -28,6 +29,8 @@ const EMPTY_PATIENT_FORM: CreatePatientInput = { }; export default function AppointmentsPage() { + const t = useTranslations('appointments'); + const tPatients = useTranslations('patients'); const { currentOrganization } = useAuth(); const [scheduleDate, setScheduleDate] = useState(() => startOfLocalDay(new Date())); @@ -99,13 +102,13 @@ export default function AppointmentsPage() { if (gen !== scheduleLoadGen.current) { return; } - toast.showError(formatApiErrorMessage(err, 'Failed to load schedule.')); + toast.showError(formatApiErrorMessage(err, t('errorLoadSchedule'))); } finally { if (gen === scheduleLoadGen.current) { setLoadingSchedule(false); } } - }, [currentOrganization?.id, scheduleDate]); + }, [currentOrganization?.id, scheduleDate, t]); useEffect(() => { void loadSchedule(); @@ -149,12 +152,17 @@ export default function AppointmentsPage() { setPatientForm(EMPTY_PATIENT_FORM); await loadPatientsSearch(search); setSelectedPatient(response.data); - toast.showSuccess(`Patient ${response.data.firstName} ${response.data.lastName} was saved.`); + toast.showSuccess( + t('successPatientSaved', { + firstName: response.data.firstName, + lastName: response.data.lastName, + }), + ); } catch (err: unknown) { const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) - : 'Failed to save patient.'; + : tPatients('errorSavePatient'); toast.showError(message); } finally { setSavingPatient(false); @@ -163,11 +171,11 @@ export default function AppointmentsPage() { function handleSlotClick(startMinute: number, providerUserId: string, providerName: string) { if (isViewingPastDay) { - toast.showInfo('Past appointments are view-only.'); + toast.showInfo(t('infoPastViewOnly')); return; } if (!selectedPatient) { - toast.showInfo('Select a patient before booking.'); + toast.showInfo(t('infoSelectPatient')); return; } setBookingStartMinute(startMinute); @@ -179,7 +187,7 @@ export default function AppointmentsPage() { function handleAppointmentClick(appointment: AppointmentRecord) { if (isViewingPastDay) { - toast.showInfo('Past appointments are view-only.'); + toast.showInfo(t('infoPastViewOnly')); return; } const provider = providers.find((p) => p.userId === appointment.providerUserId); @@ -192,9 +200,7 @@ export default function AppointmentsPage() { } function handleAppointmentOutsideHours(appointment: AppointmentRecord) { - toast.showError( - 'This appointment falls outside the provider’s current working hours and cannot be edited.', - ); + toast.showError(t('errorOutsideHours')); } async function handleSaveAppointment(payload: { @@ -214,15 +220,15 @@ export default function AppointmentsPage() { } setBookingOpen(false); setEditingAppointmentId(null); - toast.showSuccess(activeEditingAppointment ? 'Appointment updated.' : 'Appointment saved.'); + toast.showSuccess(activeEditingAppointment ? t('successUpdated') : t('successSaved')); await loadSchedule(); } catch (err: unknown) { const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : activeEditingAppointment - ? 'Could not update appointment.' - : 'Could not save appointment.'; + ? t('errorUpdate') + : t('errorSave'); toast.showError(message); } finally { setSavingAppointment(false); @@ -233,7 +239,7 @@ export default function AppointmentsPage() { if (!activeEditingAppointment) { return; } - if (!window.confirm('Remove this appointment?')) { + if (!window.confirm(t('confirmRemove'))) { return; } setDeletingAppointment(true); @@ -242,13 +248,13 @@ export default function AppointmentsPage() { await appointmentsApi.remove(activeEditingAppointment.id); setBookingOpen(false); setEditingAppointmentId(null); - toast.showSuccess('Appointment removed.'); + toast.showSuccess(t('successRemoved')); await loadSchedule(); } catch (err: unknown) { const message = err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) - : 'Could not delete appointment.'; + : t('errorDelete'); toast.showError(message); } finally { setDeletingAppointment(false); @@ -258,10 +264,8 @@ export default function AppointmentsPage() { return (
- Search a patient, pick a date, then click a time slot under a provider to book. -
+{t('subtitle')}
Loading schedule…
+{t('loadingSchedule')}
)}Loading organization...
; + return{t('loadingOrganization')}
; } return ( @@ -284,13 +292,10 @@ export default function OrganizationsPage() {- Search organizations, send connection requests to existing accounts, or invitation - links when they are not on DyoLink yet. -
+{t('subtitle')}
- No organization found in directory search. + {t('noDirectoryResults')}
- Profile and security settings for your login. -
+{t('accountSubtitle')}
- Password change and profile editing will be wired here next (e.g. invite - flow, reset password). -
+{t('accountPlaceholder')}
Loading...
+{tCommon('loadingEllipsis')}
); } if (!currentOrganization.isOwner) { return ( -Redirecting...
+{tCommon('redirecting')}
); } @@ -75,55 +78,51 @@ export default function SubscriptionsSettingsPage() { href="/today" className="text-sm text-primary hover:opacity-90" > - ← Back to app + {tCommon('backToApp')} -- Your DyoLink workspace plan and seats for{' '} - {currentOrganization.name}. - Clinic and lab income tracking stays under the sidebar{' '} - Billing tab. + {t('subscriptionsSubtitle', { orgName: currentOrganization.name })}
- This organization has no active subscription. Select a plan below to start - the purchase process. -
+{t('noSubscriptionNotice')}
Current plan
+{t('currentPlan')}
{plan?.name ?? '—'}
Plan price
+{t('planPrice')}
{typeof plan?.price === 'number' ? `$${plan.price}` : '—'}
Seats used
+{t('seatsUsed')}
{typeof seatsUsed === 'number' ? seatsUsed : '—'} - {typeof maxUsers === 'number' ? ` / ${isUnlimited ? 'Unlimited' : maxUsers}` : ''} + {typeof maxUsers === 'number' + ? ` / ${isUnlimited ? t('unlimited') : maxUsers}` + : ''}
Seats remaining
+{t('seatsRemaining')}
- {isUnlimited ? 'Unlimited' : seatsRemaining ?? '—'} + {isUnlimited ? t('unlimited') : seatsRemaining ?? '—'}
Days remaining
+{t('daysRemaining')}
{daysUntilPlanEnd ?? '—'}
@@ -133,27 +132,24 @@ export default function SubscriptionsSettingsPage() { {alert?.showWarning && (No active subscription for this organization.
+{t('noActiveSubscription')}
)} {alert.trialExpired && ( -Trial period has ended. Choose a plan when checkout is available.
+{t('trialEnded')}
)} {!alert.trialExpired && alert.trialEndingSoon && (- Trial ends in {alert.daysUntilTrialEnd ?? '—'} day(s). + {t('trialEndsIn', { days: alert.daysUntilTrialEnd ?? '—' })}
)} {!alert.trialExpired && !alert.trialEndingSoon && alert.seatsLow && ( -Seat usage is high for this organization.
+{t('seatsLow')}
)}- Choose a plan to continue. Purchase integration is not active yet, so this - currently prepares the selection step only. -
+{t('choosePlanIntro')}
{option.name}
+{t(option.nameKey)}
- {option.maxUsers == null ? 'Unlimited seats' : `${option.maxUsers} seats`} + {option.maxUsers == null + ? t('unlimitedSeats') + : t('seatsCount', { n: option.maxUsers })} +
++ {t('pricePerMonth', { price: option.price })}
-${option.price} / month
); })} @@ -181,13 +181,13 @@ export default function SubscriptionsSettingsPage() { type="button" variant="primary" onClick={() => { - const selectedPlanLabel = selectedPlan?.name ?? 'the selected plan'; - setPurchaseNotice( - `Purchase flow will be enabled soon. ${selectedPlanLabel} is selected and ready for checkout setup.`, - ); + const selectedPlanLabel = selectedPlan + ? t(selectedPlan.nameKey) + : t('planSolo'); + setPurchaseNotice(t('purchaseNotice', { plan: selectedPlanLabel })); }} > - Start purchase process + {t('startPurchase')}Redirecting…
+{t('redirecting')}
); } @@ -490,10 +498,8 @@ export default function StaffPage() {- Invite teammates, set tab access, and stay within your plan seat limit. -
+{t('subtitle')}
- Seats:{' '} + {t('seatsLabel')}{' '} {seats.used} - {seats.unlimited ? ' (unlimited plan)' : ` / ${seats.limit}`} + {seats.unlimited ? ` ${t('unlimitedPlan')}` : ` / ${seats.limit}`} {!seats.unlimited && atSeatLimit && ( - {hasActivePlan - ? 'Plan seat limit reached for this organization.' - : 'No active plan selected for this organization. Choose a subscription plan to invite members.'} + {hasActivePlan ? t('seatLimitReached') : t('noActivePlan')} )}
@@ -535,7 +539,7 @@ export default function StaffPage() {- {lastInviteInfo.name} ({lastInviteInfo.email}) was invited. + {t('successInvited', { name: lastInviteInfo.name, email: lastInviteInfo.email })} {lastInviteInfo.invitationStatus === 'PENDING' - ? ' Invitation is pending until they open the link, set a password, and log in.' - : ' Invitation was accepted immediately.'} + ? ` ${t('invitedPending')}` + : ` ${t('invitedAccepted')}`}
{lastInviteInfo.invitationStatus === 'PENDING' && (- Invite link + {t('inviteLinkHeading')}
{lastInviteInfo.invitationUrl && (
@@ -591,36 +595,36 @@ export default function StaffPage() {
setCopiedInviteMembershipId(lastInviteInfo.membershipId);
setTimeout(() => setCopiedInviteMembershipId(null), 1500);
} catch (e) {
- toast.showError(formatApiErrorMessage(e, 'Could not copy invitation link.'));
+ toast.showError(formatApiErrorMessage(e, t('errorCopyInvite')));
} finally {
setCopyingInviteMembershipId(null);
}
})();
}}
>
- {copiedInviteMembershipId === lastInviteInfo.membershipId ? 'Copied' : 'Copy link'}
+ {copiedInviteMembershipId === lastInviteInfo.membershipId
+ ? tCommon('copied')
+ : tCommon('copyLink')}
-
- Share this link manually via SMS or email. A new link is generated if the previous one expired or was lost.
-
+ {t('shareLinkHint')}
Loading team…
+{t('loadingTeam')}
) : (| Name | -Role | -Status | -Access | +{t('tableName')} | +{t('tableEmail')} | +{t('tableRole')} | +{t('tableStatus')} | +{t('tableAccess')} | - Action + {t('tableAction')} | } @@ -632,28 +636,28 @@ export default function StaffPage() {{m.email} | {m.isOwner ? ( - Owner + {t('roleOwner')} ) : ( - Staff + {t('roleStaff')} )} |
{m.isOwner || m.invitationStatus === 'ACTIVE' ? (
- |
{m.isOwner ? ( - All features + {t('allFeatures')} ) : ( - {formatAccessSummary(m.permissions, currentOrganization?.type)} + {formatAccessSummary(m.permissions, currentOrganization?.type, tFeatures)} )} | @@ -666,8 +670,8 @@ export default function StaffPage() { className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50" disabled={copyingInviteMembershipId === m.id} onClick={() => void copyStaffInviteLink(m)} - aria-label="Copy invitation link" - title="Copy invitation link (generates a new link if needed)" + aria-label={t('copyInviteLink')} + title={t('copyInviteLinkTitle')} > {copiedInviteMembershipId === m.id ? (
|---|
| - Organization + {t('tableOrganization')} | - Owner email + {t('tableOwnerEmail')} | - Date + {t('tableDate')} | - Status + {t('tableStatus')} | - Invitation link + {t('tableInvitationLink')} | } diff --git a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx index bfe1140..4d1f225 100644 --- a/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx +++ b/frontend/src/components/ui/organizations/OrganizationSelectorContent.tsx @@ -1,6 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; +import { useTranslations } from 'next-intl'; import { useAuth } from '@/lib/hooks/useAuth'; import { canCreateOrganizationFromCurrentOrg } from '@/components/shared/permissions'; import { Building2, Beaker, Mail } from 'lucide-react'; @@ -8,6 +9,9 @@ import { Input } from '@/components/ui/shared/Input'; import { Button } from '@/components/ui/shared/Button'; export function OrganizationSelectorContent() { + const t = useTranslations('organizations'); + const tAuth = useTranslations('auth'); + const tCommon = useTranslations('common'); const { organizations, currentOrganization, @@ -29,6 +33,9 @@ export function OrganizationSelectorContent() { const getIcon = (type: string) => type === 'CLINIC' ?
|---|