diff --git a/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx b/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx index 9b8a89c52..1e0ba931c 100644 --- a/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx +++ b/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx @@ -68,6 +68,7 @@ export function ConfigDrivenSection({ {isNationality ? ( onChange(field.key, v)} /> diff --git a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx index a4977caab..a57914085 100644 --- a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx +++ b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx @@ -50,7 +50,7 @@ import { type FormFieldConfig, type ValidationIssue, } from '@ema-platform/api'; -import { getCountryCode, ModalFooter } from '@ema-platform/ui'; +import { getCountryCode, getCountryName, ModalFooter } from '@ema-platform/ui'; import { useCurrentProfile } from '@ema-platform/auth'; import { ConfigDrivenSection } from '../components/ConfigDrivenSection'; import { DocumentSlots } from '../components/DocumentSlots'; @@ -210,11 +210,20 @@ export function LicenseApplicationPage() { // During an adjustment round only flagged sections are editable, so don't // even attempt a write the server would reject. if (isAdjusting && !flaggedSections[sectionKey]) return; + const values = { ...(draft[sectionKey] ?? {}) }; + // The picker works in alpha-2 codes (CountrySelect); the backend, like + // the profile Address endpoint, stores the full country name. + const nationalityField = config?.licenseType.formSchema.sections + .find((s) => s.key === sectionKey) + ?.fields.find((f) => f.key === 'nationality' || localized(f.label).toLowerCase().includes('nationality')); + if (nationalityField && values[nationalityField.key]) { + values[nationalityField.key] = getCountryName(values[nationalityField.key] as string) || values[nationalityField.key]; + } try { await patchSection({ id: appId as string, sectionKey, - values: draft[sectionKey] ?? {}, + values, }).unwrap(); } catch (err) { notifications.show({ diff --git a/apps/portal/src/app/features/profile/components/AddressFormContent.tsx b/apps/portal/src/app/features/profile/components/AddressFormContent.tsx index 127fb066b..9944796af 100644 --- a/apps/portal/src/app/features/profile/components/AddressFormContent.tsx +++ b/apps/portal/src/app/features/profile/components/AddressFormContent.tsx @@ -132,6 +132,7 @@ export function AddressFormContent({ /> setValue('nationality', val || '', { shouldValidate: true })} diff --git a/libs/ui/src/lib/input/CountrySelect.tsx b/libs/ui/src/lib/input/CountrySelect.tsx index 06e9f55c5..9fc0343c3 100644 --- a/libs/ui/src/lib/input/CountrySelect.tsx +++ b/libs/ui/src/lib/input/CountrySelect.tsx @@ -2,13 +2,19 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Group, Select, Text, type ComboboxItem, type SelectProps } from '@mantine/core'; import { registerLocale, getNames, getName, getAlpha2Code } from 'i18n-iso-countries'; +import { + registerLocale as registerNationalityLocale, + getName as getNationalityNameRaw, +} from 'i18n-nationality'; import * as Flags from 'country-flag-icons/react/3x2'; import en from 'i18n-iso-countries/langs/en.json'; import am from 'i18n-iso-countries/langs/am.json'; +import nationalityEn from 'i18n-nationality/langs/en.json'; // Registered once at module load — locale data is static. registerLocale(en); registerLocale(am); +registerNationalityLocale(nationalityEn); type CountryLang = 'en' | 'am'; @@ -26,6 +32,18 @@ export function getCountryCode(name: string | null | undefined, lang: CountryLan return name ? getAlpha2Code(name, lang) : undefined; } +/** + * Localized demonym for an alpha-2 code (e.g. "ET" -> "Ethiopian"), for + * nationality fields as opposed to plain country fields. + * ponytail: i18n-nationality only ships an English locale — Amharic falls + * back to the country name until an Amharic demonym dataset shows up. + */ +export function getNationalityName(code: string | null | undefined, lang: CountryLang = 'en'): string { + if (!code) return ''; + if (lang === 'en') return getNationalityNameRaw(code, 'en') ?? getCountryName(code, lang); + return getCountryName(code, lang); +} + function CountryFlag({ code }: { code: string }) { const Flag = Flags[code as keyof typeof Flags]; return Flag ? : null; @@ -57,12 +75,15 @@ export interface CountrySelectProps { error?: React.ReactNode; required?: boolean; disabled?: boolean; + /** Show nationality labels ("Ethiopian") instead of country names ("Ethiopia"). */ + demonym?: boolean; } export function CountrySelect({ value, onChange, placeholder, + demonym, ...rest }: CountrySelectProps) { const { t, i18n } = useTranslation(); @@ -70,10 +91,10 @@ export function CountrySelect({ const countries = useMemo( () => - Object.entries(getNames(lang)) - .map(([code, label]) => ({ value: code, label })) + Object.keys(getNames(lang)) + .map((code) => ({ value: code, label: demonym ? getNationalityName(code, lang) : getCountryName(code, lang) })) .sort((a, b) => a.label.localeCompare(b.label, lang)), - [lang], + [lang, demonym], ); return ( diff --git a/package-lock.json b/package-lock.json index d58722833..05e109a97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "dayjs": "^1.11.20", "ethiopian-calendar-date-converter": "^2.1.6", "i18n-iso-countries": "^7.14.0", + "i18n-nationality": "^1.4.0", "i18next": "^25.6.0", "js-cookie": "^3.0.8", "react": "^19.0.0", @@ -8426,6 +8427,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/diacritics": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/diacritics/-/diacritics-1.3.3.tgz", + "integrity": "sha512-wt0tBItmBsOUVZ8+MCrkBMoVfH/EUZeTXwYSekVVYilZlGDYssREUR+sX72mHvl2IrbdCKgpYARXKh3awD2how==", + "license": "MIT" + }, "node_modules/@types/dompurify": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", @@ -12814,6 +12821,19 @@ "node": ">= 12" } }, + "node_modules/i18n-nationality": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/i18n-nationality/-/i18n-nationality-1.4.0.tgz", + "integrity": "sha512-/zZBGY8TbdL4xsIo5RMe1XTdMyHlHhLA6S2u7XsYJDh2c2ONVwxf0Pz5yKcLjKxtl8ma4jgtGnUaEgF/4ZOWbQ==", + "license": "MIT", + "dependencies": { + "@types/diacritics": "^1.3.1", + "diacritics": "^1.3.0" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/i18next": { "version": "25.10.10", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", diff --git a/package.json b/package.json index 9e5d660fa..ed486ec5f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dayjs": "^1.11.20", "ethiopian-calendar-date-converter": "^2.1.6", "i18n-iso-countries": "^7.14.0", + "i18n-nationality": "^1.4.0", "i18next": "^25.6.0", "js-cookie": "^3.0.8", "react": "^19.0.0",