diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx
index 9d69d210..ae05be43 100644
--- a/components/contacts/contact-detail.tsx
+++ b/components/contacts/contact-detail.tsx
@@ -351,13 +351,16 @@ export function ContactDetail({ contact, onEdit, onDelete, isMobile, className }
)}
- {contact.gender && (contact.gender.sex || contact.gender.identity) && (
+ {contact.speakToAs && (contact.speakToAs.grammaticalGender || contact.speakToAs.pronouns) && (
- {contact.gender.sex && {t(`detail.gender_${contact.gender.sex.toUpperCase()}`, { defaultValue: contact.gender.sex })}}
- {contact.gender.identity && (
- {contact.gender.sex ? " — " : ""}{contact.gender.identity}
- )}
+ {contact.speakToAs.grammaticalGender && {t(`detail.gender_${contact.speakToAs.grammaticalGender}`, { defaultValue: contact.speakToAs.grammaticalGender })}}
+ {contact.speakToAs.pronouns && (() => {
+ const firstPronoun = Object.values(contact.speakToAs!.pronouns!)[0]?.pronouns;
+ return firstPronoun ? (
+ {contact.speakToAs!.grammaticalGender ? " — " : ""}{firstPronoun}
+ ) : null;
+ })()}
)}
diff --git a/components/contacts/contact-form.tsx b/components/contacts/contact-form.tsx
index 657be3a2..7ef14167 100644
--- a/components/contacts/contact-form.tsx
+++ b/components/contacts/contact-form.tsx
@@ -235,8 +235,10 @@ export function ContactForm({ contact, onSave, onCancel }: ContactFormProps) {
contact?.notes ? Object.values(contact.notes)[0]?.note || "" : ""
);
- const [genderSex, setGenderSex] = useState(contact?.gender?.sex || "");
- const [genderIdentity, setGenderIdentity] = useState(contact?.gender?.identity || "");
+ const [genderSex, setGenderSex] = useState(contact?.speakToAs?.grammaticalGender || "");
+ const [genderIdentity, setGenderIdentity] = useState(
+ contact?.speakToAs?.pronouns ? Object.values(contact.speakToAs.pronouns)[0]?.pronouns || "" : ""
+ );
const [calendarUri, setCalendarUri] = useState(contact?.calendarUri || "");
const [schedulingUri, setSchedulingUri] = useState(contact?.schedulingUri || "");
const [freeBusyUri, setFreeBusyUri] = useState(contact?.freeBusyUri || "");
@@ -371,8 +373,11 @@ export function ContactForm({ contact, onSave, onCancel }: ContactFormProps) {
notes: note.trim()
? { n0: { note: note.trim() } }
: undefined,
- gender: (genderSex.trim() || genderIdentity.trim())
- ? { sex: genderSex.trim() || undefined, identity: genderIdentity.trim() || undefined }
+ speakToAs: (genderSex.trim() || genderIdentity.trim())
+ ? {
+ grammaticalGender: genderSex.trim() || undefined,
+ pronouns: genderIdentity.trim() ? { p0: { pronouns: genderIdentity.trim() } } : undefined,
+ }
: undefined,
calendarUri: calendarUri.trim() || undefined,
schedulingUri: schedulingUri.trim() || undefined,
@@ -737,11 +742,11 @@ export function ContactForm({ contact, onSave, onCancel }: ContactFormProps) {
diff --git a/lib/__tests__/vcard.test.ts b/lib/__tests__/vcard.test.ts
index acca5283..ccff3dc7 100644
--- a/lib/__tests__/vcard.test.ts
+++ b/lib/__tests__/vcard.test.ts
@@ -226,7 +226,7 @@ describe("parseVCard", () => {
expect(result).toHaveLength(1);
const card = result[0];
- expect(card.gender).toEqual({ sex: "F", identity: "Female" });
+ expect(card.speakToAs).toEqual({ grammaticalGender: "feminine", pronouns: { p0: { pronouns: "Female" } } });
expect(card.media?.m0).toEqual({
kind: "logo",
uri: "https://example.com/logo.png",
@@ -331,7 +331,7 @@ describe("generateVCard", () => {
components: [{ kind: "given", value: "Jane" }],
isOrdered: true,
},
- gender: { sex: "F", identity: "Female" },
+ speakToAs: { grammaticalGender: "feminine", pronouns: { p0: { pronouns: "Female" } } },
media: {
m0: { kind: "logo", uri: "https://example.com/logo.png", mediaType: "image/png" },
m1: { kind: "sound", uri: "https://example.com/sound.ogg", mediaType: "audio/ogg" },
diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts
index e7457149..c6748c21 100644
--- a/lib/jmap/types.ts
+++ b/lib/jmap/types.ts
@@ -183,7 +183,10 @@ export interface ContactCard {
relatedTo?: Record;
keywords?: Record;
members?: Record;
- gender?: { sex?: string; identity?: string };
+ speakToAs?: {
+ grammaticalGender?: string;
+ pronouns?: Record }>;
+ };
calendarUri?: string;
schedulingUri?: string;
freeBusyUri?: string;
diff --git a/lib/vcard.ts b/lib/vcard.ts
index 6d0be68f..8fbc819f 100644
--- a/lib/vcard.ts
+++ b/lib/vcard.ts
@@ -1,5 +1,29 @@
import type { ContactCard, NameComponent, ContactMedia, ContactOnlineService } from "@/lib/jmap/types";
+const VCARD_SEX_TO_GENDER: Record = {
+ M: "masculine",
+ F: "feminine",
+ O: "other",
+ N: "none",
+ U: "unknown",
+};
+
+const GENDER_TO_VCARD_SEX: Record = {
+ masculine: "M",
+ feminine: "F",
+ other: "O",
+ none: "N",
+ unknown: "U",
+};
+
+function vcardSexToGrammaticalGender(sex: string): string {
+ return VCARD_SEX_TO_GENDER[sex.toUpperCase()] || sex.toLowerCase();
+}
+
+function grammaticalGenderToVcardSex(gender: string): string {
+ return GENDER_TO_VCARD_SEX[gender.toLowerCase()] || "";
+}
+
function unfoldLines(vcf: string): string {
return vcf.replace(/\r\n[ \t]/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
@@ -390,9 +414,17 @@ function buildContact(raw: Record): ContactCard | null {
case "GENDER": {
const gParts = val.split(";");
- card.gender = {};
- if (gParts[0]) card.gender.sex = gParts[0];
- if (gParts[1]) card.gender.identity = gParts[1];
+ const sexCode = gParts[0]?.toUpperCase();
+ const identityText = gParts[1];
+ if (sexCode || identityText) {
+ card.speakToAs = {};
+ if (sexCode) {
+ card.speakToAs.grammaticalGender = vcardSexToGrammaticalGender(sexCode);
+ }
+ if (identityText) {
+ card.speakToAs.pronouns = { p0: { pronouns: identityText } };
+ }
+ }
break;
}
@@ -684,10 +716,15 @@ function generateSingleVCard(contact: ContactCard): string {
}
}
- if (contact.gender) {
- const sex = contact.gender.sex || "";
- const identity = contact.gender.identity || "";
- lines.push(`GENDER:${sex}${identity ? `;${identity}` : ""}`);
+ if (contact.speakToAs) {
+ const sex = contact.speakToAs.grammaticalGender
+ ? grammaticalGenderToVcardSex(contact.speakToAs.grammaticalGender)
+ : "";
+ const pronouns = contact.speakToAs.pronouns;
+ const identity = pronouns ? Object.values(pronouns)[0]?.pronouns || "" : "";
+ if (sex || identity) {
+ lines.push(`GENDER:${sex}${identity ? `;${identity}` : ""}`);
+ }
}
if (contact.calendarUri) {
diff --git a/locales/de/common.json b/locales/de/common.json
index 0734ead9..2e1d1da0 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Interesse",
"personal_other": "Sonstiges",
"gender": "Geschlecht",
- "gender_M": "Männlich",
- "gender_F": "Weiblich",
- "gender_O": "Andere",
- "gender_N": "Nicht zutreffend",
- "gender_U": "Unbekannt",
+ "gender_masculine": "Männlich",
+ "gender_feminine": "Weiblich",
+ "gender_other": "Andere",
+ "gender_none": "Nicht zutreffend",
+ "gender_unknown": "Unbekannt",
"calendar": "Kalender",
"calendar_uri": "Kalender-URL",
"scheduling_uri": "Terminplanungs-URL",
diff --git a/locales/en/common.json b/locales/en/common.json
index cdede18f..4bc6ebb2 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -1505,11 +1505,11 @@
"personal_interest": "Interest",
"personal_other": "Other",
"gender": "Gender",
- "gender_M": "Male",
- "gender_F": "Female",
- "gender_O": "Other",
- "gender_N": "Not applicable",
- "gender_U": "Unknown",
+ "gender_masculine": "Male",
+ "gender_feminine": "Female",
+ "gender_other": "Other",
+ "gender_none": "Not applicable",
+ "gender_unknown": "Unknown",
"calendar": "Calendar",
"calendar_uri": "Calendar URL",
"scheduling_uri": "Scheduling URL",
diff --git a/locales/es/common.json b/locales/es/common.json
index 0213bb17..60051267 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Interés",
"personal_other": "Otro",
"gender": "Género",
- "gender_M": "Masculino",
- "gender_F": "Femenino",
- "gender_O": "Otro",
- "gender_N": "No aplicable",
- "gender_U": "Desconocido",
+ "gender_masculine": "Masculino",
+ "gender_feminine": "Femenino",
+ "gender_other": "Otro",
+ "gender_none": "No aplicable",
+ "gender_unknown": "Desconocido",
"calendar": "Calendario",
"calendar_uri": "URL del calendario",
"scheduling_uri": "URL de programación",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 51cfc93b..8419df01 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Intérêt",
"personal_other": "Autre",
"gender": "Genre",
- "gender_M": "Masculin",
- "gender_F": "Féminin",
- "gender_O": "Autre",
- "gender_N": "Non applicable",
- "gender_U": "Inconnu",
+ "gender_masculine": "Masculin",
+ "gender_feminine": "Féminin",
+ "gender_other": "Autre",
+ "gender_none": "Non applicable",
+ "gender_unknown": "Inconnu",
"calendar": "Calendrier",
"calendar_uri": "URL du calendrier",
"scheduling_uri": "URL de planification",
diff --git a/locales/it/common.json b/locales/it/common.json
index 010eec90..93d29fd7 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Interesse",
"personal_other": "Altro",
"gender": "Genere",
- "gender_M": "Maschile",
- "gender_F": "Femminile",
- "gender_O": "Altro",
- "gender_N": "Non applicabile",
- "gender_U": "Sconosciuto",
+ "gender_masculine": "Maschile",
+ "gender_feminine": "Femminile",
+ "gender_other": "Altro",
+ "gender_none": "Non applicabile",
+ "gender_unknown": "Sconosciuto",
"calendar": "Calendario",
"calendar_uri": "URL del calendario",
"scheduling_uri": "URL di pianificazione",
diff --git a/locales/ja/common.json b/locales/ja/common.json
index 7d1fd31d..7be34e16 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "興味",
"personal_other": "その他",
"gender": "性別",
- "gender_M": "男性",
- "gender_F": "女性",
- "gender_O": "その他",
- "gender_N": "該当なし",
- "gender_U": "不明",
+ "gender_masculine": "男性",
+ "gender_feminine": "女性",
+ "gender_other": "その他",
+ "gender_none": "該当なし",
+ "gender_unknown": "不明",
"calendar": "カレンダー",
"calendar_uri": "カレンダーURL",
"scheduling_uri": "スケジュールURL",
diff --git a/locales/nl/common.json b/locales/nl/common.json
index 775bf993..801877f0 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Interesse",
"personal_other": "Overig",
"gender": "Geslacht",
- "gender_M": "Man",
- "gender_F": "Vrouw",
- "gender_O": "Anders",
- "gender_N": "Niet van toepassing",
- "gender_U": "Onbekend",
+ "gender_masculine": "Man",
+ "gender_feminine": "Vrouw",
+ "gender_other": "Anders",
+ "gender_none": "Niet van toepassing",
+ "gender_unknown": "Onbekend",
"calendar": "Kalender",
"calendar_uri": "Kalender-URL",
"scheduling_uri": "Planning-URL",
diff --git a/locales/pt/common.json b/locales/pt/common.json
index accd419a..bb0d6af2 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -1491,11 +1491,11 @@
"personal_interest": "Interesse",
"personal_other": "Outro",
"gender": "Gênero",
- "gender_M": "Masculino",
- "gender_F": "Feminino",
- "gender_O": "Outro",
- "gender_N": "Não aplicável",
- "gender_U": "Desconhecido",
+ "gender_masculine": "Masculino",
+ "gender_feminine": "Feminino",
+ "gender_other": "Outro",
+ "gender_none": "Não aplicável",
+ "gender_unknown": "Desconhecido",
"calendar": "Calendário",
"calendar_uri": "URL do calendário",
"scheduling_uri": "URL de agendamento",