mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
localization fixes
This commit is contained in:
@@ -1,21 +1,23 @@
|
||||
import { Loader, Select, SimpleGrid, TextInput } from '@mantine/core';
|
||||
import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AmharicDatePicker } from '@ema-platform/ui';
|
||||
|
||||
export const profileSchema = z.object({
|
||||
professionId: z.string().min(1, 'Select your profession'),
|
||||
firstName: z.string().min(3, 'First name must be at least 3 characters'),
|
||||
middleName: z.string().min(3, 'Middle name must be at least 3 characters'),
|
||||
lastName: z.string().min(3, 'Last name must be at least 3 characters'),
|
||||
gender: z.string().min(1, 'Select your gender'),
|
||||
dob: z.string().min(1, 'Select your date of birth'),
|
||||
pob: z.string().optional(),
|
||||
maritalStatus: z.string().min(1, 'Select your marital status'),
|
||||
});
|
||||
export const profileSchema = (t: TFunction) =>
|
||||
z.object({
|
||||
professionId: z.string().min(1, t('profileForm.validation.professionRequired')),
|
||||
firstName: z.string().min(3, t('profileForm.validation.firstNameMin')),
|
||||
middleName: z.string().min(3, t('profileForm.validation.middleNameMin')),
|
||||
lastName: z.string().min(3, t('profileForm.validation.lastNameMin')),
|
||||
gender: z.string().min(1, t('profileForm.validation.genderRequired')),
|
||||
dob: z.string().min(1, t('profileForm.validation.dobRequired')),
|
||||
pob: z.string().optional(),
|
||||
maritalStatus: z.string().min(1, t('profileForm.validation.maritalStatusRequired')),
|
||||
});
|
||||
|
||||
export type ProfileValues = z.infer<typeof profileSchema>;
|
||||
export type ProfileValues = z.infer<ReturnType<typeof profileSchema>>;
|
||||
|
||||
export const GENDERS = ['MALE', 'FEMALE'] as const;
|
||||
export const MARITAL_STATUSES = ['SINGLE', 'MARRIED', 'DIVORCED', 'WIDOWED'] as const;
|
||||
@@ -45,8 +47,8 @@ export function ProfileFormContent({
|
||||
return (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
<Select
|
||||
label="Profession"
|
||||
placeholder={professionsLoading ? t('common.loading', 'Loading…') : 'Select'}
|
||||
label={t('profileFields.professionId')}
|
||||
placeholder={professionsLoading ? t('common.loading') : t('common.select')}
|
||||
required
|
||||
data={professionOptions}
|
||||
error={errors.professionId?.message}
|
||||
@@ -59,31 +61,31 @@ export function ProfileFormContent({
|
||||
rightSection={professionsLoading ? <Loader size="xs" type="oval" /> : undefined}
|
||||
/>
|
||||
<TextInput
|
||||
label="First Name"
|
||||
placeholder="Enter first name"
|
||||
label={t('profileFields.firstName')}
|
||||
placeholder={t('profileForm.placeholders.firstName')}
|
||||
required
|
||||
{...register('firstName')}
|
||||
error={errors.firstName?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Middle Name"
|
||||
placeholder="Enter middle name"
|
||||
label={t('profileFields.middleName')}
|
||||
placeholder={t('profileForm.placeholders.middleName')}
|
||||
required
|
||||
{...register('middleName')}
|
||||
error={errors.middleName?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Last Name"
|
||||
placeholder="Enter last name"
|
||||
label={t('profileFields.lastName')}
|
||||
placeholder={t('profileForm.placeholders.lastName')}
|
||||
required
|
||||
{...register('lastName')}
|
||||
error={errors.lastName?.message}
|
||||
/>
|
||||
<Select
|
||||
label="Gender"
|
||||
placeholder="Select"
|
||||
label={t('profileFields.gender')}
|
||||
placeholder={t('common.select')}
|
||||
required
|
||||
data={[...GENDERS]}
|
||||
data={GENDERS.map((g) => ({ value: g, label: t(`profileForm.genders.${g}`) }))}
|
||||
error={errors.gender?.message}
|
||||
value={watch('gender')}
|
||||
onChange={(val) => setValue('gender', val || '', { shouldValidate: true })}
|
||||
@@ -91,7 +93,7 @@ export function ProfileFormContent({
|
||||
name="gender"
|
||||
/>
|
||||
<AmharicDatePicker
|
||||
label="Date of Birth"
|
||||
label={t('profileFields.dob')}
|
||||
required
|
||||
value={watch('dob')}
|
||||
onChange={(val) => setValue('dob', val, { shouldValidate: true })}
|
||||
@@ -100,16 +102,16 @@ export function ProfileFormContent({
|
||||
error={errors.dob?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Place of Birth"
|
||||
placeholder="City, Region"
|
||||
label={t('profileFields.pob')}
|
||||
placeholder={t('profileForm.placeholders.pob')}
|
||||
{...register('pob')}
|
||||
error={errors.pob?.message}
|
||||
/>
|
||||
<Select
|
||||
label="Marital Status"
|
||||
placeholder="Select"
|
||||
label={t('profileFields.maritalStatus')}
|
||||
placeholder={t('common.select')}
|
||||
required
|
||||
data={[...MARITAL_STATUSES]}
|
||||
data={MARITAL_STATUSES.map((m) => ({ value: m, label: t(`profileForm.maritalStatuses.${m}`) }))}
|
||||
error={errors.maritalStatus?.message}
|
||||
value={watch('maritalStatus')}
|
||||
onChange={(val) => setValue('maritalStatus', val || '', { shouldValidate: true })}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ProfileRequirementGate({
|
||||
})}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Text size="sm">{requirement.reason}</Text>
|
||||
<Text size="sm">{t(requirement.reason)}</Text>
|
||||
<List size="sm" spacing={2}>
|
||||
{gaps.map((field) => (
|
||||
<List.Item key={field}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Center, Loader } from '@mantine/core';
|
||||
import { Navigate, useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageLoader, notify } from '@ema-platform/ui';
|
||||
@@ -35,8 +34,12 @@ export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = {
|
||||
'primaryPhoneNumber',
|
||||
'email',
|
||||
],
|
||||
reason:
|
||||
'Seafarer registration is built from your profile — these details fill it in for you.',
|
||||
// Translation key, not literal text — `ProfileRequirementGate` runs it
|
||||
// through `t()` at render time (it can't be translated here: this object
|
||||
// is built at module scope, before any component or `useTranslation` call
|
||||
// exists). Reuses `profileGate.seafarerReason`, the same sentence already
|
||||
// shown by the seafarer-redirect toast below.
|
||||
reason: 'profileGate.seafarerReason',
|
||||
};
|
||||
|
||||
const REGISTRATION_TYPE_KEY = 'SEAFARER_REGISTRATION';
|
||||
@@ -75,7 +78,7 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
|
||||
if (!gated) return <>{children}</>;
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader label="Checking seafarer profile…" height={350} />;
|
||||
return <PageLoader label={t('profileGate.checkingProfile')} height={350} />;
|
||||
}
|
||||
|
||||
// A failed lookup must not lock anyone out — the server still refuses the
|
||||
@@ -89,7 +92,7 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
|
||||
// headed back here in the same tick — deciding on the pre-save cache would
|
||||
// bounce them off the screen they just finished.
|
||||
if (isFetching) {
|
||||
return <PageLoader label="Checking seafarer profile…" height={350} />;
|
||||
return <PageLoader label={t('profileGate.checkingProfile')} height={350} />;
|
||||
}
|
||||
|
||||
const target = PROFILE_FIELD_SECTION[gaps[0]];
|
||||
|
||||
@@ -241,7 +241,7 @@ export function ProfilePage() {
|
||||
.string()
|
||||
.refine(
|
||||
(name) => Object.values(splitProfileName(name)).every(Boolean),
|
||||
{ message: 'Enter your first, middle, and last name' },
|
||||
{ message: t('profileForm.validation.nameParts') },
|
||||
),
|
||||
nameAm: z.string().min(1, { message: t('profile.validation.nameRequired') }),
|
||||
username: z.string().min(1, { message: t('profile.validation.usernameRequired') }),
|
||||
@@ -341,7 +341,7 @@ export function ProfilePage() {
|
||||
trigger: profileTriggerValidation,
|
||||
formState: { errors: profileErrors },
|
||||
} = useForm<ProfileValues>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
resolver: zodResolver(profileSchema(t)),
|
||||
values: loadedProfile ?? undefined,
|
||||
});
|
||||
|
||||
@@ -350,7 +350,7 @@ export function ProfilePage() {
|
||||
|
||||
const fullName = formatProfileName(values);
|
||||
if (user && normalizeName(fullName) !== normalizeName(user.name.en)) {
|
||||
notify.error('Profile name must match the name in the Personal tab.');
|
||||
notify.error(t('profile.nameMismatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ export function ProfilePage() {
|
||||
// the address save below) — without this, `missing` stays stale until
|
||||
// a reload.
|
||||
refetchProfile();
|
||||
notify.success('Profile updated');
|
||||
notify.success(t('profile.profileUpdated'));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
} finally {
|
||||
@@ -395,7 +395,7 @@ export function ProfilePage() {
|
||||
profileId,
|
||||
body: toAddressPayload(values),
|
||||
}).unwrap();
|
||||
notify.success('Address saved');
|
||||
notify.success(t('profile.addressSaved'));
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
}
|
||||
@@ -569,19 +569,19 @@ export function ProfilePage() {
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="personal" leftSection={<IconUserCircle size={18} />}>
|
||||
Personal
|
||||
{t('profile.tabs.personal')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="profile" leftSection={<IconUser size={18} />}>
|
||||
Profile
|
||||
{t('profile.tabs.profile')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="address" leftSection={<IconMapPin size={18} />}>
|
||||
Address
|
||||
{t('profile.tabs.address')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="operations"
|
||||
leftSection={<IconBuildingWarehouse size={18} />}
|
||||
>
|
||||
Operations
|
||||
{t('profile.tabs.operations')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="security" leftSection={<IconShieldLock size={18} />}>
|
||||
{t('profile.tabs.security')}
|
||||
@@ -676,15 +676,15 @@ export function ProfilePage() {
|
||||
<Center py="xl"><Loader /></Center>
|
||||
) : !loadedProfile ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No profile found. Complete your profile setup first.
|
||||
{t('profile.maritimeSection.noProfile')}
|
||||
</Text>
|
||||
) : (
|
||||
<form onSubmit={handleProfileSubmit(onSaveProfile)}>
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Title order={5}>Maritime Profile</Title>
|
||||
<Title order={5}>{t('profile.maritimeSection.title')}</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Your professional maritime details
|
||||
{t('profile.maritimeSection.subtitle')}
|
||||
</Text>
|
||||
<ProfileFormContent
|
||||
register={registerProfile}
|
||||
@@ -703,7 +703,7 @@ export function ProfilePage() {
|
||||
loading={isSavingMaritime}
|
||||
leftSection={<IconDeviceFloppy size={18} />}
|
||||
>
|
||||
Save Profile
|
||||
{t('profile.maritimeSection.save')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -721,9 +721,9 @@ export function ProfilePage() {
|
||||
<form onSubmit={handleAddressSubmit(onSaveAddress)}>
|
||||
<Stack gap="xl">
|
||||
<div>
|
||||
<Title order={5}>Address & Contact</Title>
|
||||
<Title order={5}>{t('profile.addressSection.title')}</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Your identity documents, contact details and emergency contact
|
||||
{t('profile.addressSection.subtitle')}
|
||||
</Text>
|
||||
<AddressFormContent
|
||||
register={registerAddress}
|
||||
@@ -877,7 +877,7 @@ export function ProfilePage() {
|
||||
{t(`language.${lng}`)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{lng === 'en' ? 'English (United States)' : 'Amharic'}
|
||||
{t(`profile.languageFull.${lng}`)}
|
||||
</Text>
|
||||
</div>
|
||||
{active ? (
|
||||
|
||||
@@ -71,6 +71,7 @@ export const am: Translations = {
|
||||
},
|
||||
|
||||
common: {
|
||||
select: 'ይምረጡ',
|
||||
back: 'ተመለስ',
|
||||
continue: 'ቀጥል',
|
||||
submit: 'አስገባ',
|
||||
@@ -283,6 +284,7 @@ export const am: Translations = {
|
||||
viewProfile: 'ሙሉ መገለጫ ይመልከቱ',
|
||||
seafarerReason: 'የባህረኞች ምዝገባ የሚዘጋጀው ከመገለጫዎ ነው — እነዚህ መረጃዎች ራሱ ይሞላሉ።',
|
||||
seafarerRedirect: 'የባህረኛ ምዝገባ ለማድረግ መገለጫዎን ያጠናቅቁ። የሚያስፈልጉ፡ {{fields}}',
|
||||
checkingProfile: 'የባህረኛ መገለጫ በመፈተሽ ላይ…',
|
||||
},
|
||||
|
||||
profileSections: {
|
||||
@@ -311,10 +313,30 @@ export const am: Translations = {
|
||||
verified: 'ተረጋግጧል',
|
||||
unverified: 'አልተረጋገጠም',
|
||||
tabs: {
|
||||
personal: 'የግል መረጃ',
|
||||
profile: 'መገለጫ',
|
||||
address: 'አድራሻ',
|
||||
operations: 'የስራ ዘርፍ',
|
||||
security: 'ደህንነት',
|
||||
preferences: 'ምርጫዎች',
|
||||
},
|
||||
maritimeSection: {
|
||||
title: 'የባህር ሙያ መገለጫ',
|
||||
subtitle: 'የባህር ሙያ ዝርዝሮችዎ',
|
||||
noProfile: 'ምንም መገለጫ አልተገኘም። መጀመሪያ መገለጫዎን ያጠናቅቁ።',
|
||||
save: 'መገለጫ አስቀምጥ',
|
||||
},
|
||||
addressSection: {
|
||||
title: 'አድራሻ እና መገናኛ',
|
||||
subtitle: 'የመታወቂያ ሰነዶችዎ፣ የመገናኛ ዝርዝሮችዎ እና የአደጋ ጊዜ ተጠሪ',
|
||||
save: 'አድራሻ አስቀምጥ',
|
||||
},
|
||||
addressSaved: 'አድራሻ ተቀምጧል',
|
||||
nameMismatch: 'የመገለጫ ስም ከግል መረጃ ትር ውስጥ ካለው ስም ጋር መዛመድ አለበት።',
|
||||
languageFull: {
|
||||
en: 'እንግሊዝኛ (አሜሪካ)',
|
||||
am: 'አማርኛ',
|
||||
},
|
||||
personalHint: 'ስምዎ በይፋዊ የ EMA ሰነዶች ላይ እንደሚታየው።',
|
||||
languageTitle: 'ቋንቋ',
|
||||
languageHint: 'በ EMA ፖርታል ላይ የሚጠቀሙበትን ቋንቋ ይምረጡ።',
|
||||
@@ -369,6 +391,35 @@ export const am: Translations = {
|
||||
},
|
||||
},
|
||||
|
||||
profileForm: {
|
||||
placeholders: {
|
||||
firstName: 'የመጀመሪያ ስም ያስገቡ',
|
||||
middleName: 'የአባት ስም ያስገቡ',
|
||||
lastName: 'የአያት ስም ያስገቡ',
|
||||
pob: 'ከተማ፣ ክልል',
|
||||
},
|
||||
genders: {
|
||||
MALE: 'ወንድ',
|
||||
FEMALE: 'ሴት',
|
||||
},
|
||||
maritalStatuses: {
|
||||
SINGLE: 'ያላገባ/ች',
|
||||
MARRIED: 'ያገባ/ች',
|
||||
DIVORCED: 'የፈታ/ች',
|
||||
WIDOWED: 'የሞተበት/ባት',
|
||||
},
|
||||
validation: {
|
||||
professionRequired: 'ሙያዎን ይምረጡ',
|
||||
firstNameMin: 'የመጀመሪያ ስም ቢያንስ 3 ቁምፊዎች መሆን አለበት',
|
||||
middleNameMin: 'የአባት ስም ቢያንስ 3 ቁምፊዎች መሆን አለበት',
|
||||
lastNameMin: 'የአያት ስም ቢያንስ 3 ቁምፊዎች መሆን አለበት',
|
||||
genderRequired: 'ጾታዎን ይምረጡ',
|
||||
dobRequired: 'የትውልድ ቀንዎን ይምረጡ',
|
||||
maritalStatusRequired: 'የጋብቻ ሁኔታዎን ይምረጡ',
|
||||
nameParts: 'የመጀመሪያ፣ የአባት እና የአያት ስምዎን ያስገቡ',
|
||||
},
|
||||
},
|
||||
|
||||
country: {
|
||||
select: 'አገር ይምረጡ',
|
||||
notFound: 'ምንም አገር አልተገኘም',
|
||||
|
||||
@@ -70,6 +70,7 @@ export const en = {
|
||||
},
|
||||
|
||||
common: {
|
||||
select: 'Select',
|
||||
back: 'Back',
|
||||
continue: 'Continue',
|
||||
submit: 'Submit',
|
||||
@@ -283,6 +284,7 @@ export const en = {
|
||||
seafarerReason:
|
||||
'Seafarer registration is built from your profile — these details fill it in for you.',
|
||||
seafarerRedirect: 'Finish your profile to register as a seafarer. Still needed: {{fields}}',
|
||||
checkingProfile: 'Checking seafarer profile…',
|
||||
},
|
||||
|
||||
profileSections: {
|
||||
@@ -311,10 +313,30 @@ export const en = {
|
||||
verified: 'Verified',
|
||||
unverified: 'Unverified',
|
||||
tabs: {
|
||||
personal: 'Personal',
|
||||
profile: 'Profile',
|
||||
address: 'Address',
|
||||
operations: 'Operations',
|
||||
security: 'Security',
|
||||
preferences: 'Preferences',
|
||||
},
|
||||
maritimeSection: {
|
||||
title: 'Maritime Profile',
|
||||
subtitle: 'Your professional maritime details',
|
||||
noProfile: 'No profile found. Complete your profile setup first.',
|
||||
save: 'Save Profile',
|
||||
},
|
||||
addressSection: {
|
||||
title: 'Address & Contact',
|
||||
subtitle: 'Your identity documents, contact details and emergency contact',
|
||||
save: 'Save Address',
|
||||
},
|
||||
addressSaved: 'Address saved',
|
||||
nameMismatch: 'Profile name must match the name in the Personal tab.',
|
||||
languageFull: {
|
||||
en: 'English (United States)',
|
||||
am: 'Amharic',
|
||||
},
|
||||
personalHint: 'Your name as it appears on official EMA documents.',
|
||||
languageTitle: 'Language',
|
||||
languageHint: 'Choose the language used across the EMA portal.',
|
||||
@@ -369,6 +391,35 @@ export const en = {
|
||||
},
|
||||
},
|
||||
|
||||
profileForm: {
|
||||
placeholders: {
|
||||
firstName: 'Enter first name',
|
||||
middleName: 'Enter middle name',
|
||||
lastName: 'Enter last name',
|
||||
pob: 'City, Region',
|
||||
},
|
||||
genders: {
|
||||
MALE: 'Male',
|
||||
FEMALE: 'Female',
|
||||
},
|
||||
maritalStatuses: {
|
||||
SINGLE: 'Single',
|
||||
MARRIED: 'Married',
|
||||
DIVORCED: 'Divorced',
|
||||
WIDOWED: 'Widowed',
|
||||
},
|
||||
validation: {
|
||||
professionRequired: 'Select your profession',
|
||||
firstNameMin: 'First name must be at least 3 characters',
|
||||
middleNameMin: 'Middle name must be at least 3 characters',
|
||||
lastNameMin: 'Last name must be at least 3 characters',
|
||||
genderRequired: 'Select your gender',
|
||||
dobRequired: 'Select your date of birth',
|
||||
maritalStatusRequired: 'Select your marital status',
|
||||
nameParts: 'Enter your first, middle, and last name',
|
||||
},
|
||||
},
|
||||
|
||||
country: {
|
||||
select: 'Select a country',
|
||||
notFound: 'No countries found',
|
||||
|
||||
Reference in New Issue
Block a user