mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: enhance internationalization support across various components and pages
This commit is contained in:
@@ -71,9 +71,11 @@ export function ActivityRail({ detail }: { detail: ApplicationDetail }) {
|
|||||||
? t(`review.events.${history.event}`, {
|
? t(`review.events.${history.event}`, {
|
||||||
defaultValue: history.event,
|
defaultValue: history.event,
|
||||||
})
|
})
|
||||||
: `${history.fromStatus ? STATUS_LABELS[history.fromStatus] : '—'} → ${
|
: `${
|
||||||
STATUS_LABELS[history.toStatus]
|
history.fromStatus
|
||||||
}`,
|
? t(`queue.statusValues.${history.fromStatus}`, STATUS_LABELS[history.fromStatus])
|
||||||
|
: '—'
|
||||||
|
} → ${t(`queue.statusValues.${history.toStatus}`, STATUS_LABELS[history.toStatus])}`,
|
||||||
detail: history.remark ?? undefined,
|
detail: history.remark ?? undefined,
|
||||||
color: STATUS_COLORS[history.toStatus],
|
color: STATUS_COLORS[history.toStatus],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function DecisionBar({
|
|||||||
{/* Left: where the application stands, and who has it. */}
|
{/* Left: where the application stands, and who has it. */}
|
||||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<Badge color={STATUS_COLORS[status]} variant="light" size="lg">
|
<Badge color={STATUS_COLORS[status]} variant="light" size="lg">
|
||||||
{STATUS_LABELS[status]}
|
{t(`queue.statusValues.${status}`, STATUS_LABELS[status])}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
{assigneeName && (
|
{assigneeName && (
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export function DocumentsTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uploadedKeys = new Set(attachments.map((a) => a.documentKey));
|
const uploadedKeys = new Set(attachments.map((a) => a.documentKey));
|
||||||
|
const requirementByKey = new Map(requirements.map((r) => [r.key, r]));
|
||||||
const mandatory = requirements.filter((r) => r.mode !== 'OPTIONAL');
|
const mandatory = requirements.filter((r) => r.mode !== 'OPTIONAL');
|
||||||
const missing = mandatory.filter((r) => !uploadedKeys.has(r.key));
|
const missing = mandatory.filter((r) => !uploadedKeys.has(r.key));
|
||||||
const completeness = mandatory.length
|
const completeness = mandatory.length
|
||||||
@@ -174,7 +175,7 @@ export function DocumentsTab({
|
|||||||
with its own wrap, the name truncates cleanly instead. */}
|
with its own wrap, the name truncates cleanly instead. */}
|
||||||
<Group gap={6} wrap="wrap" align="center">
|
<Group gap={6} wrap="wrap" align="center">
|
||||||
<Text size="sm" fw={500} truncate style={{ maxWidth: '100%' }}>
|
<Text size="sm" fw={500} truncate style={{ maxWidth: '100%' }}>
|
||||||
{attachment.documentKey}
|
{localized(requirementByKey.get(attachment.documentKey)?.name) || attachment.documentKey}
|
||||||
</Text>
|
</Text>
|
||||||
{verdict && (
|
{verdict && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -396,7 +397,11 @@ export function DocumentsTab({
|
|||||||
onClose={() => setPreview(null)}
|
onClose={() => setPreview(null)}
|
||||||
position="right"
|
position="right"
|
||||||
size="xl"
|
size="xl"
|
||||||
title={preview?.documentKey}
|
title={
|
||||||
|
preview
|
||||||
|
? localized(requirementByKey.get(preview.documentKey)?.name) || preview.documentKey
|
||||||
|
: ''
|
||||||
|
}
|
||||||
// Focus is trapped and returned so keyboard users are not dropped at
|
// Focus is trapped and returned so keyboard users are not dropped at
|
||||||
// the top of the page when the drawer closes.
|
// the top of the page when the drawer closes.
|
||||||
trapFocus
|
trapFocus
|
||||||
@@ -406,13 +411,13 @@ export function DocumentsTab({
|
|||||||
isPdf ? (
|
isPdf ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={previewFile.url}
|
src={previewFile.url}
|
||||||
title={preview?.documentKey ?? 'document'}
|
title={preview?.documentKey ?? t('review.documents.previewFallback', 'document')}
|
||||||
style={{ width: '100%', height: '80vh', border: 'none' }}
|
style={{ width: '100%', height: '80vh', border: 'none' }}
|
||||||
/>
|
/>
|
||||||
) : isImage ? (
|
) : isImage ? (
|
||||||
<img
|
<img
|
||||||
src={previewFile.url}
|
src={previewFile.url}
|
||||||
alt={preview?.documentKey ?? 'document'}
|
alt={preview?.documentKey ?? t('review.documents.previewFallback', 'document')}
|
||||||
style={{ maxWidth: '100%' }}
|
style={{ maxWidth: '100%' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -144,10 +144,15 @@ export interface EligibilityRule {
|
|||||||
* pass/fail line means the rule, the figure it was checked against, and the
|
* pass/fail line means the rule, the figure it was checked against, and the
|
||||||
* outcome are all on screen.
|
* outcome are all on screen.
|
||||||
*/
|
*/
|
||||||
|
/** Loose enough to accept i18next's real `t` structurally — see sla.ts for
|
||||||
|
* why this can't just be typed as `TFunction`. */
|
||||||
|
type Translate = (key: string, options?: unknown) => string;
|
||||||
|
|
||||||
export function evaluateEligibility(
|
export function evaluateEligibility(
|
||||||
application: LicenseApplication,
|
application: LicenseApplication,
|
||||||
licenseType: LicenseType | undefined,
|
licenseType: LicenseType | undefined,
|
||||||
locale: string,
|
locale: string,
|
||||||
|
t: Translate,
|
||||||
): EligibilityRule[] {
|
): EligibilityRule[] {
|
||||||
const rules: EligibilityRule[] = [];
|
const rules: EligibilityRule[] = [];
|
||||||
|
|
||||||
@@ -172,11 +177,18 @@ export function evaluateEligibility(
|
|||||||
|
|
||||||
rules.push({
|
rules.push({
|
||||||
id: 'capital-threshold',
|
id: 'capital-threshold',
|
||||||
label: `Paid-up capital ≥ ${format(threshold)}`,
|
label: t('review.eligibilityRule.capitalThreshold', {
|
||||||
|
amount: format(threshold),
|
||||||
|
defaultValue: 'Paid-up capital ≥ {{amount}}',
|
||||||
|
}),
|
||||||
actual:
|
actual:
|
||||||
effective === undefined
|
effective === undefined
|
||||||
? 'Not recorded'
|
? t('review.eligibilityRule.notRecorded', 'Not recorded')
|
||||||
: `${format(effective)}${verified === undefined ? ' (declared)' : ' (verified)'}`,
|
: `${format(effective)}${
|
||||||
|
verified === undefined
|
||||||
|
? t('review.eligibilityRule.declaredSuffix', ' (declared)')
|
||||||
|
: t('review.eligibilityRule.verifiedSuffix', ' (verified)')
|
||||||
|
}`,
|
||||||
// An unverified declaration is not evidence, so it reads as unknown
|
// An unverified declaration is not evidence, so it reads as unknown
|
||||||
// rather than as a pass the officer never actually made.
|
// rather than as a pass the officer never actually made.
|
||||||
status:
|
status:
|
||||||
@@ -200,8 +212,10 @@ export function evaluateEligibility(
|
|||||||
].includes(application.status);
|
].includes(application.status);
|
||||||
rules.push({
|
rules.push({
|
||||||
id: 'inspection',
|
id: 'inspection',
|
||||||
label: 'Physical inspection completed',
|
label: t('review.eligibilityRule.inspectionCompleted', 'Physical inspection completed'),
|
||||||
actual: inspected ? 'Recorded' : 'Not yet recorded',
|
actual: inspected
|
||||||
|
? t('review.eligibilityRule.recorded', 'Recorded')
|
||||||
|
: t('review.eligibilityRule.notYetRecorded', 'Not yet recorded'),
|
||||||
status: inspected ? 'pass' : 'unknown',
|
status: inspected ? 'pass' : 'unknown',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,8 @@ export function LicenseQueuePage() {
|
|||||||
{
|
{
|
||||||
header: t("queue.sla", "Age / SLA"),
|
header: t("queue.sla", "Age / SLA"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const sla = computeSla(row.original, undefined, i18n.language);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- i18next's overloaded TFunction type doesn't structurally match a plain callback signature
|
||||||
|
const sla = computeSla(row.original, undefined, i18n.language, (key, options) => t(key, options as any) as string);
|
||||||
return (
|
return (
|
||||||
// Colour is never the only signal — the label says the same thing.
|
// Colour is never the only signal — the label says the same thing.
|
||||||
<Tooltip label={sla.tooltip} withArrow>
|
<Tooltip label={sla.tooltip} withArrow>
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ type FlagMap = Record<string, { targetType: RemarkTargetType; remark: string }>;
|
|||||||
* that fills it in.
|
* that fills it in.
|
||||||
*/
|
*/
|
||||||
const INSPECTION_CHECKLIST_ITEMS = [
|
const INSPECTION_CHECKLIST_ITEMS = [
|
||||||
{ key: 'office_premises', label: 'Office premises' },
|
{ key: 'office_premises', labelKey: 'review.checklist.officePremises', fallback: 'Office premises' },
|
||||||
{ key: 'storage_facilities', label: 'Warehouse / storage facilities' },
|
{ key: 'storage_facilities', labelKey: 'review.checklist.storageFacilities', fallback: 'Warehouse / storage facilities' },
|
||||||
{ key: 'vehicles_equipment', label: 'Vehicles / equipment' },
|
{ key: 'vehicles_equipment', labelKey: 'review.checklist.vehiclesEquipment', fallback: 'Vehicles / equipment' },
|
||||||
{ key: 'safety_compliance', label: 'Safety & regulatory compliance' },
|
{ key: 'safety_compliance', labelKey: 'review.checklist.safetyCompliance', fallback: 'Safety & regulatory compliance' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
function buildChecklist(
|
function buildChecklist(
|
||||||
@@ -93,7 +93,9 @@ function buildChecklist(
|
|||||||
) {
|
) {
|
||||||
return INSPECTION_CHECKLIST_ITEMS.map((item) => ({
|
return INSPECTION_CHECKLIST_ITEMS.map((item) => ({
|
||||||
key: item.key,
|
key: item.key,
|
||||||
label: item.label,
|
// Persisted as-is (stable English), not the officer's display language —
|
||||||
|
// this is an audit record, not UI text.
|
||||||
|
label: item.fallback,
|
||||||
// Untouched rows default to PASS — the segmented control shows exactly
|
// Untouched rows default to PASS — the segmented control shows exactly
|
||||||
// that, so what the officer saw is what gets recorded.
|
// that, so what the officer saw is what gets recorded.
|
||||||
outcome: outcomes[item.key] ?? 'PASS',
|
outcome: outcomes[item.key] ?? 'PASS',
|
||||||
@@ -127,6 +129,13 @@ export function LicenseReviewPage() {
|
|||||||
{ idOrKey: data?.application.licenseTypeId ?? '', kind: data?.application.kind ?? 'NEW' },
|
{ idOrKey: data?.application.licenseTypeId ?? '', kind: data?.application.kind ?? 'NEW' },
|
||||||
{ skip: !data?.application.licenseTypeId },
|
{ skip: !data?.application.licenseTypeId },
|
||||||
);
|
);
|
||||||
|
// Staff role names are already bilingual on the config — the wire data only
|
||||||
|
// carries the role key (e.g. 'CAPTAIN'), so this is what turns it back into
|
||||||
|
// the label an officer reads.
|
||||||
|
const roleNameByKey = useMemo(
|
||||||
|
() => new Map(requirements?.staffRoleRequirements.map((r) => [r.roleKey, r.name]) ?? []),
|
||||||
|
[requirements],
|
||||||
|
);
|
||||||
|
|
||||||
const [completeReview] = useCompleteReviewMutation();
|
const [completeReview] = useCompleteReviewMutation();
|
||||||
const [requestAdjustment] = useRequestAdjustmentMutation();
|
const [requestAdjustment] = useRequestAdjustmentMutation();
|
||||||
@@ -205,7 +214,7 @@ export function LicenseReviewPage() {
|
|||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label: member
|
label: member
|
||||||
? `${member.roleKey} — ${member.fullName}`
|
? `${localized(roleNameByKey.get(member.roleKey)) || member.roleKey} — ${member.fullName}`
|
||||||
: t('review.staffMember', 'Staff member'),
|
: t('review.staffMember', 'Staff member'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -215,7 +224,7 @@ export function LicenseReviewPage() {
|
|||||||
return { key, label: key };
|
return { key, label: key };
|
||||||
}),
|
}),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[flags, data?.staff, t],
|
[flags, data?.staff, t, localized, roleNameByKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = useMemo(() => {
|
const actions = useMemo(() => {
|
||||||
@@ -272,8 +281,10 @@ export function LicenseReviewPage() {
|
|||||||
const app = data.application;
|
const app = data.application;
|
||||||
const status = app.status;
|
const status = app.status;
|
||||||
const presentation = presentationFor(app.licenseType?.key);
|
const presentation = presentationFor(app.licenseType?.key);
|
||||||
const sla = computeSla(app, undefined, i18n.language);
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- i18next's overloaded TFunction type doesn't structurally match a plain callback signature
|
||||||
const eligibility = evaluateEligibility(app, app.licenseType, i18n.language);
|
const sla = computeSla(app, undefined, i18n.language, (key, options) => t(key, options as any) as string);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- i18next's overloaded TFunction type doesn't structurally match a plain callback signature
|
||||||
|
const eligibility = evaluateEligibility(app, app.licenseType, i18n.language, (key, options) => t(key, options as any) as string);
|
||||||
|
|
||||||
const rawThreshold = app.licenseType?.capitalThreshold;
|
const rawThreshold = app.licenseType?.capitalThreshold;
|
||||||
const threshold =
|
const threshold =
|
||||||
@@ -504,7 +515,7 @@ export function LicenseReviewPage() {
|
|||||||
{app.applicationNumber}
|
{app.applicationNumber}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge color={STATUS_COLORS[status]} variant="light">
|
<Badge color={STATUS_COLORS[status]} variant="light">
|
||||||
{STATUS_LABELS[status]}
|
{t(`queue.statusValues.${status}`, STATUS_LABELS[status])}
|
||||||
</Badge>
|
</Badge>
|
||||||
{app.adjustmentRound > 0 && (
|
{app.adjustmentRound > 0 && (
|
||||||
<Badge color="orange" variant="light" size="sm">
|
<Badge color="orange" variant="light" size="sm">
|
||||||
@@ -543,7 +554,7 @@ export function LicenseReviewPage() {
|
|||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
<SummaryRow label={t('review.type', 'Type')} value={localized(app.licenseType?.name)} />
|
<SummaryRow label={t('review.type', 'Type')} value={localized(app.licenseType?.name)} />
|
||||||
<SummaryRow label={t('review.tin', 'TIN')} value={app.tinNumber} />
|
<SummaryRow label={t('review.tin', 'TIN')} value={app.tinNumber} />
|
||||||
<SummaryRow label={t('review.kind', 'Kind')} value={app.kind} />
|
<SummaryRow label={t('review.kind', 'Kind')} value={t(`review.kindValues.${app.kind}`, app.kind)} />
|
||||||
<SummaryRow
|
<SummaryRow
|
||||||
label={t('review.submitted', 'Submitted')}
|
label={t('review.submitted', 'Submitted')}
|
||||||
value={showDate(app.submittedAt)}
|
value={showDate(app.submittedAt)}
|
||||||
@@ -605,7 +616,7 @@ export function LicenseReviewPage() {
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
title={
|
title={
|
||||||
<Text size="xs" fw={600}>
|
<Text size="xs" fw={600}>
|
||||||
{STATUS_LABELS[entry.toStatus] ?? entry.toStatus}
|
{t(`queue.statusValues.${entry.toStatus}`, STATUS_LABELS[entry.toStatus] ?? entry.toStatus)}
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -797,7 +808,7 @@ export function LicenseReviewPage() {
|
|||||||
{data.staff.map((member) => (
|
{data.staff.map((member) => (
|
||||||
<Table.Tr key={member.id}>
|
<Table.Tr key={member.id}>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="xs">{member.roleKey}</Text>
|
<Text size="xs">{localized(roleNameByKey.get(member.roleKey)) || member.roleKey}</Text>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Text size="sm">{member.fullName}</Text>
|
<Text size="sm">{member.fullName}</Text>
|
||||||
@@ -878,7 +889,11 @@ export function LicenseReviewPage() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color={inspection.result === 'FAILED' ? 'red' : 'teal'}
|
color={inspection.result === 'FAILED' ? 'red' : 'teal'}
|
||||||
>
|
>
|
||||||
{inspection.result ?? inspection.status}
|
{inspection.result === 'PASSED'
|
||||||
|
? t('review.passed', 'Passed')
|
||||||
|
: inspection.result === 'FAILED'
|
||||||
|
? t('review.failed', 'Failed')
|
||||||
|
: t(`review.inspectionStatus.${inspection.status}`, inspection.status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
@@ -984,7 +999,7 @@ export function LicenseReviewPage() {
|
|||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
{INSPECTION_CHECKLIST_ITEMS.map((item) => (
|
{INSPECTION_CHECKLIST_ITEMS.map((item) => (
|
||||||
<Group key={item.key} justify="space-between" wrap="nowrap">
|
<Group key={item.key} justify="space-between" wrap="nowrap">
|
||||||
<Text size="sm">{item.label}</Text>
|
<Text size="sm">{t(item.labelKey, item.fallback)}</Text>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
size="xs"
|
size="xs"
|
||||||
value={checklist[item.key] ?? 'PASS'}
|
value={checklist[item.key] ?? 'PASS'}
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ export interface SlaState {
|
|||||||
ratio: number;
|
ratio: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Loose enough to accept i18next's real `t` structurally; callers pass that
|
||||||
|
* one, and a plain function falls back to English by resolving
|
||||||
|
* `defaultValue` itself, so `computeSla` still works with no i18n context
|
||||||
|
* (the CSV export). */
|
||||||
|
export type SlaTranslate = (key: string, options?: unknown) => string;
|
||||||
|
|
||||||
|
function tr(t: SlaTranslate | undefined, key: string, options: Record<string, unknown> | string): string {
|
||||||
|
if (t) return t(key, options);
|
||||||
|
if (typeof options === 'string') return options;
|
||||||
|
const template = String(options['defaultValue'] ?? '');
|
||||||
|
return template.replace(/\{\{(\w+)\}\}/g, (_, name) => String(options[name] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
const hours = Math.floor(Math.abs(ms) / HOUR_MS);
|
const hours = Math.floor(Math.abs(ms) / HOUR_MS);
|
||||||
if (hours < 1) return '<1h';
|
if (hours < 1) return '<1h';
|
||||||
@@ -37,6 +50,7 @@ export function computeSla(
|
|||||||
application: LicenseApplication,
|
application: LicenseApplication,
|
||||||
now: number = Date.now(),
|
now: number = Date.now(),
|
||||||
language = 'en',
|
language = 'en',
|
||||||
|
t?: SlaTranslate,
|
||||||
): SlaState {
|
): SlaState {
|
||||||
const slaHours = application.licenseType?.slaHours;
|
const slaHours = application.licenseType?.slaHours;
|
||||||
const submittedAt = application.submittedAt;
|
const submittedAt = application.submittedAt;
|
||||||
@@ -46,7 +60,7 @@ export function computeSla(
|
|||||||
state: 'untracked',
|
state: 'untracked',
|
||||||
color: 'gray',
|
color: 'gray',
|
||||||
label: '—',
|
label: '—',
|
||||||
tooltip: 'No turnaround target is set for this licence type.',
|
tooltip: tr(t, 'review.sla.untracked', 'No turnaround target is set for this licence type.'),
|
||||||
ratio: 0,
|
ratio: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -56,15 +70,23 @@ export function computeSla(
|
|||||||
const elapsed = (application.decidedAt ? new Date(application.decidedAt).getTime() : now) - submitted;
|
const elapsed = (application.decidedAt ? new Date(application.decidedAt).getTime() : now) - submitted;
|
||||||
const window = slaHours * HOUR_MS;
|
const window = slaHours * HOUR_MS;
|
||||||
const ratio = Math.min(Math.max(elapsed / window, 0), 1);
|
const ratio = Math.min(Math.max(elapsed / window, 0), 1);
|
||||||
const targetText = `Target ${slaHours}h from submission (${dateDisplayer(target, language)})`;
|
const targetText = tr(t, 'review.sla.target', {
|
||||||
|
hours: slaHours,
|
||||||
|
date: dateDisplayer(target, language),
|
||||||
|
defaultValue: 'Target {{hours}}h from submission ({{date}})',
|
||||||
|
});
|
||||||
|
|
||||||
if (application.decidedAt) {
|
if (application.decidedAt) {
|
||||||
const met = elapsed <= window;
|
const met = elapsed <= window;
|
||||||
return {
|
return {
|
||||||
state: 'decided',
|
state: 'decided',
|
||||||
color: met ? 'teal' : 'gray',
|
color: met ? 'teal' : 'gray',
|
||||||
label: met ? 'Met' : 'Missed',
|
label: tr(t, met ? 'review.sla.met' : 'review.sla.missed', met ? 'Met' : 'Missed'),
|
||||||
tooltip: `Decided in ${formatDuration(elapsed)}. ${targetText}`,
|
tooltip: tr(t, 'review.sla.decidedIn', {
|
||||||
|
duration: formatDuration(elapsed),
|
||||||
|
target: targetText,
|
||||||
|
defaultValue: 'Decided in {{duration}}. {{target}}',
|
||||||
|
}),
|
||||||
ratio,
|
ratio,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -74,8 +96,15 @@ export function computeSla(
|
|||||||
return {
|
return {
|
||||||
state: 'breached',
|
state: 'breached',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
label: `Overdue ${formatDuration(remaining)}`,
|
label: tr(t, 'review.sla.overdue', {
|
||||||
tooltip: `Overdue by ${formatDuration(remaining)}. ${targetText}`,
|
duration: formatDuration(remaining),
|
||||||
|
defaultValue: 'Overdue {{duration}}',
|
||||||
|
}),
|
||||||
|
tooltip: tr(t, 'review.sla.overdueBy', {
|
||||||
|
duration: formatDuration(remaining),
|
||||||
|
target: targetText,
|
||||||
|
defaultValue: 'Overdue by {{duration}}. {{target}}',
|
||||||
|
}),
|
||||||
ratio: 1,
|
ratio: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -84,8 +113,15 @@ export function computeSla(
|
|||||||
return {
|
return {
|
||||||
state: used >= WARNING_RATIO ? 'warning' : 'ok',
|
state: used >= WARNING_RATIO ? 'warning' : 'ok',
|
||||||
color: used >= WARNING_RATIO ? 'yellow' : 'teal',
|
color: used >= WARNING_RATIO ? 'yellow' : 'teal',
|
||||||
label: `${formatDuration(remaining)} left`,
|
label: tr(t, 'review.sla.left', {
|
||||||
tooltip: `${formatDuration(remaining)} remaining. ${targetText}`,
|
duration: formatDuration(remaining),
|
||||||
|
defaultValue: '{{duration}} left',
|
||||||
|
}),
|
||||||
|
tooltip: tr(t, 'review.sla.remaining', {
|
||||||
|
duration: formatDuration(remaining),
|
||||||
|
target: targetText,
|
||||||
|
defaultValue: '{{duration}} remaining. {{target}}',
|
||||||
|
}),
|
||||||
ratio,
|
ratio,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1016,6 +1016,42 @@ export const am: Translations = {
|
|||||||
noFile: "ፋይል የለም",
|
noFile: "ፋይል የለም",
|
||||||
noFileUploaded: "እስካሁን ምንም አልተጫነም",
|
noFileUploaded: "እስካሁን ምንም አልተጫነም",
|
||||||
noInlinePreview: "ይህ የፋይል ዓይነት በአሳሹ ውስጥ ቅድመ እይታ አይደረግም።",
|
noInlinePreview: "ይህ የፋይል ዓይነት በአሳሹ ውስጥ ቅድመ እይታ አይደረግም።",
|
||||||
|
previewFallback: "ሰነድ",
|
||||||
|
},
|
||||||
|
inspectionStatus: {
|
||||||
|
SCHEDULED: "ተይዟል",
|
||||||
|
COMPLETED: "ተጠናቋል",
|
||||||
|
CANCELLED: "ተሰርዟል",
|
||||||
|
},
|
||||||
|
kindValues: {
|
||||||
|
NEW: "አዲስ",
|
||||||
|
RENEWAL: "እድሳት",
|
||||||
|
},
|
||||||
|
checklist: {
|
||||||
|
officePremises: "የቢሮ ግቢ",
|
||||||
|
storageFacilities: "መጋዘን / የማከማቻ ተቋማት",
|
||||||
|
vehiclesEquipment: "ተሽከርካሪዎች / መሳሪያዎች",
|
||||||
|
safetyCompliance: "ደህንነት እና ደንብ ማክበር",
|
||||||
|
},
|
||||||
|
eligibilityRule: {
|
||||||
|
capitalThreshold: "የተከፈለ ካፒታል ≥ {{amount}}",
|
||||||
|
notRecorded: "አልተመዘገበም",
|
||||||
|
declaredSuffix: " (የተገለጸ)",
|
||||||
|
verifiedSuffix: " (የተረጋገጠ)",
|
||||||
|
inspectionCompleted: "አካላዊ ምርመራ ተጠናቋል",
|
||||||
|
recorded: "ተመዝግቧል",
|
||||||
|
notYetRecorded: "ገና አልተመዘገበም",
|
||||||
|
},
|
||||||
|
sla: {
|
||||||
|
untracked: "ለዚህ የፈቃድ ዓይነት የማጠናቀቂያ ኢላማ አልተቀመጠም።",
|
||||||
|
target: "ኢላማ ከቀረበ በኋላ {{hours}} ሰዓት ({{date}})",
|
||||||
|
met: "ተሟልቷል",
|
||||||
|
missed: "አልተሟላም",
|
||||||
|
decidedIn: "በ{{duration}} ውስጥ ተወስኗል። {{target}}",
|
||||||
|
overdue: "{{duration}} ዘግይቷል",
|
||||||
|
overdueBy: "በ{{duration}} ዘግይቷል። {{target}}",
|
||||||
|
left: "{{duration}} ቀርቷል",
|
||||||
|
remaining: "{{duration}} ቀርቷል። {{target}}",
|
||||||
},
|
},
|
||||||
done: {
|
done: {
|
||||||
completeReview: "ግምገማ ተጠናቋል",
|
completeReview: "ግምገማ ተጠናቋል",
|
||||||
|
|||||||
@@ -1012,6 +1012,42 @@ export const en = {
|
|||||||
noFile: 'No file',
|
noFile: 'No file',
|
||||||
noFileUploaded: 'Nothing uploaded yet',
|
noFileUploaded: 'Nothing uploaded yet',
|
||||||
noInlinePreview: 'This file type cannot be previewed in the browser.',
|
noInlinePreview: 'This file type cannot be previewed in the browser.',
|
||||||
|
previewFallback: 'document',
|
||||||
|
},
|
||||||
|
inspectionStatus: {
|
||||||
|
SCHEDULED: 'Scheduled',
|
||||||
|
COMPLETED: 'Completed',
|
||||||
|
CANCELLED: 'Cancelled',
|
||||||
|
},
|
||||||
|
kindValues: {
|
||||||
|
NEW: 'New',
|
||||||
|
RENEWAL: 'Renewal',
|
||||||
|
},
|
||||||
|
checklist: {
|
||||||
|
officePremises: 'Office premises',
|
||||||
|
storageFacilities: 'Warehouse / storage facilities',
|
||||||
|
vehiclesEquipment: 'Vehicles / equipment',
|
||||||
|
safetyCompliance: 'Safety & regulatory compliance',
|
||||||
|
},
|
||||||
|
eligibilityRule: {
|
||||||
|
capitalThreshold: 'Paid-up capital ≥ {{amount}}',
|
||||||
|
notRecorded: 'Not recorded',
|
||||||
|
declaredSuffix: ' (declared)',
|
||||||
|
verifiedSuffix: ' (verified)',
|
||||||
|
inspectionCompleted: 'Physical inspection completed',
|
||||||
|
recorded: 'Recorded',
|
||||||
|
notYetRecorded: 'Not yet recorded',
|
||||||
|
},
|
||||||
|
sla: {
|
||||||
|
untracked: 'No turnaround target is set for this licence type.',
|
||||||
|
target: 'Target {{hours}}h from submission ({{date}})',
|
||||||
|
met: 'Met',
|
||||||
|
missed: 'Missed',
|
||||||
|
decidedIn: 'Decided in {{duration}}. {{target}}',
|
||||||
|
overdue: 'Overdue {{duration}}',
|
||||||
|
overdueBy: 'Overdue by {{duration}}. {{target}}',
|
||||||
|
left: '{{duration}} left',
|
||||||
|
remaining: '{{duration}} remaining. {{target}}',
|
||||||
},
|
},
|
||||||
done: {
|
done: {
|
||||||
completeReview: 'Review completed',
|
completeReview: 'Review completed',
|
||||||
|
|||||||
Reference in New Issue
Block a user