feat: add internationalization support for WaiverPage with translations for English and Amharic

This commit is contained in:
estifanos
2026-08-12 11:04:47 +00:00
parent 8f2dcb30f0
commit 7242f8425a
3 changed files with 107 additions and 29 deletions

View File

@@ -17,11 +17,13 @@ import {
IconInfoCircle,
} from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
STATUS_COLORS,
STATUS_LABELS,
TERMINAL_STATUSES,
extractErrorMessage,
localized,
useGetCertificateUrlMutation,
useGetMyApplicationsQuery,
useGetMyLicensesQuery,
@@ -40,6 +42,7 @@ const WAIVER_TYPE_KEYS = ['PRE_WAIVER', 'POST_WAIVER'];
* verifiable references (US-WAV-010).
*/
export function WaiverPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { data: applications, isLoading } = useGetMyApplicationsQuery();
const { data: licenses } = useGetMyLicensesQuery();
@@ -61,7 +64,7 @@ export function WaiverPage() {
const result = await getCertificateUrl(licenseId).unwrap();
window.open(result.url, '_blank', 'noopener');
} catch (error) {
notify.error(extractErrorMessage(error, 'Could not fetch the letter'));
notify.error(extractErrorMessage(error, t('waiver.fetchFailed', 'Could not fetch the letter')));
}
}
@@ -76,65 +79,72 @@ export function WaiverPage() {
return (
<Stack maw={900} mx="auto">
<div>
<Title order={2}>Maritime Waiver</Title>
<Title order={2}>{t('waiver.title', 'Maritime Waiver')}</Title>
<Text size="sm" c="dimmed">
Apply for a waiver when Ethiopian Shipping &amp; Logistics cannot
carry your shipment. Approval issues the bank waiver letter.
{t(
'waiver.subtitle',
'Apply for a waiver when Ethiopian Shipping & Logistics cannot carry your shipment. Approval issues the bank waiver letter.',
)}
</Text>
</div>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<Card withBorder radius="md" p="lg">
<Text fw={600}>Pre-waiver</Text>
<Text fw={600}>{t('waiver.preWaiver.title', 'Pre-waiver')}</Text>
<Text size="sm" c="dimmed" mt={4} mb="md">
The cargo has not yet arrived. Applying before arrival avoids the
post-waiver penalty.
{t(
'waiver.preWaiver.body',
'The cargo has not yet arrived. Applying before arrival avoids the post-waiver penalty.',
)}
</Text>
<Button
rightSection={<IconArrowRight size={16} />}
onClick={() => navigate('/licensing/PRE_WAIVER/apply')}
>
Apply for a pre-waiver
{t('waiver.preWaiver.apply', 'Apply for a pre-waiver')}
</Button>
</Card>
<Card withBorder radius="md" p="lg">
<Text fw={600}>Post-waiver</Text>
<Text fw={600}>{t('waiver.postWaiver.title', 'Post-waiver')}</Text>
<Text size="sm" c="dimmed" mt={4} mb="md">
The cargo has already arrived. Granted once per shipment, and only
against a settled penalty with the receipt attached.
{t(
'waiver.postWaiver.body',
'The cargo has already arrived. Granted once per shipment, and only against a settled penalty with the receipt attached.',
)}
</Text>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => navigate('/licensing/POST_WAIVER/apply')}
>
Apply for a post-waiver
{t('waiver.postWaiver.apply', 'Apply for a post-waiver')}
</Button>
</Card>
</SimpleGrid>
<Alert color="blue" icon={<IconInfoCircle size={16} />}>
Each waiver covers one shipment, identified by its bill of lading. A
second application quoting the same bill of lading is refused while the
first is live.
{t(
'waiver.oneShipmentNotice',
'Each waiver covers one shipment, identified by its bill of lading. A second application quoting the same bill of lading is refused while the first is live.',
)}
</Alert>
{inFlight.length > 0 && (
<Stack gap="xs">
<Title order={4}>Applications in progress</Title>
<Title order={4}>{t('waiver.inProgress', 'Applications in progress')}</Title>
{inFlight.map((app) => (
<Card key={app.id} withBorder radius="md" p="md">
<Group justify="space-between">
<div>
<Text fw={600}>{app.applicationNumber}</Text>
<Text size="xs" c="dimmed">
{app.licenseType?.name?.en}
{localized(app.licenseType?.name)}
</Text>
</div>
<Group>
<Badge color={STATUS_COLORS[app.status]}>
{STATUS_LABELS[app.status]}
{t(`applications.status.${app.status}`, STATUS_LABELS[app.status])}
</Badge>
<Button
size="compact-sm"
@@ -146,8 +156,8 @@ export function WaiverPage() {
}
>
{app.status === 'DRAFT' || app.status === 'RESUBMIT_REQUIRED'
? 'Continue'
: 'View'}
? t('applications.actions.continue', 'Continue')
: t('applications.actions.view', 'View')}
</Button>
</Group>
</Group>
@@ -157,11 +167,11 @@ export function WaiverPage() {
)}
<Stack gap="xs">
<Title order={4}>Issued waiver letters</Title>
<Title order={4}>{t('waiver.issuedLetters', 'Issued waiver letters')}</Title>
{letters.length === 0 ? (
<Card withBorder radius="md" p="lg">
<Text size="sm" c="dimmed" ta="center">
No waiver letters issued yet.
{t('waiver.emptyLetters', 'No waiver letters issued yet.')}
</Text>
</Card>
) : (
@@ -169,10 +179,10 @@ export function WaiverPage() {
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Reference</Table.Th>
<Table.Th>Kind</Table.Th>
<Table.Th>Issued</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>{t('waiver.columns.reference', 'Reference')}</Table.Th>
<Table.Th>{t('waiver.columns.kind', 'Kind')}</Table.Th>
<Table.Th>{t('waiver.columns.issued', 'Issued')}</Table.Th>
<Table.Th>{t('waiver.columns.status', 'Status')}</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
@@ -184,7 +194,7 @@ export function WaiverPage() {
{license.certificateNumber}
</Text>
</Table.Td>
<Table.Td>{license.licenseType?.name?.en ?? '—'}</Table.Td>
<Table.Td>{localized(license.licenseType?.name) || '—'}</Table.Td>
<Table.Td>{showDate(license.issueDate)}</Table.Td>
<Table.Td>
<Badge
@@ -192,7 +202,7 @@ export function WaiverPage() {
variant="light"
color={license.status === 'ACTIVE' ? 'green' : 'red'}
>
{license.status}
{t(`waiver.licenseStatus.${license.status}`, license.status)}
</Badge>
</Table.Td>
<Table.Td>
@@ -202,7 +212,7 @@ export function WaiverPage() {
leftSection={<IconFileText size={13} />}
onClick={() => download(license.id)}
>
Letter
{t('waiver.letter', 'Letter')}
</Button>
</Table.Td>
</Table.Tr>

View File

@@ -352,4 +352,38 @@ export const am: Translations = {
a4: 'በማመልከቻው ላይ "እርምጃ ያስፈልጋል" የሚል ማስታወሻ ያያሉ። ግምገማውን ለመቀጠል የተጠየቀውን ሰነድ ወይም ዝርዝር ያቅርቡ።',
},
},
waiver: {
title: "የባህር ትራንስፖርት ነፃ ፈቃድ",
subtitle: "የኢትዮጵያ መርከብ እና ሎጂስቲክስ ጭነትዎን ማጓጓዝ በማይችልበት ጊዜ ነፃ ፈቃድ ያመልክቱ። መጽደቁ የባንክ ነፃ ፈቃድ ደብዳቤ ያወጣል።",
preWaiver: {
title: "ቅድመ ነፃ ፈቃድ",
body: "ጭነቱ ገና አልደረሰም። ከመድረሱ በፊት ማመልከት ድህረ-ነፃ ፈቃድ ቅጣትን ያስቀራል።",
apply: "ለቅድመ ነፃ ፈቃድ ያመልክቱ",
},
postWaiver: {
title: "ድህረ ነፃ ፈቃድ",
body: "ጭነቱ አስቀድሞ ደርሷል። በአንድ ጭነት አንድ ጊዜ ብቻ ይሰጣል፣ እና ደረሰኙ ተያይዞ የተከፈለ ቅጣት ካለ ብቻ።",
apply: "ለድህረ ነፃ ፈቃድ ያመልክቱ",
},
oneShipmentNotice: "እያንዳንዱ ነፃ ፈቃድ በማጫኛ ሰነዱ የሚለይ አንድ ጭነት ይሸፍናል። ተመሳሳይ የማጫኛ ሰነድ ጠቅሶ የሚቀርብ ሁለተኛ ማመልከቻ የመጀመሪያው ገና ንቁ ሆኖ ሳለ ውድቅ ይደረጋል።",
inProgress: "በሂደት ላይ ያሉ ማመልከቻዎች",
issuedLetters: "የወጡ ነፃ ፈቃድ ደብዳቤዎች",
emptyLetters: "እስካሁን የወጣ ነፃ ፈቃድ ደብዳቤ የለም።",
columns: {
reference: "ማጣቀሻ",
kind: "ዓይነት",
issued: "የወጣበት ቀን",
status: "ሁኔታ",
},
letter: "ደብዳቤ",
fetchFailed: "ደብዳቤውን ማግኘት አልተቻለም",
licenseStatus: {
ACTIVE: "ንቁ",
EXPIRED: "ጊዜው ያለፈበት",
SUSPENDED: "ታግዷል",
CANCELLED: "ተሰርዟል",
SUPERSEDED: "ተተክቷል",
},
},
};

View File

@@ -351,6 +351,40 @@ export const en = {
a4: 'You will see an "Action required" notice on the application. Provide the requested document or detail to resume the review.',
},
},
waiver: {
title: 'Maritime Waiver',
subtitle: 'Apply for a waiver when Ethiopian Shipping & Logistics cannot carry your shipment. Approval issues the bank waiver letter.',
preWaiver: {
title: 'Pre-waiver',
body: 'The cargo has not yet arrived. Applying before arrival avoids the post-waiver penalty.',
apply: 'Apply for a pre-waiver',
},
postWaiver: {
title: 'Post-waiver',
body: 'The cargo has already arrived. Granted once per shipment, and only against a settled penalty with the receipt attached.',
apply: 'Apply for a post-waiver',
},
oneShipmentNotice: 'Each waiver covers one shipment, identified by its bill of lading. A second application quoting the same bill of lading is refused while the first is live.',
inProgress: 'Applications in progress',
issuedLetters: 'Issued waiver letters',
emptyLetters: 'No waiver letters issued yet.',
columns: {
reference: 'Reference',
kind: 'Kind',
issued: 'Issued',
status: 'Status',
},
letter: 'Letter',
fetchFailed: 'Could not fetch the letter',
licenseStatus: {
ACTIVE: 'Active',
EXPIRED: 'Expired',
SUSPENDED: 'Suspended',
CANCELLED: 'Cancelled',
SUPERSEDED: 'Superseded',
},
},
};
export type Translations = typeof en;