feat: add internationalization support for seafarer registry

- Integrated i18n for seafarer registry page, including translations for various UI elements and statuses.
- Updated SeafarerDetailDrawer and StatusModal components to utilize translation hooks.
- Enhanced user feedback messages for status updates and errors.
- Added new translations for Amharic and English locales, covering seafarer statuses, drawer labels, modal content, and table headers.
localization for backoffice
This commit is contained in:
estifanos
2026-08-12 10:55:14 +00:00
parent 9897a9bf78
commit 8f2dcb30f0
6 changed files with 620 additions and 149 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
Badge,
@@ -59,6 +60,7 @@ function feeText(amount: string | number | null, currency: string): string {
}
export function PaymentConfigPage() {
const { t } = useTranslation();
const { data, isLoading, isFetching, error, refetch } =
useGetLicenseTypesQuery();
const { data: capabilities } = useGetPaymentCapabilitiesQuery();
@@ -78,7 +80,7 @@ export function PaymentConfigPage() {
<Alert
color="red"
icon={<IconAlertTriangle size={18} />}
title="Could not load licence types"
title={t('paymentConfig.loadError', 'Could not load licence types')}
>
<Text size="sm">{extractErrorMessage(error)}</Text>
</Alert>
@@ -92,7 +94,7 @@ export function PaymentConfigPage() {
const columns: AdvancedColumn<LicenseType>[] = [
{
header: 'Licence type',
header: t('paymentConfig.columns.type', 'Licence type'),
cell: ({ row }) => (
<>
<Text size="sm" fw={600}>
@@ -105,7 +107,7 @@ export function PaymentConfigPage() {
),
},
{
header: 'New application',
header: t('paymentConfig.columns.newApplication', 'New application'),
cell: ({ row }) => (
<Text size="sm" fw={500}>
{feeText(row.original.feeNewApplication, row.original.feeCurrency)}
@@ -113,7 +115,7 @@ export function PaymentConfigPage() {
),
},
{
header: 'Renewal',
header: t('paymentConfig.columns.renewal', 'Renewal'),
cell: ({ row }) => {
const type = row.original;
if (type.feeNewApplication === null) {
@@ -126,11 +128,16 @@ export function PaymentConfigPage() {
}
if (type.feeRenewal === null) {
return (
<Tooltip label="No separate renewal fee — renewal is charged at the new-application rate">
<Tooltip
label={t(
'paymentConfig.renewalSameTooltip',
'No separate renewal fee — renewal is charged at the new-application rate',
)}
>
<Text size="sm" c="dimmed">
{feeText(type.feeNewApplication, type.feeCurrency)}{' '}
<Text span size="xs" c="dimmed">
(same as new)
{t('paymentConfig.renewalSameAsNew', '(same as new)')}
</Text>
</Text>
</Tooltip>
@@ -144,16 +151,21 @@ export function PaymentConfigPage() {
},
},
{
header: 'Charged?',
header: t('paymentConfig.columns.charged', 'Charged?'),
cell: ({ row }) =>
row.original.issuesCertificate ? (
<Badge variant="light" color="teal" size="sm">
On approval
{t('paymentConfig.chargedOnApproval', 'On approval')}
</Badge>
) : (
<Tooltip label="This licence type ends with an EMA decision and never reaches a payment stage">
<Tooltip
label={t(
'paymentConfig.chargedNotChargedTooltip',
'This licence type ends with an EMA decision and never reaches a payment stage',
)}
>
<Badge variant="light" color="gray" size="sm">
Not charged
{t('paymentConfig.chargedNotCharged', 'Not charged')}
</Badge>
</Tooltip>
),
@@ -169,7 +181,7 @@ export function PaymentConfigPage() {
leftSection={<IconEdit size={14} />}
onClick={() => setEditing(row.original)}
>
Edit
{t('paymentConfig.edit', 'Edit')}
</Button>
),
},
@@ -179,10 +191,12 @@ export function PaymentConfigPage() {
<Stack gap="lg">
<Group justify="space-between" align="flex-start">
<div>
<Title order={3}>Payment configuration</Title>
<Title order={3}>{t('paymentConfig.title', 'Payment configuration')}</Title>
<Text size="sm" c="dimmed" mt={4}>
What each licence costs. Applicants are charged after approval, and
the amount is fixed onto the application at that moment.
{t(
'paymentConfig.subtitle',
'What each licence costs. Applicants are charged after approval, and the amount is fixed onto the application at that moment.',
)}
</Text>
</div>
<ThemeIcon size="xl" radius="md" variant="light">
@@ -197,9 +211,10 @@ export function PaymentConfigPage() {
icon={<IconInfoCircle size={18} />}
>
<Text size="sm">
A change applies to applications approved from now on. Anything
already approved keeps the amount it was quoted, so an edit here can
never alter what an applicant has already been asked to pay.
{t(
'paymentConfig.changeNotice',
'A change applies to applications approved from now on. Anything already approved keeps the amount it was quoted, so an edit here can never alter what an applicant has already been asked to pay.',
)}
</Text>
</Alert>
@@ -229,6 +244,7 @@ export function PaymentConfigPage() {
* them as editable fields would be a lie.
*/
function GatewayPanel({ bypassEnabled }: { bypassEnabled?: boolean }) {
const { t } = useTranslation();
return (
<Paper withBorder radius="md" p="md">
<Group gap="xs" mb="xs">
@@ -236,23 +252,31 @@ function GatewayPanel({ bypassEnabled }: { bypassEnabled?: boolean }) {
<IconLock size={13} />
</ThemeIcon>
<Text fw={600} size="sm">
Payment gateway
{t('paymentConfig.gateway.title', 'Payment gateway')}
</Text>
<Text size="xs" c="dimmed">
Set by the payment service environment not editable here.
{t(
'paymentConfig.gateway.subtitle',
'Set by the payment service environment — not editable here.',
)}
</Text>
</Group>
<Group gap="sm">
<Badge variant="light">Telebirr</Badge>
<Badge variant="light">{t('paymentConfig.gateway.provider', 'Telebirr')}</Badge>
{bypassEnabled ? (
<Tooltip label="ALLOW_PAYMENT_BYPASS is on, so an applicant can settle a fee without paying. It is refused in production.">
<Tooltip
label={t(
'paymentConfig.gateway.bypassTooltip',
'ALLOW_PAYMENT_BYPASS is on, so an applicant can settle a fee without paying. It is refused in production.',
)}
>
<Badge variant="light" color="orange">
Test bypass enabled
{t('paymentConfig.gateway.bypassEnabled', 'Test bypass enabled')}
</Badge>
</Tooltip>
) : (
<Badge variant="light" color="gray">
Test bypass off
{t('paymentConfig.gateway.bypassOff', 'Test bypass off')}
</Badge>
)}
</Group>
@@ -267,6 +291,7 @@ function FeeEditModal({
licenseType: LicenseType | null;
onClose: () => void;
}) {
const { t } = useTranslation();
const [updateFees, { isLoading }] = useUpdateLicenseFeesMutation();
const [newFee, setNewFee] = useState<number | ''>('');
const [renewalFee, setRenewalFee] = useState<number | ''>('');
@@ -294,11 +319,21 @@ function FeeEditModal({
async function save() {
if (!licenseType) return;
if (chargeable && newFee === '') {
notify.error('Enter a new-application fee, or turn off "carries a fee".');
notify.error(
t(
'paymentConfig.modal.missingNewFee',
'Enter a new-application fee, or turn off "carries a fee".',
),
);
return;
}
if (chargeable && !sameAsNew && renewalFee === '') {
notify.error('Enter a renewal fee, or charge renewal at the same rate.');
notify.error(
t(
'paymentConfig.modal.missingRenewalFee',
'Enter a renewal fee, or charge renewal at the same rate.',
),
);
return;
}
try {
@@ -308,10 +343,17 @@ function FeeEditModal({
feeRenewal: chargeable && !sameAsNew ? Number(renewalFee) : null,
feeCurrency: currency.trim() || 'ETB',
}).unwrap();
notify.success(`${localized(licenseType.name)} fees updated.`);
notify.success(
t('paymentConfig.modal.updated', {
type: localized(licenseType.name),
defaultValue: '{{type}} fees updated.',
}),
);
onClose();
} catch (err) {
notify.error(extractErrorMessage(err, 'Could not save the fees'));
notify.error(
extractErrorMessage(err, t('paymentConfig.modal.saveFailed', 'Could not save the fees')),
);
}
}
@@ -319,7 +361,14 @@ function FeeEditModal({
<Modal
opened={!!licenseType}
onClose={onClose}
title={licenseType ? `Fees — ${localized(licenseType.name)}` : ''}
title={
licenseType
? t('paymentConfig.modal.title', {
type: localized(licenseType.name),
defaultValue: 'Fees — {{type}}',
})
: ''
}
centered
radius="lg"
>
@@ -332,9 +381,10 @@ function FeeEditModal({
icon={<IconInfoCircle size={16} />}
>
<Text size="sm">
This licence type concludes with an EMA decision and never
reaches a payment stage, so a fee set here stays unused until
that changes.
{t(
'paymentConfig.modal.noPaymentStage',
'This licence type concludes with an EMA decision and never reaches a payment stage, so a fee set here stays unused until that changes.',
)}
</Text>
</Alert>
)}
@@ -342,14 +392,17 @@ function FeeEditModal({
<Switch
checked={chargeable}
onChange={(e) => setChargeable(e.currentTarget.checked)}
label="This licence carries a fee"
description="Turn off for licence types applicants are never charged for."
label={t('paymentConfig.modal.chargeableLabel', 'This licence carries a fee')}
description={t(
'paymentConfig.modal.chargeableDescription',
'Turn off for licence types applicants are never charged for.',
)}
/>
{chargeable && (
<>
<NumberInput
label="New application fee"
label={t('paymentConfig.modal.newFeeLabel', 'New application fee')}
value={newFee}
onChange={(v) => setNewFee(v === '' ? '' : Number(v))}
min={0}
@@ -362,13 +415,16 @@ function FeeEditModal({
<Switch
checked={sameAsNew}
onChange={(e) => setSameAsNew(e.currentTarget.checked)}
label="Charge renewal at the same rate"
description="Turn off to set a separate renewal fee."
label={t('paymentConfig.modal.sameRateLabel', 'Charge renewal at the same rate')}
description={t(
'paymentConfig.modal.sameRateDescription',
'Turn off to set a separate renewal fee.',
)}
/>
{!sameAsNew && (
<NumberInput
label="Renewal fee"
label={t('paymentConfig.modal.renewalFeeLabel', 'Renewal fee')}
value={renewalFee}
onChange={(v) => setRenewalFee(v === '' ? '' : Number(v))}
min={0}
@@ -380,7 +436,7 @@ function FeeEditModal({
)}
<TextInput
label="Currency"
label={t('paymentConfig.modal.currencyLabel', 'Currency')}
value={currency}
onChange={(e) =>
setCurrency(e.currentTarget.value.toUpperCase())
@@ -392,10 +448,10 @@ function FeeEditModal({
<ModalFooter mt="xs">
<Button variant="default" onClick={onClose} disabled={isLoading}>
Cancel
{t('paymentConfig.modal.cancel', 'Cancel')}
</Button>
<Button onClick={save} loading={isLoading}>
Save fees
{t('paymentConfig.modal.save', 'Save fees')}
</Button>
</ModalFooter>
</Stack>