feature: localization's first implmentation done. all frontend hardcoded text is now localized.

This commit is contained in:
2026-06-20 14:51:43 +03:30
parent 284fbd08aa
commit b2f4dfa4ca
52 changed files with 3306 additions and 1041 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
import { AppointmentsStrip } from '@/components/ui/treatment/AppointmentsStrip';
import { FdiToothChart } from '@/components/ui/treatment/FdiToothChart';
import { PastTreatmentsPanel } from '@/components/ui/treatment/PastTreatmentsPanel';
@@ -34,6 +35,14 @@ import type {
TreatmentCaseDraft,
} from '@/types/treatment';
const TREATMENT_TYPE_KEYS = {
consultation: 'typeConsultation',
filling: 'typeFilling',
endo: 'typeEndo',
visit: 'typeVisit',
hygiene: 'typeHygiene',
} as const;
function newCase(): TreatmentCaseDraft {
return {
clientId:
@@ -121,6 +130,7 @@ interface TreatmentWorkspaceProps {
}
export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWorkspaceProps) {
const t = useTranslations('treatment');
const { showError, showSuccess, messages: toastMessages } = useToast();
const canView = canViewTreatment(currentOrganization);
const canEdit = canEditTreatment(currentOrganization);
@@ -185,12 +195,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
const currentDraftPreview = useMemo<PastTreatment | null>(() => {
if (!selectedAppointment) return null;
return casesToPreviewTreatment(cases, {
title: `Draft · ${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`,
title: t('draftTitle', {
patientName: `${selectedAppointment.patientFirstName} ${selectedAppointment.patientLastName}`,
}),
patientId: selectedAppointment.patientId,
treatmentAt: new Date().toISOString(),
status: 'draft',
});
}, [cases, selectedAppointment]);
}, [cases, selectedAppointment, t]);
useEffect(() => {
setSelectionLocked(false);
@@ -217,7 +229,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
}
} catch (error: unknown) {
if (!cancelled) {
showError(formatApiErrorMessage(error, 'Failed to load appointments.'));
showError(formatApiErrorMessage(error, t('errorLoadAppointments')));
}
} finally {
if (!cancelled) setApptsLoading(false);
@@ -226,7 +238,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
return () => {
cancelled = true;
};
}, [userId, selectedDay, showError]);
}, [userId, selectedDay, showError, t]);
useEffect(() => {
const today = startOfLocalDay(new Date());
@@ -250,14 +262,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
if (!cancelled) setOrgs(list.data);
} catch (error: unknown) {
if (!cancelled) {
showError(formatApiErrorMessage(error, 'Failed to load linked organizations.'));
showError(formatApiErrorMessage(error, t('errorLoadOrgs')));
}
}
})();
return () => {
cancelled = true;
};
}, [showError]);
}, [showError, t]);
useEffect(() => {
if (!selectedAppointment) {
@@ -272,7 +284,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
if (!cancelled) setHistory(response.data);
} catch (error: unknown) {
if (!cancelled) {
showError(formatApiErrorMessage(error, 'Failed to load treatment history.'));
showError(formatApiErrorMessage(error, t('errorLoadHistory')));
}
} finally {
if (!cancelled) setHistoryLoading(false);
@@ -281,7 +293,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
return () => {
cancelled = true;
};
}, [selectedAppointment?.patientId, showError]);
}, [selectedAppointment?.patientId, showError, t]);
useEffect(() => {
const appointmentId = selectedAppointment?.id;
@@ -310,19 +322,19 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
setOrganizationSearch('');
} catch (error: unknown) {
if (!cancelled) {
showError(formatApiErrorMessage(error, 'Failed to load treatment draft.'));
showError(formatApiErrorMessage(error, t('errorLoadDraft')));
}
}
})();
return () => {
cancelled = true;
};
}, [selectedAppointment?.id, showError]);
}, [selectedAppointment?.id, showError, t]);
const confirmDiscardIfDirty = useCallback(() => {
if (!isDirty) return true;
return window.confirm('You have unsaved changes. Discard them and continue?');
}, [isDirty]);
return window.confirm(t('confirmDiscard'));
}, [isDirty, t]);
const onPickAppointment = useCallback(
(id: string) => {
@@ -361,16 +373,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
: c,
),
);
showSuccess(
`${uploaded.data.length} file${uploaded.data.length === 1 ? '' : 's'} uploaded successfully.`,
);
showSuccess(t('successFilesUploaded', { count: uploaded.data.length }));
} catch (error: unknown) {
showError(formatApiErrorMessage(error, 'Failed to upload attachments.'));
showError(formatApiErrorMessage(error, t('errorUpload')));
} finally {
setUploadBusyCaseId(null);
}
},
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError],
[canEditTreatmentForDay, selectedAppointment, showSuccess, showError, t],
);
const persistDraft = useCallback(async () => {
@@ -401,13 +411,13 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
setSaveBusy(true);
try {
await persistDraft();
showSuccess('Treatment draft saved.');
showSuccess(t('successDraftSaved'));
} catch (error: unknown) {
showError(formatApiErrorMessage(error, 'Failed to save treatment draft.'));
showError(formatApiErrorMessage(error, t('errorSaveDraft')));
} finally {
setSaveBusy(false);
}
}, [canEditTreatmentForDay, selectedAppointment, persistDraft, showSuccess, showError]);
}, [canEditTreatmentForDay, selectedAppointment, persistDraft, showSuccess, showError, t]);
const handleSendCase = useCallback(
async (treatmentCase: TreatmentCaseDraft) => {
@@ -416,14 +426,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
orgs.some((o) => o.id === id && o.active),
);
if (targets.length === 0) {
showError('Choose at least one active organization to send this case.');
showError(t('errorChooseOrg'));
return;
}
setSendBusyId(treatmentCase.clientId);
try {
const saved = await persistDraft();
const serverCase = saved.cases.find((c) => c.clientId === treatmentCase.clientId);
if (!serverCase?.id) throw new Error('Case must be saved before sending.');
if (!serverCase?.id) throw new Error(t('errorCaseMustSave'));
const response = await treatmentsApi.sendCase(serverCase.id, { organizationIds: targets });
setCases((prev) => {
@@ -445,14 +455,14 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
const next = [...targets.filter((id) => !prev.includes(id)), ...prev];
return next.slice(0, 10);
});
showSuccess('Case sent to selected organizations.');
showSuccess(t('successCaseSent'));
} catch (error: unknown) {
showError(formatApiErrorMessage(error, 'Failed to send case.'));
showError(formatApiErrorMessage(error, t('errorSendCase')));
} finally {
setSendBusyId(null);
}
},
[canEditTreatmentForDay, selectedAppointment, orgs, persistDraft, showSuccess, showError],
[canEditTreatmentForDay, selectedAppointment, orgs, persistDraft, showSuccess, showError, t],
);
const openPreview = useCallback(
@@ -489,9 +499,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
if (!canView) {
return (
<div className="surface-card p-6 max-w-xl">
<h2 className="text-lg font-semibold text-text-primary">Treatment workspace</h2>
<h2 className="text-lg font-semibold text-text-primary">{t('noPermissionTitle')}</h2>
<p className="text-sm text-text-secondary mt-2">
You do not have permission to view the Treatment tab for this organization.
{t('noPermissionBody')}
</p>
</div>
);
@@ -500,11 +510,9 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
return (
<div className="space-y-4">
<header className="space-y-1">
<h1 className="text-2xl font-semibold text-text-primary">Treatment</h1>
<h1 className="text-2xl font-semibold text-text-primary">{t('title')}</h1>
<p className="text-sm text-text-secondary">
{canEdit
? 'Document cases for your appointments, save drafts, and send work to linked organizations.'
: 'View-only access — you can review appointments and treatment history but cannot edit.'}
{canEdit ? t('subtitleEdit') : t('subtitleReadOnly')}
</p>
</header>
@@ -523,8 +531,7 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
{isViewingPastDay && (
<p className="text-sm text-text-secondary rounded-[var(--radius-md)] border border-border/60 bg-background-secondary/50 px-3 py-2">
Past days are view-only. You can review appointments and history, but treatment cases
cannot be added or changed.
{t('pastDayNotice')}
</p>
)}
@@ -532,18 +539,20 @@ export function TreatmentWorkspace({ userId, currentOrganization }: TreatmentWor
<div className="space-y-3 min-w-0 xl:max-w-[380px]">
{selectedAppointment ? (
<div className="surface-card p-3 space-y-0.5">
<p className="text-[10px] uppercase tracking-wide text-text-muted">Selected patient</p>
<p className="text-[10px] uppercase tracking-wide text-text-muted">{t('selectedPatient')}</p>
<p className="text-base font-semibold text-text-primary">
{selectedAppointment.patientFirstName} {selectedAppointment.patientLastName}
</p>
<p className="text-[11px] text-text-secondary">
Purpose:{' '}
<span className="capitalize text-text-primary">{selectedAppointment.purpose}</span>
{t('purposeLabel')}{' '}
<span className="capitalize text-text-primary">
{t(TREATMENT_TYPE_KEYS[selectedAppointment.purpose as keyof typeof TREATMENT_TYPE_KEYS] ?? selectedAppointment.purpose)}
</span>
</p>
</div>
) : (
<div className="surface-card p-3 text-sm text-text-muted">
{apptsLoading ? 'Loading appointments' : 'Select a day with at least one appointment.'}
{apptsLoading ? t('loadingAppointments') : t('selectDayWithAppointment')}
</div>
)}