fix: call a voice number Phone and a private entry Home on contacts

The contact-detail test now expects the new label key for private, since the wording moved to the ux_pim_contact namespace.
This commit is contained in:
Linus Rath
2026-11-04 10:55:22 +01:00
parent d783981373
commit 6cd05da510
34 changed files with 179 additions and 10 deletions
@@ -68,7 +68,7 @@ describe('ContactDetail', () => {
render(<ContactDetail contact={typed} onEdit={vi.fn()} onDelete={vi.fn()} />);
// The test translator returns the key, so a translated label shows up as its key.
expect(screen.getByText('form.context_work')).toBeInTheDocument();
expect(screen.getByText('form.context_private · form.phone_cell, x-sat')).toBeInTheDocument();
expect(screen.getByText('ux_pim_contact.context_private · form.phone_cell, x-sat')).toBeInTheDocument();
// Custom vCard types are shown as stored.
expect(screen.getByText('form.context_work, x-hq')).toBeInTheDocument();
});
@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { ContactCard } from '@/lib/jmap/types';
// Keys with their namespace, so a label read from ux_pim_contact can be told
// from the old one in contacts.form.
vi.mock('next-intl', () => ({
useTranslations: (ns?: string) => (key: string) => (ns ? `${ns}.${key}` : key),
useLocale: () => 'en',
useFormatter: () => ({ dateTime: String, relativeTime: String, number: String }),
}));
vi.mock('@/stores/account-store', () => {
const state = { accounts: [], activeAccountId: null };
const hook = (sel?: (s: typeof state) => unknown) => (typeof sel === 'function' ? sel(state) : state);
hook.getState = () => state;
return { useAccountStore: hook };
});
vi.mock('@/components/contacts/contact-activity', () => ({ ContactActivity: () => null }));
import { ContactForm } from '../contact-form';
import { ContactDetail } from '../contact-detail';
const anna: ContactCard = {
id: 'c1',
addressBookIds: {},
name: { components: [{ kind: 'given', value: 'Anna' }, { kind: 'surname', value: 'Example' }], isOrdered: true },
emails: { e0: { address: 'anna@example.net', contexts: { private: true } } },
phones: { p0: { number: '+49 30 1', features: { voice: true }, contexts: { private: true } } },
};
describe('pim-19: standard words read as people say them', () => {
it('offers "Phone" and "Home" in the editor, storing voice and private as before', () => {
render(<ContactForm contact={anna} onSave={vi.fn()} onCancel={vi.fn()} />);
const phoneType = screen.getByDisplayValue('ux_pim_contact.phone_voice') as HTMLSelectElement;
expect(phoneType.value).toBe('voice');
expect(screen.getAllByDisplayValue('ux_pim_contact.context_private').map(s => (s as HTMLSelectElement).value)).toEqual(['private', 'private']);
expect(screen.queryByText('contacts.form.phone_voice')).toBeNull();
expect(screen.queryByText('contacts.form.context_private')).toBeNull();
});
it('labels a voice phone "Phone" and a private one "Home" on the contact page', () => {
render(<ContactDetail contact={anna} onEdit={vi.fn()} onDelete={vi.fn()} />);
expect(screen.getByText('ux_pim_contact.context_private · ux_pim_contact.phone_voice')).toBeInTheDocument();
});
});
+8 -5
View File
@@ -40,14 +40,15 @@ interface ContactDetailProps {
}
// Contexts and phone features the contact form offers, shown with the same
// labels here. Anything else (custom vCard types) is shown as stored.
// labels here. Anything else (custom vCard types) is shown as stored. The
// standard's "private" reads "Home" and "voice" reads "Phone" (#pim-19).
const CONTEXT_LABEL_KEYS: Record<string, string> = {
work: "form.context_work",
private: "form.context_private",
private: "ux_pim_contact.context_private",
};
const PHONE_FEATURE_LABEL_KEYS: Record<string, string> = {
voice: "form.phone_voice",
voice: "ux_pim_contact.phone_voice",
cell: "form.phone_cell",
mobile: "form.phone_cell",
fax: "form.phone_fax",
@@ -78,12 +79,14 @@ function getCompletedYears(dateInput: AnniversaryDate): number | null {
export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDuplicate, onMoveTo, onCompose, onPrint, isMobile, className, readOnly = false }: ContactDetailProps) {
const t = useTranslations("contacts");
const tRoot = useTranslations();
const tCommon = useTranslations("common");
const locale = useLocale();
// The ⋮ shows the contact's menu, the one right-click shows in the list (R6).
const moreMenu = useContextMenu<ContactCard>();
const contextLabel = (key: string) => (CONTEXT_LABEL_KEYS[key] ? t(CONTEXT_LABEL_KEYS[key]) : key);
const featureLabel = (key: string) => (PHONE_FEATURE_LABEL_KEYS[key] ? t(PHONE_FEATURE_LABEL_KEYS[key]) : key);
const label = (labelKey: string) => (labelKey.startsWith("ux_pim_contact.") ? tRoot(labelKey) : t(labelKey));
const contextLabel = (key: string) => (CONTEXT_LABEL_KEYS[key] ? label(CONTEXT_LABEL_KEYS[key]) : key);
const featureLabel = (key: string) => (PHONE_FEATURE_LABEL_KEYS[key] ? label(PHONE_FEATURE_LABEL_KEYS[key]) : key);
const cryptoKeys = contact?.cryptoKeys ? Object.values(contact.cryptoKeys) : [];
const hasCryptoKeysSlotOffers = usePluginSlotOffers('contact-cryptokeys').length > 0;
+5 -4
View File
@@ -220,6 +220,7 @@ function Select({ value, onChange, children, className, id, "aria-label": ariaLa
export function ContactForm({ contact, addressBooks, allKeywords, defaultAddressBookId, prefill, onSave, onCancel, loadCurrentMedia, onDirtyChange }: ContactFormProps) {
const t = useTranslations("contacts.form");
const tx = useTranslations("ux_pim_contact");
const isEditing = !!contact;
// The header's close goes to its start when the window's own close is at its end.
const [header, setHeader] = useState<HTMLDivElement | null>(null);
@@ -916,7 +917,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
>
<option value="">-</option>
<option value="work">{t("context_work")}</option>
<option value="private">{t("context_private")}</option>
<option value="private">{tx("context_private")}</option>
</Select>
{emails.length > 1 && (
<IconButton label={t("remove_entry")} onClick={() => setEmails(emails.filter((_, j) => j !== i))} className="shrink-0">
@@ -966,7 +967,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
aria-label={t("phone_type")}
>
<option value="">{t("phone_type")}</option>
<option value="voice">{t("phone_voice")}</option>
<option value="voice">{tx("phone_voice")}</option>
<option value="mobile">{t("phone_cell")}</option>
<option value="fax">{t("phone_fax")}</option>
<option value="pager">{t("phone_pager")}</option>
@@ -984,7 +985,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
>
<option value="">-</option>
<option value="work">{t("context_work")}</option>
<option value="private">{t("context_private")}</option>
<option value="private">{tx("context_private")}</option>
</Select>
<IconButton label={t("remove_entry")} onClick={() => setPhones(phones.filter((_, j) => j !== i))} className="shrink-0">
<X />
@@ -1046,7 +1047,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
>
<option value="">-</option>
<option value="work">{t("context_work")}</option>
<option value="private">{t("context_private")}</option>
<option value="private">{tx("context_private")}</option>
</Select>
</div>
</div>
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "فتح قائمة جهة الاتصال"
}
},
"ux_pim_contact": {
"context_private": "المنزل",
"phone_voice": "هاتف"
},
"uxs_wording": {
"more_options": "خيارات إضافية",
"use_in_other_apps": "الاستخدام في تطبيقات أخرى",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Obre el menú del contacte"
}
},
"ux_pim_contact": {
"context_private": "Casa",
"phone_voice": "Telèfon"
},
"uxs_wording": {
"more_options": "Més opcions",
"use_in_other_apps": "Fes-ho servir en altres aplicacions",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Otevřít kategorie",
"row_actions": "Akce pro {name}"
},
"ux_pim_contact": {
"context_private": "Domácí",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Další možnosti",
"use_in_other_apps": "Použít v jiných aplikacích",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Åbn kategorier",
"row_actions": "Handlinger for {name}"
},
"ux_pim_contact": {
"context_private": "Hjem",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Flere indstillinger",
"use_in_other_apps": "Brug i andre apps",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Kategorien öffnen",
"row_actions": "Aktionen für {name}"
},
"ux_pim_contact": {
"context_private": "Privat",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Weitere Optionen",
"use_in_other_apps": "In anderen Apps nutzen",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Open the contact's menu"
}
},
"ux_pim_contact": {
"context_private": "Home",
"phone_voice": "Phone"
},
"uxs_wording": {
"more_options": "More options",
"use_in_other_apps": "Use in other apps",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Abrir categorías",
"row_actions": "Acciones para {name}"
},
"ux_pim_contact": {
"context_private": "Casa",
"phone_voice": "Teléfono"
},
"uxs_wording": {
"more_options": "Más opciones",
"use_in_other_apps": "Usar en otras aplicaciones",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Ava kontakti menüü"
}
},
"ux_pim_contact": {
"context_private": "Kodu",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Rohkem valikuid",
"use_in_other_apps": "Kasuta teistes rakendustes",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "باز کردن منوی مخاطب"
}
},
"ux_pim_contact": {
"context_private": "منزل",
"phone_voice": "تلفن"
},
"uxs_wording": {
"more_options": "گزینه‌های بیشتر",
"use_in_other_apps": "استفاده در برنامه‌های دیگر",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Ouvrir les catégories",
"row_actions": "Actions pour {name}"
},
"ux_pim_contact": {
"context_private": "Domicile",
"phone_voice": "Téléphone"
},
"uxs_wording": {
"more_options": "Plus d'options",
"use_in_other_apps": "Utiliser dans d'autres applications",
+4
View File
@@ -4376,6 +4376,10 @@
"menu": "פתח את התפריט של איש הקשר"
}
},
"ux_pim_contact": {
"context_private": "בית",
"phone_voice": "טלפון"
},
"uxs_wording": {
"more_options": "אפשרויות נוספות",
"use_in_other_apps": "שימוש באפליקציות אחרות",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Névjegy menüjének megnyitása"
}
},
"ux_pim_contact": {
"context_private": "Otthoni",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "További beállítások",
"use_in_other_apps": "Használat más alkalmazásokban",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Apri categorie",
"row_actions": "Azioni per {name}"
},
"ux_pim_contact": {
"context_private": "Casa",
"phone_voice": "Telefono"
},
"uxs_wording": {
"more_options": "Altre opzioni",
"use_in_other_apps": "Usa in altre app",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "カテゴリを開く",
"row_actions": "{name} の操作"
},
"ux_pim_contact": {
"context_private": "自宅",
"phone_voice": "電話"
},
"uxs_wording": {
"more_options": "その他のオプション",
"use_in_other_apps": "ほかのアプリで使う",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "카테고리 열기",
"row_actions": "{name} 작업"
},
"ux_pim_contact": {
"context_private": "집",
"phone_voice": "전화"
},
"uxs_wording": {
"more_options": "옵션 더 보기",
"use_in_other_apps": "다른 앱에서 사용",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Atvērt kategorijas",
"row_actions": "Darbības: {name}"
},
"ux_pim_contact": {
"context_private": "Mājas",
"phone_voice": "Tālrunis"
},
"uxs_wording": {
"more_options": "Vairāk iespēju",
"use_in_other_apps": "Lietot citās lietotnēs",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Харилцагчийн цэсийг нээх"
}
},
"ux_pim_contact": {
"context_private": "Гэр",
"phone_voice": "Утас"
},
"uxs_wording": {
"more_options": "Бусад сонголт",
"use_in_other_apps": "Бусад апп-д ашиглах",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Åpne kontaktens meny"
}
},
"ux_pim_contact": {
"context_private": "Hjem",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Flere valg",
"use_in_other_apps": "Bruk i andre apper",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Categorieën openen",
"row_actions": "Acties voor {name}"
},
"ux_pim_contact": {
"context_private": "Thuis",
"phone_voice": "Telefoon"
},
"uxs_wording": {
"more_options": "Meer opties",
"use_in_other_apps": "Gebruiken in andere apps",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Otwórz kategorie",
"row_actions": "Działania dla {name}"
},
"ux_pim_contact": {
"context_private": "Dom",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Więcej opcji",
"use_in_other_apps": "Użyj w innych aplikacjach",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Abrir categorias",
"row_actions": "Ações para {name}"
},
"ux_pim_contact": {
"context_private": "Casa",
"phone_voice": "Telefone"
},
"uxs_wording": {
"more_options": "Mais opções",
"use_in_other_apps": "Usar noutras aplicações",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Deschideți meniul contactului"
}
},
"ux_pim_contact": {
"context_private": "Acasă",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Mai multe opțiuni",
"use_in_other_apps": "Folosește în alte aplicații",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Открыть категории",
"row_actions": "Действия для {name}"
},
"ux_pim_contact": {
"context_private": "Домашний",
"phone_voice": "Телефон"
},
"uxs_wording": {
"more_options": "Другие параметры",
"use_in_other_apps": "Использовать в других приложениях",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "Otvoriť ponuku kontaktu"
}
},
"ux_pim_contact": {
"context_private": "Domov",
"phone_voice": "Telefón"
},
"uxs_wording": {
"more_options": "Ďalšie možnosti",
"use_in_other_apps": "Použiť v iných aplikáciách",
+4
View File
@@ -3821,6 +3821,10 @@
"menu": "เปิดเมนูของผู้ติดต่อ"
}
},
"ux_pim_contact": {
"context_private": "บ้าน",
"phone_voice": "โทรศัพท์"
},
"uxs_wording": {
"more_options": "ตัวเลือกเพิ่มเติม",
"use_in_other_apps": "ใช้ในแอปอื่น",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Kategorileri aç",
"row_actions": "{name} için işlemler"
},
"ux_pim_contact": {
"context_private": "Ev",
"phone_voice": "Telefon"
},
"uxs_wording": {
"more_options": "Diğer seçenekler",
"use_in_other_apps": "Başka uygulamalarda kullan",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "Відкрити категорії",
"row_actions": "Дії для {name}"
},
"ux_pim_contact": {
"context_private": "Домашній",
"phone_voice": "Телефон"
},
"uxs_wording": {
"more_options": "Інші параметри",
"use_in_other_apps": "Використовувати в інших застосунках",
+4
View File
@@ -4495,6 +4495,10 @@
"menu": "Mở menu của liên hệ"
}
},
"ux_pim_contact": {
"context_private": "Nhà riêng",
"phone_voice": "Điện thoại"
},
"uxs_wording": {
"more_options": "Tùy chọn khác",
"use_in_other_apps": "Dùng trong ứng dụng khác",
+4
View File
@@ -4483,6 +4483,10 @@
"menu": "開啟聯絡人選單"
}
},
"ux_pim_contact": {
"context_private": "住家",
"phone_voice": "電話"
},
"uxs_wording": {
"more_options": "更多選項",
"use_in_other_apps": "在其他應用程式中使用",
+4
View File
@@ -4483,6 +4483,10 @@
"open_categories": "打开分类",
"row_actions": "{name} 的操作"
},
"ux_pim_contact": {
"context_private": "住宅",
"phone_voice": "电话"
},
"uxs_wording": {
"more_options": "更多选项",
"use_in_other_apps": "在其他应用中使用",