mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
feat: add nationality support with demonym in CountrySelect and related components
This commit is contained in:
@@ -68,6 +68,7 @@ export function ConfigDrivenSection({
|
||||
{isNationality ? (
|
||||
<CountrySelect
|
||||
{...common}
|
||||
demonym
|
||||
value={(value as string) ?? null}
|
||||
onChange={(v) => onChange(field.key, v)}
|
||||
/>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -132,6 +132,7 @@ export function AddressFormContent({
|
||||
/>
|
||||
<CountrySelect
|
||||
label="Nationality"
|
||||
demonym
|
||||
required
|
||||
value={watch('nationality') || null}
|
||||
onChange={(val) => setValue('nationality', val || '', { shouldValidate: true })}
|
||||
|
||||
@@ -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 ? <Flag style={{ width: 22, borderRadius: 2, display: 'block' }} /> : 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 (
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user