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', showPassword: 'Show password', hidePassword: 'Hide password', 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.');