mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 | ''>('');
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user