From 31c52c02e072272a3952cafa87a864badaa6cc4e Mon Sep 17 00:00:00 2001 From: estifanos Date: Wed, 12 Aug 2026 11:31:20 +0000 Subject: [PATCH] feat: introduce useLocalized hook for bilingual value retrieval.(localization round 2) - Added useLocalized hook to provide a stable function for retrieving bilingual values based on the current language. - Updated various components across the portal and backoffice to utilize the new useLocalized hook for consistent bilingual label rendering. - Refactored localized function in licensing.helpers to handle empty Amharic strings correctly. - Enhanced localization handling in LicenseApplicationPage, ProfilePage, and other components to ensure proper language switching. --- .../pages/CertificateDesignerPage.tsx | 4 ++- .../pages/LicenseRegisterPage.tsx | 3 +- .../components/DocumentsTab.tsx | 4 ++- .../src/app/features/license-review/export.ts | 4 +-- .../pages/LicenseReviewPage.tsx | 33 +++++++++++++++---- .../location/components/LocationTree.tsx | 4 ++- .../pages/PaymentConfigPage.tsx | 4 ++- .../app/features/result/pages/ResultPage.tsx | 5 +-- .../certificates/pages/CertificatesPage.tsx | 6 ++-- .../dashboard/pages/DashboardPage.tsx | 3 +- .../endorsement/pages/EndorsementPage.tsx | 3 +- .../app/features/exams/pages/ExamsPage.tsx | 12 ++++--- .../components/ConfigDrivenSection.tsx | 12 ++++--- .../licensing/components/DocumentSlots.tsx | 3 +- .../licensing/components/LicenseCard.tsx | 3 +- .../licensing/components/LicenseCatalogue.tsx | 4 ++- .../licensing/components/StaffEvidence.tsx | 3 +- .../pages/LicenseApplicationPage.tsx | 18 ++++++---- .../location/components/LocationPicker.tsx | 28 ++++++++-------- .../notifications/pages/NotificationsPage.tsx | 3 +- .../components/OperationsFormContent.tsx | 3 +- .../features/profile/pages/ProfilePage.tsx | 11 ++++--- .../app/features/waiver/pages/WaiverPage.tsx | 3 +- libs/api/src/lib/features/licensing/index.ts | 1 + .../features/licensing/licensing.helpers.ts | 14 +++++--- .../lib/features/licensing/use-localized.ts | 27 +++++++++++++++ 26 files changed, 154 insertions(+), 64 deletions(-) create mode 100644 libs/api/src/lib/features/licensing/use-localized.ts diff --git a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx index d98ef0ed0..9f47684df 100644 --- a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx +++ b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx @@ -39,6 +39,7 @@ import { useGetLicenseTemplatesQuery, useGetLicenseTypesQuery, useGetTemplateVariablesQuery, + useLocalized, usePublishLicenseTemplateMutation, useUpdateLicenseValidityMutation, useUpdateLicenseTemplateMutation, @@ -70,6 +71,7 @@ const STATUS_COLOR: Record = { */ export function CertificateDesignerPage() { const { t } = useTranslation(); + const localized = useLocalized(); const { can } = usePermissions(); const canEdit = can([PERMISSIONS.UPDATE_TEMPLATE]); @@ -226,7 +228,7 @@ export function CertificateDesignerPage() { label={t('designer.licenceType', 'Licence type')} data={(licenseTypes?.items ?? []).map((type) => ({ value: type.id, - label: type.name?.en ?? type.key, + label: localized(type.name) || type.key, }))} value={typeId} onChange={(value) => { diff --git a/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage.tsx b/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage.tsx index 84f1ede1a..5dc41726b 100644 --- a/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage.tsx +++ b/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage.tsx @@ -22,7 +22,7 @@ import { notify } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; import { extractErrorMessage, - localized, + useLocalized, useGetLicensesQuery, useReinstateLicenseMutation, useRevokeLicenseMutation, @@ -168,6 +168,7 @@ export function LicenseRegisterPage() { const items = data?.items ?? []; const showDate = useDateDisplayer(); + const localized = useLocalized(); return ( diff --git a/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx b/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx index ea69922ac..ea6880719 100644 --- a/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx +++ b/apps/backoffice/src/app/features/license-review/components/DocumentsTab.tsx @@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next'; import { useClearDocumentReviewMutation, useGetDocumentReviewsQuery, + useLocalized, useReviewDocumentMutation, type Attachment, type DocumentRequirement, @@ -62,6 +63,7 @@ export function DocumentsTab({ onFlagRemark, }: DocumentsTabProps) { const { t } = useTranslation(); + const localized = useLocalized(); const [preview, setPreview] = useState(null); const [rejecting, setRejecting] = useState>({}); @@ -148,7 +150,7 @@ export function DocumentsTab({ } variant="light"> {t('review.documents.missing', 'Not yet uploaded')}:{' '} - {missing.map((r) => r.name.en ?? r.key).join(', ')} + {missing.map((r) => localized(r.name) || r.key).join(', ')} )} diff --git a/apps/backoffice/src/app/features/license-review/export.ts b/apps/backoffice/src/app/features/license-review/export.ts index e38e92105..6bdc0f0c9 100644 --- a/apps/backoffice/src/app/features/license-review/export.ts +++ b/apps/backoffice/src/app/features/license-review/export.ts @@ -1,4 +1,4 @@ -import { STATUS_LABELS, type LicenseApplication } from '@ema-platform/api'; +import { STATUS_LABELS, localized, type LicenseApplication } from '@ema-platform/api'; import { dateDisplayer } from '@ema-platform/shared'; import { computeSla } from './sla'; @@ -22,7 +22,7 @@ const COLUMNS: Array<{ { header: 'Company', value: (a) => a.companyName }, { header: 'Trade name', value: (a) => a.tradeName }, { header: 'TIN', value: (a) => a.tinNumber }, - { header: 'Licence type', value: (a) => a.licenseType?.name?.en ?? a.licenseTypeId }, + { header: 'Licence type', value: (a, locale) => localized(a.licenseType?.name, locale) || a.licenseTypeId }, { header: 'Status', value: (a) => STATUS_LABELS[a.status] }, { header: 'Kind', value: (a) => a.kind }, { header: 'Assigned officer', value: (a) => a.assignedOfficerId }, diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage.tsx index 43c23f731..fbe5188f4 100644 --- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage.tsx +++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage.tsx @@ -39,6 +39,7 @@ import { STATUS_COLORS, STATUS_LABELS, extractErrorMessage, + useLocalized, useApproveDocumentsMutation, useAssignApplicationMutation, useCompleteReviewMutation, @@ -111,6 +112,7 @@ function buildChecklist( export function LicenseReviewPage() { const { t, i18n } = useTranslation(); const showDate = useDateDisplayer(); + const localized = useLocalized(); const { id = '' } = useParams(); const { can } = usePermissions(); const currentUserId = useAppSelector((state) => state.auth.user?.id) ?? ''; @@ -485,6 +487,12 @@ export function LicenseReviewPage() { const sections = presentation.detailSections; const formSections = Object.entries(app.formData ?? {}); + // The bilingual section/field labels the applicant's wizard renders — this + // page already fetches them (`requirements` above) but used to fall back to + // the raw formData keys, so an officer saw `vesselId` instead of a label in + // either language. + const configSections = requirements?.licenseType.formSchema.sections ?? []; + const sectionsByKey = new Map(configSections.map((s) => [s.key, s])); return ( @@ -533,7 +541,7 @@ export function LicenseReviewPage() { {t('review.summary', 'Summary')} - + - {formSections.map(([sectionKey, values]) => ( + {formSections.map(([sectionKey, values]) => { + const sectionConfig = sectionsByKey.get(sectionKey); + const fieldsByKey = new Map( + (sectionConfig?.fields ?? []).map((f) => [f.key, f]), + ); + return ( - {sectionKey.replace(/([A-Z])/g, ' $1')} + {sectionConfig + ? localized(sectionConfig.title) + : sectionKey.replace(/([A-Z])/g, ' $1')} - {Object.entries(values ?? {}).map(([k, v]) => ( + {Object.entries(values ?? {}).map(([k, v]) => { + const fieldConfig = fieldsByKey.get(k); + return ( - {k} + {fieldConfig ? localized(fieldConfig.label) : k} {v === null ? '—' : String(v)} - ))} + ); + })}
{flags[sectionKey] && ( @@ -702,7 +720,8 @@ export function LicenseReviewPage() { /> )}
- ))} + ); + })}
diff --git a/apps/backoffice/src/app/features/location/components/LocationTree.tsx b/apps/backoffice/src/app/features/location/components/LocationTree.tsx index dda3d9574..311c7622e 100644 --- a/apps/backoffice/src/app/features/location/components/LocationTree.tsx +++ b/apps/backoffice/src/app/features/location/components/LocationTree.tsx @@ -22,6 +22,7 @@ import { import { useGetLocationsQuery } from '../api/location-api'; import type { Location } from '../types/location'; import { useTranslation } from 'react-i18next'; +import { useLocalized } from '@ema-platform/api'; interface LocationTreeProps { selectedId: string | null; @@ -51,6 +52,7 @@ function TreeNode({ onSelect: (location: Location) => void; depth: number; }) { + const localized = useLocalized(); const [opened, setOpened] = useState(depth < 1); const isSelected = selectedId === location.id; const hasChildren = @@ -134,7 +136,7 @@ function TreeNode({ style={{ flexShrink: 0, opacity: 0.6 }} /> - {location.names.en} + {localized(location.names)} {hasChildren && ( diff --git a/apps/backoffice/src/app/features/payment-config/pages/PaymentConfigPage.tsx b/apps/backoffice/src/app/features/payment-config/pages/PaymentConfigPage.tsx index a393a3032..7c10da105 100644 --- a/apps/backoffice/src/app/features/payment-config/pages/PaymentConfigPage.tsx +++ b/apps/backoffice/src/app/features/payment-config/pages/PaymentConfigPage.tsx @@ -34,7 +34,7 @@ import { } from '@ema-platform/ui'; import { extractErrorMessage, - localized, + useLocalized, useGetLicenseTypesQuery, useGetPaymentCapabilitiesQuery, useUpdateLicenseFeesMutation, @@ -61,6 +61,7 @@ function feeText(amount: string | number | null, currency: string): string { export function PaymentConfigPage() { const { t } = useTranslation(); + const localized = useLocalized(); const { data, isLoading, isFetching, error, refetch } = useGetLicenseTypesQuery(); const { data: capabilities } = useGetPaymentCapabilitiesQuery(); @@ -292,6 +293,7 @@ function FeeEditModal({ onClose: () => void; }) { const { t } = useTranslation(); + const localized = useLocalized(); const [updateFees, { isLoading }] = useUpdateLicenseFeesMutation(); const [newFee, setNewFee] = useState(''); const [renewalFee, setRenewalFee] = useState(''); diff --git a/apps/backoffice/src/app/features/result/pages/ResultPage.tsx b/apps/backoffice/src/app/features/result/pages/ResultPage.tsx index 79c538844..ffd8b6b94 100644 --- a/apps/backoffice/src/app/features/result/pages/ResultPage.tsx +++ b/apps/backoffice/src/app/features/result/pages/ResultPage.tsx @@ -40,7 +40,7 @@ import { import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, ModalFooter, type AdvancedColumn } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; import type { BilingualValue } from '@ema-platform/ui'; -import { extractErrorMessage } from '@ema-platform/api'; +import { extractErrorMessage, useLocalized } from '@ema-platform/api'; import { useGetResultsQuery, useLazyGetResultQuery, @@ -117,6 +117,7 @@ function InfoRow({ label, value }: { label: string; value: string }) { export function ResultPage() { const { t, i18n } = useTranslation(); + const localized = useLocalized(); const showDate = useDateDisplayer(); const locale = i18n.language as 'en' | 'am'; const { handleError } = useErrorHandler(); @@ -152,7 +153,7 @@ export function ResultPage() { const [qcRemark, setQcRemark] = useState(''); const [qcAdjustment, setQcAdjustment] = useState(0); - const examOptions = exams.map((e) => ({ value: e.id, label: `${e.title.en} (${e.date})` })); + const examOptions = exams.map((e) => ({ value: e.id, label: `${localized(e.title)} (${e.date})` })); const startRecord = () => { const ex = exams.find((e) => e.id === pickerExamId); diff --git a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx index 37f9cf6da..5883717ac 100644 --- a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx @@ -25,6 +25,7 @@ import { STATUS_LABELS, TERMINAL_STATUSES, extractErrorMessage, + useLocalized, useGetCertificateUrlMutation, useGetMyApplicationsQuery, useGetMyLicensesQuery, @@ -74,6 +75,7 @@ export function CertificatesPage() { const { data: licenses } = useGetMyLicensesQuery(); const [getCertificateUrl] = useGetCertificateUrlMutation(); const showDate = useDateDisplayer(); + const localized = useLocalized(); const registered = Boolean(profile?.seafarerNumber) && profile?.seafarerStatus === 'ACTIVE'; @@ -189,7 +191,7 @@ export function CertificatesPage() {
{app.applicationNumber} - {app.licenseType?.name?.en} + {localized(app.licenseType?.name)}
@@ -245,7 +247,7 @@ export function CertificatesPage() { {license.certificateNumber} - {license.licenseType?.name?.en} + {localized(license.licenseType?.name)} {showDate(license.issueDate)} {showDate(license.expiryDate)} diff --git a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx index 3738afc99..2ed1fa9c9 100644 --- a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx +++ b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx @@ -37,7 +37,7 @@ import { STATUS_LABELS, STATUS_PROGRESS, TERMINAL_STATUSES, - localized, + useLocalized, useGetCertificateUrlMutation, useGetMyApplicationsQuery, useGetMyLicensesQuery, @@ -457,6 +457,7 @@ function ApplicationTable({ applications: LicenseApplication[]; navigate: (path: string) => void; }) { + const localized = useLocalized(); return ( diff --git a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx index e0498e511..ac7ae28d1 100644 --- a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx +++ b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx @@ -25,7 +25,7 @@ import { STATUS_LABELS, TERMINAL_STATUSES, extractErrorMessage, - localized, + useLocalized, useGetCertificateUrlMutation, useGetMyApplicationsQuery, useGetMyLicensesQuery, @@ -68,6 +68,7 @@ export function EndorsementPage() { useGetMyApplicationsQuery(); const { data: licenses } = useGetMyLicensesQuery(); const showDate = useDateDisplayer(); + const localized = useLocalized(); const [getCertificateUrl] = useGetCertificateUrlMutation(); const registered = diff --git a/apps/portal/src/app/features/exams/pages/ExamsPage.tsx b/apps/portal/src/app/features/exams/pages/ExamsPage.tsx index 391e06847..f49865d3b 100644 --- a/apps/portal/src/app/features/exams/pages/ExamsPage.tsx +++ b/apps/portal/src/app/features/exams/pages/ExamsPage.tsx @@ -20,6 +20,7 @@ import { useApiMutation, extractErrorMessage, openAuthedDocument, + useLocalized, } from '@ema-platform/api'; interface OpenExam { @@ -83,6 +84,7 @@ const ATTENDANCE_COLOR: Record = { */ export function ExamsPage() { const showDate = useDateDisplayer(); + const localized = useLocalized(); const [appealFor, setAppealFor] = useState(null); const [appealReason, setAppealReason] = useState(''); @@ -197,9 +199,9 @@ export function ExamsPage() {
- {exam.title?.en} + {localized(exam.title)} - {exam.certification?.name?.en ?? ''} ·{' '} + {localized(exam.certification?.name)} ·{' '} {showDate(exam.date)} {exam.venue ? ` · ${exam.venue}` : ''} @@ -254,7 +256,7 @@ export function ExamsPage() { {registration.admissionNumber} - {registration.exam?.title?.en ?? '—'} + {localized(registration.exam?.title) || '—'} {showDate(registration.exam?.date)} {registration.exam?.venue ?? '—'} @@ -326,7 +328,7 @@ export function ExamsPage() { ); return ( - {result.exam?.title?.en ?? '—'} + {localized(result.exam?.title) || '—'} {showDate(result.publishedAt)} @@ -376,7 +378,7 @@ export function ExamsPage() { Explain what you believe went wrong with the marking or the - administration of {appealFor?.exam?.title?.en ?? 'this examination'}. + administration of {localized(appealFor?.exam?.title) || 'this examination'}. Appeals must be lodged within 14 days of publication.