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.
This commit is contained in:
estifanos
2026-08-12 11:31:20 +00:00
parent faf58850f0
commit 31c52c02e0
26 changed files with 154 additions and 64 deletions

View File

@@ -39,6 +39,7 @@ import {
useGetLicenseTemplatesQuery,
useGetLicenseTypesQuery,
useGetTemplateVariablesQuery,
useLocalized,
usePublishLicenseTemplateMutation,
useUpdateLicenseValidityMutation,
useUpdateLicenseTemplateMutation,
@@ -70,6 +71,7 @@ const STATUS_COLOR: Record<LicenseTemplate['status'], string> = {
*/
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) => {

View File

@@ -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 (
<Container size="xl" py="md">

View File

@@ -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<Attachment | null>(null);
const [rejecting, setRejecting] = useState<Record<string, string>>({});
@@ -148,7 +150,7 @@ export function DocumentsTab({
<Alert mt="sm" color="orange" icon={<IconAlertCircle size={16} />} variant="light">
<Text size="sm">
{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(', ')}
</Text>
</Alert>
)}

View File

@@ -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 },

View File

@@ -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 (
<Container size="xl" py="md">
@@ -533,7 +541,7 @@ export function LicenseReviewPage() {
{t('review.summary', 'Summary')}
</Text>
<Stack gap={6}>
<SummaryRow label={t('review.type', 'Type')} value={app.licenseType?.name?.en} />
<SummaryRow label={t('review.type', 'Type')} value={localized(app.licenseType?.name)} />
<SummaryRow label={t('review.tin', 'TIN')} value={app.tinNumber} />
<SummaryRow label={t('review.kind', 'Kind')} value={app.kind} />
<SummaryRow
@@ -639,11 +647,18 @@ export function LicenseReviewPage() {
<Tabs.Panel value="overview">
<Stack>
{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 (
<Card withBorder key={sectionKey} padding="md">
<Group justify="space-between" mb="xs">
<Text fw={600} size="sm" tt="capitalize">
{sectionKey.replace(/([A-Z])/g, ' $1')}
{sectionConfig
? localized(sectionConfig.title)
: sectionKey.replace(/([A-Z])/g, ' $1')}
</Text>
<Checkbox
size="xs"
@@ -654,18 +669,21 @@ export function LicenseReviewPage() {
</Group>
<Table withTableBorder>
<Table.Tbody>
{Object.entries(values ?? {}).map(([k, v]) => (
{Object.entries(values ?? {}).map(([k, v]) => {
const fieldConfig = fieldsByKey.get(k);
return (
<Table.Tr key={k}>
<Table.Td w="40%">
<Text size="xs" c="dimmed">
{k}
{fieldConfig ? localized(fieldConfig.label) : k}
</Text>
</Table.Td>
<Table.Td>
<Text size="sm">{v === null ? '—' : String(v)}</Text>
</Table.Td>
</Table.Tr>
))}
);
})}
</Table.Tbody>
</Table>
{flags[sectionKey] && (
@@ -702,7 +720,8 @@ export function LicenseReviewPage() {
/>
)}
</Card>
))}
);
})}
</Stack>
</Tabs.Panel>

View File

@@ -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 }}
/>
<Text size="sm" truncate style={{ flex: 1 }}>
{location.names.en}
{localized(location.names)}
</Text>
</UnstyledButton>
{hasChildren && (

View File

@@ -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<number | ''>('');
const [renewalFee, setRenewalFee] = useState<number | ''>('');

View File

@@ -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);