feat: allow contact cards for organizations #701
This commit is contained in:
@@ -102,4 +102,98 @@ describe('ContactForm', () => {
|
|||||||
expect.arrayContaining([expect.objectContaining({ kind: 'given', value: 'Jane' })])
|
expect.arrayContaining([expect.objectContaining({ kind: 'given', value: 'Jane' })])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('saves an organization-only card when the organization type is selected', async () => {
|
||||||
|
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||||
|
render(<ContactForm onSave={onSave} onCancel={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('type_organization'));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('organization_placeholder'), { target: { value: 'Acme Corp' } });
|
||||||
|
fireEvent.submit(screen.getByText('save').closest('form')!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onSave).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedData = onSave.mock.calls[0][0];
|
||||||
|
expect(savedData.kind).toBe('org');
|
||||||
|
expect(savedData.organizations.o0.name).toBe('Acme Corp');
|
||||||
|
// No personal name components; the org name carries the display name instead.
|
||||||
|
expect(savedData.name.components).toBeUndefined();
|
||||||
|
expect(savedData.name.full).toBe('Acme Corp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides the personal name fields in organization mode', () => {
|
||||||
|
render(<ContactForm onSave={vi.fn()} onCancel={vi.fn()} />);
|
||||||
|
expect(screen.getByPlaceholderText('given_name')).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('type_organization'));
|
||||||
|
|
||||||
|
expect(screen.queryByPlaceholderText('given_name')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByPlaceholderText('surname')).not.toBeInTheDocument();
|
||||||
|
// The organization field moves into the identity section, so it appears once.
|
||||||
|
expect(screen.getAllByPlaceholderText('organization_placeholder')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still requires a name in organization mode', async () => {
|
||||||
|
const onSave = vi.fn();
|
||||||
|
render(<ContactForm onSave={onSave} onCancel={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('type_organization'));
|
||||||
|
fireEvent.submit(screen.getByText('save').closest('form')!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('name_required')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(onSave).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts an organization instead of a personal name in person mode', async () => {
|
||||||
|
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||||
|
render(<ContactForm onSave={onSave} onCancel={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('section_work'));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('organization_placeholder'), { target: { value: 'Acme Corp' } });
|
||||||
|
fireEvent.submit(screen.getByText('save').closest('form')!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onSave).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
expect(onSave.mock.calls[0][0].name.full).toBe('Acme Corp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens an existing org card in organization mode', () => {
|
||||||
|
const orgContact: ContactCard = {
|
||||||
|
id: '2',
|
||||||
|
addressBookIds: {},
|
||||||
|
kind: 'org',
|
||||||
|
name: { full: 'Acme Corp' },
|
||||||
|
organizations: { o0: { name: 'Acme Corp' } },
|
||||||
|
};
|
||||||
|
render(<ContactForm contact={orgContact} onSave={vi.fn()} onCancel={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(screen.queryByPlaceholderText('given_name')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByDisplayValue('Acme Corp')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches an org card back to a person', async () => {
|
||||||
|
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const orgContact: ContactCard = {
|
||||||
|
id: '2',
|
||||||
|
addressBookIds: {},
|
||||||
|
kind: 'org',
|
||||||
|
name: { full: 'Acme Corp' },
|
||||||
|
organizations: { o0: { name: 'Acme Corp' } },
|
||||||
|
};
|
||||||
|
render(<ContactForm contact={orgContact} onSave={onSave} onCancel={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('type_person'));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('given_name'), { target: { value: 'Jane' } });
|
||||||
|
fireEvent.submit(screen.getByText('save').closest('form')!);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onSave).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
expect(onSave.mock.calls[0][0].kind).toBe('individual');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -171,7 +171,9 @@ export function ContactDetail({ contact, onEdit, onDelete, onAddToGroup, onDupli
|
|||||||
|
|
||||||
const hasNickname = nicknames.length > 0;
|
const hasNickname = nicknames.length > 0;
|
||||||
const titleLine = jobTitles.length > 0 ? jobTitles.map(t => t.name).join(", ") : undefined;
|
const titleLine = jobTitles.length > 0 ? jobTitles.map(t => t.name).join(", ") : undefined;
|
||||||
const subtitleParts = [titleLine, orgs[0]?.name].filter(Boolean) as string[];
|
// On an organization card the org name is already the heading; don't repeat it.
|
||||||
|
const orgName = orgs[0]?.name;
|
||||||
|
const subtitleParts = [titleLine, orgName === name ? undefined : orgName].filter(Boolean) as string[];
|
||||||
const hasContactDetails = emails.length > 0 || phones.length > 0 || addresses.length > 0 || onlineServices.length > 0;
|
const hasContactDetails = emails.length > 0 || phones.length > 0 || addresses.length > 0 || onlineServices.length > 0;
|
||||||
const hasWork = titles.length > 0 || orgs.length > 0;
|
const hasWork = titles.length > 0 || orgs.length > 0;
|
||||||
const hasGender = !!(contact.speakToAs && (contact.speakToAs.grammaticalGender || contact.speakToAs.pronouns));
|
const hasGender = !!(contact.speakToAs && (contact.speakToAs.grammaticalGender || contact.speakToAs.pronouns));
|
||||||
|
|||||||
@@ -266,6 +266,16 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
contact?.organizations ? (Object.values(contact.organizations)[0]?.units?.[0]?.name || "") : ""
|
contact?.organizations ? (Object.values(contact.organizations)[0]?.units?.[0]?.name || "") : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// A card may describe an organization instead of a person (RFC 9553 kind "org").
|
||||||
|
// Older cards predate the explicit kind, so fall back to "has an org name but no
|
||||||
|
// personal name".
|
||||||
|
const [isOrg, setIsOrg] = useState(() => {
|
||||||
|
if (!contact) return false;
|
||||||
|
if (contact.kind) return contact.kind === "org";
|
||||||
|
const hasPersonName = !!(findComponent("given") || findComponent("surname"));
|
||||||
|
return !hasPersonName && !!Object.values(contact.organizations || {})[0]?.name;
|
||||||
|
});
|
||||||
|
|
||||||
const [jobTitle, setJobTitle] = useState(() => {
|
const [jobTitle, setJobTitle] = useState(() => {
|
||||||
if (contact?.titles) {
|
if (contact?.titles) {
|
||||||
const t = Object.values(contact.titles).find(t => t.kind !== "role");
|
const t = Object.values(contact.titles).find(t => t.kind !== "role");
|
||||||
@@ -424,7 +434,10 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
if (!givenName.trim() && !surname.trim()) {
|
// An organization name identifies the card just as well as a personal name.
|
||||||
|
const orgName = organization.trim();
|
||||||
|
const hasPersonName = !!(givenName.trim() || surname.trim());
|
||||||
|
if (isOrg ? !orgName : (!hasPersonName && !orgName)) {
|
||||||
setError(t("name_required"));
|
setError(t("name_required"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -461,11 +474,19 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
|
|
||||||
// Emit JSContact-standard kinds (RFC 9553) so the JMAP server stores them losslessly.
|
// Emit JSContact-standard kinds (RFC 9553) so the JMAP server stores them losslessly.
|
||||||
const nameComponents = [];
|
const nameComponents = [];
|
||||||
if (prefix.trim()) nameComponents.push({ kind: "title" as const, value: prefix.trim() });
|
if (!isOrg) {
|
||||||
if (givenName.trim()) nameComponents.push({ kind: "given" as const, value: givenName.trim() });
|
if (prefix.trim()) nameComponents.push({ kind: "title" as const, value: prefix.trim() });
|
||||||
if (additionalName.trim()) nameComponents.push({ kind: "given2" as const, value: additionalName.trim() });
|
if (givenName.trim()) nameComponents.push({ kind: "given" as const, value: givenName.trim() });
|
||||||
if (surname.trim()) nameComponents.push({ kind: "surname" as const, value: surname.trim() });
|
if (additionalName.trim()) nameComponents.push({ kind: "given2" as const, value: additionalName.trim() });
|
||||||
if (suffix.trim()) nameComponents.push({ kind: "generation" as const, value: suffix.trim() });
|
if (surname.trim()) nameComponents.push({ kind: "surname" as const, value: surname.trim() });
|
||||||
|
if (suffix.trim()) nameComponents.push({ kind: "generation" as const, value: suffix.trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without personal name components, carry the organization name in `name.full`
|
||||||
|
// so servers and other clients have something to display.
|
||||||
|
const nameValue: ContactCard["name"] = nameComponents.length > 0
|
||||||
|
? { components: nameComponents, isOrdered: true }
|
||||||
|
: { full: orgName };
|
||||||
|
|
||||||
const titlesMap: Record<string, { name: string; kind?: "title" | "role" }> = {};
|
const titlesMap: Record<string, { name: string; kind?: "title" | "role" }> = {};
|
||||||
if (jobTitle.trim()) titlesMap["t0"] = { name: jobTitle.trim(), kind: "title" };
|
if (jobTitle.trim()) titlesMap["t0"] = { name: jobTitle.trim(), kind: "title" };
|
||||||
@@ -530,14 +551,20 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
const mediaValue: Record<string, ContactMedia> | null | undefined =
|
const mediaValue: Record<string, ContactMedia> | null | undefined =
|
||||||
Object.keys(mediaMap).length > 0 ? mediaMap : (hadMedia ? null : undefined);
|
Object.keys(mediaMap).length > 0 ? mediaMap : (hadMedia ? null : undefined);
|
||||||
|
|
||||||
|
// Only send `kind` when this form owns the answer: switching a card between
|
||||||
|
// person and organization. Leave other kinds (group, location, ...) untouched.
|
||||||
|
const kindValue: ContactCard["kind"] | undefined =
|
||||||
|
isOrg ? "org" : (contact?.kind === "org" ? "individual" : undefined);
|
||||||
|
|
||||||
const data: Partial<ContactCard> = {
|
const data: Partial<ContactCard> = {
|
||||||
name: { components: nameComponents, isOrdered: true },
|
name: nameValue,
|
||||||
|
...(kindValue ? { kind: kindValue } : {}),
|
||||||
nicknames: nickname.trim() ? { n0: { name: nickname.trim() } } : undefined,
|
nicknames: nickname.trim() ? { n0: { name: nickname.trim() } } : undefined,
|
||||||
emails: Object.keys(emailsMap).length > 0 ? emailsMap : undefined,
|
emails: Object.keys(emailsMap).length > 0 ? emailsMap : undefined,
|
||||||
phones: Object.keys(phonesMap).length > 0 ? phonesMap : undefined,
|
phones: Object.keys(phonesMap).length > 0 ? phonesMap : undefined,
|
||||||
titles: Object.keys(titlesMap).length > 0 ? titlesMap : undefined,
|
titles: Object.keys(titlesMap).length > 0 ? titlesMap : undefined,
|
||||||
organizations: organization.trim()
|
organizations: orgName
|
||||||
? { o0: { name: organization.trim(), units: orgUnits } }
|
? { o0: { name: orgName, units: orgUnits } }
|
||||||
: undefined,
|
: undefined,
|
||||||
addresses: Object.keys(addressesMap).length > 0 ? addressesMap : undefined,
|
addresses: Object.keys(addressesMap).length > 0 ? addressesMap : undefined,
|
||||||
onlineServices: Object.keys(onlineServicesMap).length > 0 ? onlineServicesMap : undefined,
|
onlineServices: Object.keys(onlineServicesMap).length > 0 ? onlineServicesMap : undefined,
|
||||||
@@ -570,7 +597,7 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewName = [givenName, surname].filter(Boolean).join(" ").trim();
|
const previewName = (isOrg ? "" : [givenName, surname].filter(Boolean).join(" ").trim()) || organization.trim();
|
||||||
const previewEmail = emails.find(e => e.address.trim())?.address.trim() || "";
|
const previewEmail = emails.find(e => e.address.trim())?.address.trim() || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -661,38 +688,81 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FormSection icon={User} title={t("section_identity")}>
|
<FormSection icon={User} title={t("section_identity")}>
|
||||||
<div className="grid grid-cols-[auto_1fr_1fr_auto] gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<span className="text-xs text-muted-foreground">{t("contact_type")}</span>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("prefix")}</label>
|
<div role="radiogroup" aria-label={t("contact_type")} className="inline-flex gap-0.5 rounded-md border border-input p-0.5">
|
||||||
<Input value={prefix} onChange={(e) => setPrefix(e.target.value)} placeholder={t("prefix_placeholder")} className="w-20" />
|
{[
|
||||||
</div>
|
{ org: false, label: t("type_person"), icon: User },
|
||||||
<div>
|
{ org: true, label: t("type_organization"), icon: Building },
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">
|
].map(({ org, label, icon: Icon }) => (
|
||||||
{t("given_name")} <span className="text-red-500">*</span>
|
<button
|
||||||
</label>
|
key={label}
|
||||||
<Input value={givenName} onChange={(e) => setGivenName(e.target.value)} placeholder={t("given_name")} autoFocus />
|
type="button"
|
||||||
</div>
|
role="radio"
|
||||||
<div>
|
aria-checked={isOrg === org}
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">
|
onClick={() => setIsOrg(org)}
|
||||||
{t("surname")} <span className="text-red-500">*</span>
|
className={cn(
|
||||||
</label>
|
"flex items-center gap-1.5 px-2.5 py-1 text-xs rounded transition-colors",
|
||||||
<Input value={surname} onChange={(e) => setSurname(e.target.value)} placeholder={t("surname")} />
|
isOrg === org
|
||||||
</div>
|
? "bg-primary text-primary-foreground"
|
||||||
<div>
|
: "text-muted-foreground hover:text-foreground"
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("suffix")}</label>
|
)}
|
||||||
<Input value={suffix} onChange={(e) => setSuffix(e.target.value)} placeholder={t("suffix_placeholder")} className="w-20" />
|
>
|
||||||
|
<Icon className="w-3.5 h-3.5" />
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
{isOrg ? (
|
||||||
<div>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("middle_name")}</label>
|
<div>
|
||||||
<Input value={additionalName} onChange={(e) => setAdditionalName(e.target.value)} placeholder={t("middle_name")} />
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t("organization")} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input value={organization} onChange={(e) => setOrganization(e.target.value)} placeholder={t("organization_placeholder")} autoFocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">{t("nickname")}</label>
|
||||||
|
<Input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder={t("nickname_placeholder")} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
) : (
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("nickname")}</label>
|
<>
|
||||||
<Input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder={t("nickname_placeholder")} />
|
<div className="grid grid-cols-[auto_1fr_1fr_auto] gap-2">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<label className="text-xs text-muted-foreground mb-1 block">{t("prefix")}</label>
|
||||||
|
<Input value={prefix} onChange={(e) => setPrefix(e.target.value)} placeholder={t("prefix_placeholder")} className="w-20" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t("given_name")} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input value={givenName} onChange={(e) => setGivenName(e.target.value)} placeholder={t("given_name")} autoFocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
{t("surname")} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input value={surname} onChange={(e) => setSurname(e.target.value)} placeholder={t("surname")} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">{t("suffix")}</label>
|
||||||
|
<Input value={suffix} onChange={(e) => setSuffix(e.target.value)} placeholder={t("suffix_placeholder")} className="w-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">{t("middle_name")}</label>
|
||||||
|
<Input value={additionalName} onChange={(e) => setAdditionalName(e.target.value)} placeholder={t("middle_name")} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground mb-1 block">{t("nickname")}</label>
|
||||||
|
<Input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder={t("nickname_placeholder")} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
@@ -806,12 +876,15 @@ export function ContactForm({ contact, addressBooks, allKeywords, defaultAddress
|
|||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
{/* Work & Organization */}
|
{/* Work & Organization */}
|
||||||
<FormSection icon={Building} title={t("section_work")} collapsible defaultOpen={!!(organization || department || jobTitle || role)}>
|
<FormSection icon={Building} title={t("section_work")} collapsible defaultOpen={!!((organization && !isOrg) || department || jobTitle || role)}>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
<div>
|
{/* In organization mode the org name is the card's identity, edited above. */}
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("organization")}</label>
|
{!isOrg && (
|
||||||
<Input value={organization} onChange={(e) => setOrganization(e.target.value)} placeholder={t("organization_placeholder")} />
|
<div>
|
||||||
</div>
|
<label className="text-xs text-muted-foreground mb-1 block">{t("organization")}</label>
|
||||||
|
<Input value={organization} onChange={(e) => setOrganization(e.target.value)} placeholder={t("organization_placeholder")} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground mb-1 block">{t("department")}</label>
|
<label className="text-xs text-muted-foreground mb-1 block">{t("department")}</label>
|
||||||
<Input value={department} onChange={(e) => setDepartment(e.target.value)} placeholder={t("department_placeholder")} />
|
<Input value={department} onChange={(e) => setDepartment(e.target.value)} placeholder={t("department_placeholder")} />
|
||||||
|
|||||||
@@ -445,6 +445,48 @@ describe("generateVCard", () => {
|
|||||||
const vcf = generateVCard([contact]);
|
const vcf = generateVCard([contact]);
|
||||||
expect(vcf).toContain("NOTE:Has comma\\, semicolon\\; and newline\\nhere");
|
expect(vcf).toContain("NOTE:Has comma\\, semicolon\\; and newline\\nhere");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the organization name as FN for organization cards (issue #701)", () => {
|
||||||
|
const contact: ContactCard = {
|
||||||
|
id: "c4",
|
||||||
|
addressBookIds: {},
|
||||||
|
kind: "org",
|
||||||
|
name: { full: "Acme Corp" },
|
||||||
|
organizations: { o0: { name: "Acme Corp" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const vcf = generateVCard([contact]);
|
||||||
|
expect(vcf).toContain("KIND:org");
|
||||||
|
expect(vcf).toContain("FN:Acme Corp");
|
||||||
|
expect(vcf).toContain("ORG:Acme Corp");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to ORG for FN when the card has no name at all", () => {
|
||||||
|
const contact: ContactCard = {
|
||||||
|
id: "c5",
|
||||||
|
addressBookIds: {},
|
||||||
|
kind: "org",
|
||||||
|
organizations: { o0: { name: "Acme Corp" } },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(generateVCard([contact])).toContain("FN:Acme Corp");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("organization-only cards (issue #701)", () => {
|
||||||
|
it("keeps a vCard that has only an organization name", () => {
|
||||||
|
const parsed = parseVCard([
|
||||||
|
"BEGIN:VCARD",
|
||||||
|
"VERSION:4.0",
|
||||||
|
"KIND:org",
|
||||||
|
"ORG:Acme Corp",
|
||||||
|
"END:VCARD",
|
||||||
|
].join("\r\n"));
|
||||||
|
|
||||||
|
expect(parsed).toHaveLength(1);
|
||||||
|
expect(parsed[0].kind).toBe("org");
|
||||||
|
expect(parsed[0].organizations?.o0.name).toBe("Acme Corp");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("round-trip: parse → generate → parse", () => {
|
describe("round-trip: parse → generate → parse", () => {
|
||||||
|
|||||||
+8
-2
@@ -844,7 +844,9 @@ function buildContact(raw: Record<string, string[]>): ContactCard | null {
|
|||||||
|
|
||||||
const hasName = card.name && (card.name.components?.length ?? 0) > 0 || !!card.name?.full;
|
const hasName = card.name && (card.name.components?.length ?? 0) > 0 || !!card.name?.full;
|
||||||
const hasEmail = card.emails && Object.keys(card.emails).length > 0;
|
const hasEmail = card.emails && Object.keys(card.emails).length > 0;
|
||||||
if (!hasName && !hasEmail && card.kind !== "group") return null;
|
// An organization name identifies the card just as well as a personal name.
|
||||||
|
const hasOrg = !!Object.values(card.organizations || {})[0]?.name;
|
||||||
|
if (!hasName && !hasEmail && !hasOrg && card.kind !== "group") return null;
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
@@ -882,7 +884,11 @@ function generateSingleVCard(contact: ContactCard): string {
|
|||||||
const suffix = findKind("generation", "suffix");
|
const suffix = findKind("generation", "suffix");
|
||||||
const additional = findKind("given2", "additional", "middle");
|
const additional = findKind("given2", "additional", "middle");
|
||||||
|
|
||||||
const fn = [prefix, given, additional, surname, suffix].filter(Boolean).join(" ") || contact.name?.full || "";
|
// FN is mandatory in vCard, so fall back to the organization name for org cards.
|
||||||
|
const fn = [prefix, given, additional, surname, suffix].filter(Boolean).join(" ")
|
||||||
|
|| contact.name?.full
|
||||||
|
|| Object.values(contact.organizations || {})[0]?.name
|
||||||
|
|| "";
|
||||||
if (fn) {
|
if (fn) {
|
||||||
lines.push(`FN:${encodeValue(fn)}`);
|
lines.push(`FN:${encodeValue(fn)}`);
|
||||||
lines.push(`N:${encodeValue(surname)};${encodeValue(given)};${encodeValue(additional)};${encodeValue(prefix)};${encodeValue(suffix)}`);
|
lines.push(`N:${encodeValue(surname)};${encodeValue(given)};${encodeValue(additional)};${encodeValue(prefix)};${encodeValue(suffix)}`);
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "الدليل",
|
"section_address_book": "الدليل",
|
||||||
"select_address_book": "اختر دليلًا...",
|
"select_address_book": "اختر دليلًا...",
|
||||||
"section_identity": "الاسم والهوية",
|
"section_identity": "الاسم والهوية",
|
||||||
|
"contact_type": "نوع جهة الاتصال",
|
||||||
|
"type_person": "شخص",
|
||||||
|
"type_organization": "المؤسسة",
|
||||||
"section_work": "العمل والمؤسسة",
|
"section_work": "العمل والمؤسسة",
|
||||||
"prefix": "اللقب",
|
"prefix": "اللقب",
|
||||||
"prefix_placeholder": "د.، أ.، السيدة",
|
"prefix_placeholder": "د.، أ.، السيدة",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"creating": "جارٍ الإنشاء...",
|
"creating": "جارٍ الإنشاء...",
|
||||||
"updating": "جارٍ التحديث...",
|
"updating": "جارٍ التحديث...",
|
||||||
"name_required": "يلزم إدخال الاسم الأول أو اسم العائلة على الأقل",
|
"name_required": "أدخل اسمًا أول أو اسم عائلة أو مؤسسة",
|
||||||
"email_invalid": "يرجى إدخال عنوان بريد إلكتروني صالح",
|
"email_invalid": "يرجى إدخال عنوان بريد إلكتروني صالح",
|
||||||
"email_error_inline": "تنسيق البريد الإلكتروني غير صالح",
|
"email_error_inline": "تنسيق البريد الإلكتروني غير صالح",
|
||||||
"save_failed": "فشل حفظ جهة الاتصال",
|
"save_failed": "فشل حفظ جهة الاتصال",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "Directori",
|
"section_address_book": "Directori",
|
||||||
"select_address_book": "Seleccioneu un directori...",
|
"select_address_book": "Seleccioneu un directori...",
|
||||||
"section_identity": "Nom i identitat",
|
"section_identity": "Nom i identitat",
|
||||||
|
"contact_type": "Tipus de contacte",
|
||||||
|
"type_person": "Persona",
|
||||||
|
"type_organization": "Organització",
|
||||||
"section_work": "Feina i organització",
|
"section_work": "Feina i organització",
|
||||||
"prefix": "Prefix",
|
"prefix": "Prefix",
|
||||||
"prefix_placeholder": "Dr., Sr., Sra.",
|
"prefix_placeholder": "Dr., Sr., Sra.",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"creating": "Creant...",
|
"creating": "Creant...",
|
||||||
"updating": "Actualitzant...",
|
"updating": "Actualitzant...",
|
||||||
"name_required": "Cal com a mínim un nom o un cognom",
|
"name_required": "Introduïu un nom, un cognom o una organització",
|
||||||
"email_invalid": "Introduïu una adreça electrònica vàlida",
|
"email_invalid": "Introduïu una adreça electrònica vàlida",
|
||||||
"email_error_inline": "Format de correu electrònic no vàlid",
|
"email_error_inline": "Format de correu electrònic no vàlid",
|
||||||
"save_failed": "No s'ha pogut desar el contacte",
|
"save_failed": "No s'ha pogut desar el contacte",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Adresář",
|
"section_address_book": "Adresář",
|
||||||
"select_address_book": "Vyberte adresář...",
|
"select_address_book": "Vyberte adresář...",
|
||||||
"section_identity": "Jméno a identita",
|
"section_identity": "Jméno a identita",
|
||||||
|
"contact_type": "Typ kontaktu",
|
||||||
|
"type_person": "Osoba",
|
||||||
|
"type_organization": "Organizace",
|
||||||
"section_work": "Práce a organizace",
|
"section_work": "Práce a organizace",
|
||||||
"prefix": "Titul",
|
"prefix": "Titul",
|
||||||
"prefix_placeholder": "Dr., Pan, Paní",
|
"prefix_placeholder": "Dr., Pan, Paní",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"creating": "Vytváření...",
|
"creating": "Vytváření...",
|
||||||
"updating": "Aktualizování...",
|
"updating": "Aktualizování...",
|
||||||
"name_required": "Je vyžadováno alespoň jméno nebo příjmení",
|
"name_required": "Zadejte jméno, příjmení nebo organizaci",
|
||||||
"email_invalid": "Zadejte platnou e-mailovou adresu",
|
"email_invalid": "Zadejte platnou e-mailovou adresu",
|
||||||
"email_error_inline": "Neplatný formát e-mailové adresy",
|
"email_error_inline": "Neplatný formát e-mailové adresy",
|
||||||
"save_failed": "Uložení kontaktu selhalo",
|
"save_failed": "Uložení kontaktu selhalo",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Adressebog",
|
"section_address_book": "Adressebog",
|
||||||
"select_address_book": "Vælg en adressebog...",
|
"select_address_book": "Vælg en adressebog...",
|
||||||
"section_identity": "Navn & identitet",
|
"section_identity": "Navn & identitet",
|
||||||
|
"contact_type": "Kontakttype",
|
||||||
|
"type_person": "Person",
|
||||||
|
"type_organization": "Organisation",
|
||||||
"section_work": "Arbejde & organisation",
|
"section_work": "Arbejde & organisation",
|
||||||
"prefix": "Præfiks",
|
"prefix": "Præfiks",
|
||||||
"prefix_placeholder": "Dr., hr., fru",
|
"prefix_placeholder": "Dr., hr., fru",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Annuller",
|
"cancel": "Annuller",
|
||||||
"creating": "Opretter...",
|
"creating": "Opretter...",
|
||||||
"updating": "Opdaterer...",
|
"updating": "Opdaterer...",
|
||||||
"name_required": "Mindst et fornavn eller efternavn er påkrævet",
|
"name_required": "Angiv et fornavn, efternavn eller en organisation",
|
||||||
"email_invalid": "Indtast en gyldig e-mailadresse",
|
"email_invalid": "Indtast en gyldig e-mailadresse",
|
||||||
"email_error_inline": "Ugyldigt e-mailformat",
|
"email_error_inline": "Ugyldigt e-mailformat",
|
||||||
"save_failed": "Kunne ikke gemme kontakt",
|
"save_failed": "Kunne ikke gemme kontakt",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Verzeichnis",
|
"section_address_book": "Verzeichnis",
|
||||||
"select_address_book": "Verzeichnis auswählen...",
|
"select_address_book": "Verzeichnis auswählen...",
|
||||||
"section_identity": "Name & Identität",
|
"section_identity": "Name & Identität",
|
||||||
|
"contact_type": "Kontakttyp",
|
||||||
|
"type_person": "Person",
|
||||||
|
"type_organization": "Organisation",
|
||||||
"section_work": "Beruf & Organisation",
|
"section_work": "Beruf & Organisation",
|
||||||
"prefix": "Anrede",
|
"prefix": "Anrede",
|
||||||
"prefix_placeholder": "Dr., Herr, Frau",
|
"prefix_placeholder": "Dr., Herr, Frau",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"creating": "Wird erstellt...",
|
"creating": "Wird erstellt...",
|
||||||
"updating": "Wird aktualisiert...",
|
"updating": "Wird aktualisiert...",
|
||||||
"name_required": "Mindestens ein Vor- oder Nachname ist erforderlich",
|
"name_required": "Bitte Vorname, Nachname oder Organisation angeben",
|
||||||
"email_invalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
"email_invalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||||
"email_error_inline": "Ungültiges E-Mail-Format",
|
"email_error_inline": "Ungültiges E-Mail-Format",
|
||||||
"save_failed": "Kontakt konnte nicht gespeichert werden",
|
"save_failed": "Kontakt konnte nicht gespeichert werden",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "Directory",
|
"section_address_book": "Directory",
|
||||||
"select_address_book": "Select a directory...",
|
"select_address_book": "Select a directory...",
|
||||||
"section_identity": "Name & Identity",
|
"section_identity": "Name & Identity",
|
||||||
|
"contact_type": "Contact type",
|
||||||
|
"type_person": "Person",
|
||||||
|
"type_organization": "Organization",
|
||||||
"section_work": "Work & Organization",
|
"section_work": "Work & Organization",
|
||||||
"prefix": "Prefix",
|
"prefix": "Prefix",
|
||||||
"prefix_placeholder": "Dr., Mr., Mrs.",
|
"prefix_placeholder": "Dr., Mr., Mrs.",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"creating": "Creating...",
|
"creating": "Creating...",
|
||||||
"updating": "Updating...",
|
"updating": "Updating...",
|
||||||
"name_required": "At least a first name or last name is required",
|
"name_required": "Enter a first name, last name, or organization",
|
||||||
"email_invalid": "Please enter a valid email address",
|
"email_invalid": "Please enter a valid email address",
|
||||||
"email_error_inline": "Invalid email format",
|
"email_error_inline": "Invalid email format",
|
||||||
"save_failed": "Failed to save contact",
|
"save_failed": "Failed to save contact",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Directorio",
|
"section_address_book": "Directorio",
|
||||||
"select_address_book": "Seleccionar un directorio...",
|
"select_address_book": "Seleccionar un directorio...",
|
||||||
"section_identity": "Nombre e identidad",
|
"section_identity": "Nombre e identidad",
|
||||||
|
"contact_type": "Tipo de contacto",
|
||||||
|
"type_person": "Persona",
|
||||||
|
"type_organization": "Organización",
|
||||||
"section_work": "Trabajo y organización",
|
"section_work": "Trabajo y organización",
|
||||||
"prefix": "Prefijo",
|
"prefix": "Prefijo",
|
||||||
"prefix_placeholder": "Dr., Sr., Sra.",
|
"prefix_placeholder": "Dr., Sr., Sra.",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"creating": "Creando...",
|
"creating": "Creando...",
|
||||||
"updating": "Actualizando...",
|
"updating": "Actualizando...",
|
||||||
"name_required": "Se requiere al menos un nombre o apellido",
|
"name_required": "Introduce un nombre, un apellido o una organización",
|
||||||
"email_invalid": "Introduce una dirección de correo válida",
|
"email_invalid": "Introduce una dirección de correo válida",
|
||||||
"email_error_inline": "Formato de correo inválido",
|
"email_error_inline": "Formato de correo inválido",
|
||||||
"save_failed": "Error al guardar el contacto",
|
"save_failed": "Error al guardar el contacto",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "دفترچه",
|
"section_address_book": "دفترچه",
|
||||||
"select_address_book": "انتخاب دفترچه...",
|
"select_address_book": "انتخاب دفترچه...",
|
||||||
"section_identity": "نام و هویت",
|
"section_identity": "نام و هویت",
|
||||||
|
"contact_type": "نوع مخاطب",
|
||||||
|
"type_person": "شخص",
|
||||||
|
"type_organization": "سازمان",
|
||||||
"section_work": "کار و سازمان",
|
"section_work": "کار و سازمان",
|
||||||
"prefix": "پیشوند",
|
"prefix": "پیشوند",
|
||||||
"prefix_placeholder": "دکتر، مهندس",
|
"prefix_placeholder": "دکتر، مهندس",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "انصراف",
|
"cancel": "انصراف",
|
||||||
"creating": "در حال ایجاد...",
|
"creating": "در حال ایجاد...",
|
||||||
"updating": "در حال بهروزرسانی...",
|
"updating": "در حال بهروزرسانی...",
|
||||||
"name_required": "حداقل نام یا نام خانوادگی الزامی است",
|
"name_required": "نام، نام خانوادگی یا سازمان را وارد کنید",
|
||||||
"email_invalid": "لطفاً یک آدرس ایمیل معتبر وارد کنید",
|
"email_invalid": "لطفاً یک آدرس ایمیل معتبر وارد کنید",
|
||||||
"email_error_inline": "فرمت ایمیل نامعتبر است",
|
"email_error_inline": "فرمت ایمیل نامعتبر است",
|
||||||
"save_failed": "ذخیره مخاطب ناموفق بود",
|
"save_failed": "ذخیره مخاطب ناموفق بود",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Répertoire",
|
"section_address_book": "Répertoire",
|
||||||
"select_address_book": "Sélectionner un répertoire...",
|
"select_address_book": "Sélectionner un répertoire...",
|
||||||
"section_identity": "Nom et identité",
|
"section_identity": "Nom et identité",
|
||||||
|
"contact_type": "Type de contact",
|
||||||
|
"type_person": "Personne",
|
||||||
|
"type_organization": "Organisation",
|
||||||
"section_work": "Travail et organisation",
|
"section_work": "Travail et organisation",
|
||||||
"prefix": "Préfixe",
|
"prefix": "Préfixe",
|
||||||
"prefix_placeholder": "Dr., M., Mme",
|
"prefix_placeholder": "Dr., M., Mme",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"creating": "Création...",
|
"creating": "Création...",
|
||||||
"updating": "Mise à jour...",
|
"updating": "Mise à jour...",
|
||||||
"name_required": "Un prénom ou un nom est requis",
|
"name_required": "Saisissez un prénom, un nom ou une organisation",
|
||||||
"email_invalid": "Veuillez saisir une adresse e-mail valide",
|
"email_invalid": "Veuillez saisir une adresse e-mail valide",
|
||||||
"email_error_inline": "Format d'e-mail invalide",
|
"email_error_inline": "Format d'e-mail invalide",
|
||||||
"save_failed": "Échec de l'enregistrement du contact",
|
"save_failed": "Échec de l'enregistrement du contact",
|
||||||
|
|||||||
@@ -2289,6 +2289,9 @@
|
|||||||
"section_address_book": "ספרייה",
|
"section_address_book": "ספרייה",
|
||||||
"select_address_book": "בחר ספרייה...",
|
"select_address_book": "בחר ספרייה...",
|
||||||
"section_identity": "שם וזהות",
|
"section_identity": "שם וזהות",
|
||||||
|
"contact_type": "סוג איש קשר",
|
||||||
|
"type_person": "אדם",
|
||||||
|
"type_organization": "ארגון",
|
||||||
"section_work": "עבודה וארגון",
|
"section_work": "עבודה וארגון",
|
||||||
"prefix": "קידומת",
|
"prefix": "קידומת",
|
||||||
"prefix_placeholder": "ד\"ר, מר, גברת.",
|
"prefix_placeholder": "ד\"ר, מר, גברת.",
|
||||||
@@ -2373,7 +2376,7 @@
|
|||||||
"cancel": "לְבַטֵל",
|
"cancel": "לְבַטֵל",
|
||||||
"creating": "יוצר...",
|
"creating": "יוצר...",
|
||||||
"updating": "מעדכן...",
|
"updating": "מעדכן...",
|
||||||
"name_required": "נדרש לפחות שם פרטי או שם משפחה",
|
"name_required": "יש להזין שם פרטי, שם משפחה או ארגון",
|
||||||
"email_invalid": "נא להזין כתובת אימייל חוקית",
|
"email_invalid": "נא להזין כתובת אימייל חוקית",
|
||||||
"email_error_inline": "פורמט אימייל לא חוקי",
|
"email_error_inline": "פורמט אימייל לא חוקי",
|
||||||
"save_failed": "שמירת איש הקשר נכשלה",
|
"save_failed": "שמירת איש הקשר נכשלה",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "Címtár",
|
"section_address_book": "Címtár",
|
||||||
"select_address_book": "Címtár kiválasztása...",
|
"select_address_book": "Címtár kiválasztása...",
|
||||||
"section_identity": "Név és azonosság",
|
"section_identity": "Név és azonosság",
|
||||||
|
"contact_type": "Névjegy típusa",
|
||||||
|
"type_person": "Személy",
|
||||||
|
"type_organization": "Szervezet",
|
||||||
"section_work": "Munka és szervezet",
|
"section_work": "Munka és szervezet",
|
||||||
"prefix": "Előtag",
|
"prefix": "Előtag",
|
||||||
"prefix_placeholder": "Dr., Úr., Mrs.",
|
"prefix_placeholder": "Dr., Úr., Mrs.",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"creating": "Létrehozás...",
|
"creating": "Létrehozás...",
|
||||||
"updating": "Frissítés...",
|
"updating": "Frissítés...",
|
||||||
"name_required": "Legalább a keresztnév vagy vezetéknév megadása kötelező",
|
"name_required": "Adjon meg egy keresztnevet, vezetéknevet vagy szervezetet",
|
||||||
"email_invalid": "Kérjük, adj meg egy érvényes e-mail címet",
|
"email_invalid": "Kérjük, adj meg egy érvényes e-mail címet",
|
||||||
"email_error_inline": "Érvénytelen e-mail formátum",
|
"email_error_inline": "Érvénytelen e-mail formátum",
|
||||||
"save_failed": "Nem sikerült menteni a névjegyet",
|
"save_failed": "Nem sikerült menteni a névjegyet",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Rubrica",
|
"section_address_book": "Rubrica",
|
||||||
"select_address_book": "Seleziona una rubrica...",
|
"select_address_book": "Seleziona una rubrica...",
|
||||||
"section_identity": "Nome e identità",
|
"section_identity": "Nome e identità",
|
||||||
|
"contact_type": "Tipo di contatto",
|
||||||
|
"type_person": "Persona",
|
||||||
|
"type_organization": "Organizzazione",
|
||||||
"section_work": "Lavoro e organizzazione",
|
"section_work": "Lavoro e organizzazione",
|
||||||
"prefix": "Prefisso",
|
"prefix": "Prefisso",
|
||||||
"prefix_placeholder": "Dott., Sig., Sig.ra",
|
"prefix_placeholder": "Dott., Sig., Sig.ra",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"creating": "Creazione...",
|
"creating": "Creazione...",
|
||||||
"updating": "Aggiornamento...",
|
"updating": "Aggiornamento...",
|
||||||
"name_required": "È richiesto almeno un nome o cognome",
|
"name_required": "Inserisci un nome, un cognome o un'organizzazione",
|
||||||
"email_invalid": "Inserisci un indirizzo email valido",
|
"email_invalid": "Inserisci un indirizzo email valido",
|
||||||
"email_error_inline": "Formato email non valido",
|
"email_error_inline": "Formato email non valido",
|
||||||
"save_failed": "Impossibile salvare il contatto",
|
"save_failed": "Impossibile salvare il contatto",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "ディレクトリ",
|
"section_address_book": "ディレクトリ",
|
||||||
"select_address_book": "ディレクトリを選択...",
|
"select_address_book": "ディレクトリを選択...",
|
||||||
"section_identity": "名前と識別情報",
|
"section_identity": "名前と識別情報",
|
||||||
|
"contact_type": "連絡先の種類",
|
||||||
|
"type_person": "個人",
|
||||||
|
"type_organization": "組織",
|
||||||
"section_work": "職業と組織",
|
"section_work": "職業と組織",
|
||||||
"prefix": "敬称",
|
"prefix": "敬称",
|
||||||
"prefix_placeholder": "博士、氏",
|
"prefix_placeholder": "博士、氏",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"creating": "作成中...",
|
"creating": "作成中...",
|
||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"name_required": "名前は必須です",
|
"name_required": "名、姓、または組織を入力してください",
|
||||||
"email_invalid": "有効なメールアドレスを入力してください",
|
"email_invalid": "有効なメールアドレスを入力してください",
|
||||||
"email_error_inline": "メールアドレスの形式が正しくありません",
|
"email_error_inline": "メールアドレスの形式が正しくありません",
|
||||||
"save_failed": "連絡先の保存に失敗しました",
|
"save_failed": "連絡先の保存に失敗しました",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "디렉터리",
|
"section_address_book": "디렉터리",
|
||||||
"select_address_book": "디렉터리 선택...",
|
"select_address_book": "디렉터리 선택...",
|
||||||
"section_identity": "이름 및 신원",
|
"section_identity": "이름 및 신원",
|
||||||
|
"contact_type": "연락처 유형",
|
||||||
|
"type_person": "개인",
|
||||||
|
"type_organization": "소속(회사)",
|
||||||
"section_work": "직장 및 소속",
|
"section_work": "직장 및 소속",
|
||||||
"prefix": "호칭",
|
"prefix": "호칭",
|
||||||
"prefix_placeholder": "예: Dr., Mr., Mrs.",
|
"prefix_placeholder": "예: Dr., Mr., Mrs.",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"creating": "만드는 중...",
|
"creating": "만드는 중...",
|
||||||
"updating": "업데이트 중...",
|
"updating": "업데이트 중...",
|
||||||
"name_required": "이름이나 성 중에 하나는 꼭 필요해요",
|
"name_required": "이름, 성 또는 조직을 입력하세요",
|
||||||
"email_invalid": "올바른 이메일 주소를 입력해 주세요",
|
"email_invalid": "올바른 이메일 주소를 입력해 주세요",
|
||||||
"email_error_inline": "이메일 형식이 잘못되었어요",
|
"email_error_inline": "이메일 형식이 잘못되었어요",
|
||||||
"save_failed": "연락처를 저장하지 못했어요",
|
"save_failed": "연락처를 저장하지 못했어요",
|
||||||
|
|||||||
@@ -2371,6 +2371,9 @@
|
|||||||
"section_address_book": "Katalogs",
|
"section_address_book": "Katalogs",
|
||||||
"select_address_book": "Izvēlieties katalogu...",
|
"select_address_book": "Izvēlieties katalogu...",
|
||||||
"section_identity": "Vārds un identitāte",
|
"section_identity": "Vārds un identitāte",
|
||||||
|
"contact_type": "Kontakta veids",
|
||||||
|
"type_person": "Persona",
|
||||||
|
"type_organization": "Organizācija",
|
||||||
"section_work": "Darbs un organizācija",
|
"section_work": "Darbs un organizācija",
|
||||||
"prefix": "Prefikss",
|
"prefix": "Prefikss",
|
||||||
"prefix_placeholder": "Dr., kungs, kundze",
|
"prefix_placeholder": "Dr., kungs, kundze",
|
||||||
@@ -2455,7 +2458,7 @@
|
|||||||
"cancel": "Atcelt",
|
"cancel": "Atcelt",
|
||||||
"creating": "Izveido...",
|
"creating": "Izveido...",
|
||||||
"updating": "Atjaunina...",
|
"updating": "Atjaunina...",
|
||||||
"name_required": "Nepieciešams vismaz vārds vai uzvārds",
|
"name_required": "Ievadiet vārdu, uzvārdu vai organizāciju",
|
||||||
"email_invalid": "Ievadiet derīgu e-pasta adresi",
|
"email_invalid": "Ievadiet derīgu e-pasta adresi",
|
||||||
"email_error_inline": "Nederīgs e-pasta formāts",
|
"email_error_inline": "Nederīgs e-pasta formāts",
|
||||||
"save_failed": "Neizdevās saglabāt kontaktu",
|
"save_failed": "Neizdevās saglabāt kontaktu",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Adresboek",
|
"section_address_book": "Adresboek",
|
||||||
"select_address_book": "Selecteer een adresboek...",
|
"select_address_book": "Selecteer een adresboek...",
|
||||||
"section_identity": "Naam en identiteit",
|
"section_identity": "Naam en identiteit",
|
||||||
|
"contact_type": "Contacttype",
|
||||||
|
"type_person": "Persoon",
|
||||||
|
"type_organization": "Organisatie",
|
||||||
"section_work": "Werk en organisatie",
|
"section_work": "Werk en organisatie",
|
||||||
"prefix": "Voorvoegsel",
|
"prefix": "Voorvoegsel",
|
||||||
"prefix_placeholder": "Dr., Dhr., Mevr.",
|
"prefix_placeholder": "Dr., Dhr., Mevr.",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"creating": "Aanmaken...",
|
"creating": "Aanmaken...",
|
||||||
"updating": "Bijwerken...",
|
"updating": "Bijwerken...",
|
||||||
"name_required": "Ten minste een voor- of achternaam is vereist",
|
"name_required": "Voer een voornaam, achternaam of organisatie in",
|
||||||
"email_invalid": "Voer een geldig e-mailadres in",
|
"email_invalid": "Voer een geldig e-mailadres in",
|
||||||
"email_error_inline": "Ongeldig e-mailformaat",
|
"email_error_inline": "Ongeldig e-mailformaat",
|
||||||
"save_failed": "Kon contact niet opslaan",
|
"save_failed": "Kon contact niet opslaan",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Katalog",
|
"section_address_book": "Katalog",
|
||||||
"select_address_book": "Wybierz katalog...",
|
"select_address_book": "Wybierz katalog...",
|
||||||
"section_identity": "Imię i tożsamość",
|
"section_identity": "Imię i tożsamość",
|
||||||
|
"contact_type": "Typ kontaktu",
|
||||||
|
"type_person": "Osoba",
|
||||||
|
"type_organization": "Organizacja",
|
||||||
"section_work": "Praca i organizacja",
|
"section_work": "Praca i organizacja",
|
||||||
"prefix": "Tytuł",
|
"prefix": "Tytuł",
|
||||||
"prefix_placeholder": "Dr, Pan, Pani",
|
"prefix_placeholder": "Dr, Pan, Pani",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"creating": "Tworzenie...",
|
"creating": "Tworzenie...",
|
||||||
"updating": "Aktualizowanie...",
|
"updating": "Aktualizowanie...",
|
||||||
"name_required": "Wymagane jest przynajmniej imię lub nazwisko",
|
"name_required": "Podaj imię, nazwisko lub organizację",
|
||||||
"email_invalid": "Wprowadź prawidłowy adres e-mail",
|
"email_invalid": "Wprowadź prawidłowy adres e-mail",
|
||||||
"email_error_inline": "Nieprawidłowy format adresu e-mail",
|
"email_error_inline": "Nieprawidłowy format adresu e-mail",
|
||||||
"save_failed": "Nie udało się zapisać kontaktu",
|
"save_failed": "Nie udało się zapisać kontaktu",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Diretório",
|
"section_address_book": "Diretório",
|
||||||
"select_address_book": "Selecionar um diretório...",
|
"select_address_book": "Selecionar um diretório...",
|
||||||
"section_identity": "Nome e identidade",
|
"section_identity": "Nome e identidade",
|
||||||
|
"contact_type": "Tipo de contacto",
|
||||||
|
"type_person": "Pessoa",
|
||||||
|
"type_organization": "Organização",
|
||||||
"section_work": "Trabalho e organização",
|
"section_work": "Trabalho e organização",
|
||||||
"prefix": "Prefixo",
|
"prefix": "Prefixo",
|
||||||
"prefix_placeholder": "Dr., Sr., Sra.",
|
"prefix_placeholder": "Dr., Sr., Sra.",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"creating": "Criando...",
|
"creating": "Criando...",
|
||||||
"updating": "Atualizando...",
|
"updating": "Atualizando...",
|
||||||
"name_required": "É necessário pelo menos um nome ou sobrenome",
|
"name_required": "Introduza um nome próprio, apelido ou organização",
|
||||||
"email_invalid": "Por favor, insira um endereço de e-mail válido",
|
"email_invalid": "Por favor, insira um endereço de e-mail válido",
|
||||||
"email_error_inline": "Formato de e-mail inválido",
|
"email_error_inline": "Formato de e-mail inválido",
|
||||||
"save_failed": "Falha ao salvar contato",
|
"save_failed": "Falha ao salvar contato",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "Director",
|
"section_address_book": "Director",
|
||||||
"select_address_book": "Selectați un director...",
|
"select_address_book": "Selectați un director...",
|
||||||
"section_identity": "Nume și identitate",
|
"section_identity": "Nume și identitate",
|
||||||
|
"contact_type": "Tip de contact",
|
||||||
|
"type_person": "Persoană",
|
||||||
|
"type_organization": "Organizare",
|
||||||
"section_work": "Muncă și organizare",
|
"section_work": "Muncă și organizare",
|
||||||
"prefix": "Prefix",
|
"prefix": "Prefix",
|
||||||
"prefix_placeholder": "Dr., Dl., Dna.",
|
"prefix_placeholder": "Dr., Dl., Dna.",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "Anulează",
|
"cancel": "Anulează",
|
||||||
"creating": "Se creează...",
|
"creating": "Se creează...",
|
||||||
"updating": "Se actualizează...",
|
"updating": "Se actualizează...",
|
||||||
"name_required": "Este necesar cel puțin un prenume sau un nume de familie",
|
"name_required": "Introduceți un prenume, un nume sau o organizație",
|
||||||
"email_invalid": "Vă rugăm să introduceți o adresă de e-mail validă",
|
"email_invalid": "Vă rugăm să introduceți o adresă de e-mail validă",
|
||||||
"email_error_inline": "Format de e-mail nevalid",
|
"email_error_inline": "Format de e-mail nevalid",
|
||||||
"save_failed": "Nu s-a putut salva contactul",
|
"save_failed": "Nu s-a putut salva contactul",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Каталог",
|
"section_address_book": "Каталог",
|
||||||
"select_address_book": "Выберите каталог...",
|
"select_address_book": "Выберите каталог...",
|
||||||
"section_identity": "Имя и личность",
|
"section_identity": "Имя и личность",
|
||||||
|
"contact_type": "Тип контакта",
|
||||||
|
"type_person": "Человек",
|
||||||
|
"type_organization": "Организация",
|
||||||
"section_work": "Работа и организация",
|
"section_work": "Работа и организация",
|
||||||
"prefix": "Префикс",
|
"prefix": "Префикс",
|
||||||
"prefix_placeholder": "Д-р., Г-н., Г-жа.",
|
"prefix_placeholder": "Д-р., Г-н., Г-жа.",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"creating": "Создание...",
|
"creating": "Создание...",
|
||||||
"updating": "Обновление...",
|
"updating": "Обновление...",
|
||||||
"name_required": "Требуется хотя бы имя или фамилия",
|
"name_required": "Укажите имя, фамилию или организацию",
|
||||||
"email_invalid": "Введите корректный адрес электронной почты",
|
"email_invalid": "Введите корректный адрес электронной почты",
|
||||||
"email_error_inline": "Неверный формат email",
|
"email_error_inline": "Неверный формат email",
|
||||||
"save_failed": "Не удалось сохранить контакт",
|
"save_failed": "Не удалось сохранить контакт",
|
||||||
|
|||||||
@@ -2376,6 +2376,9 @@
|
|||||||
"section_address_book": "Adresár",
|
"section_address_book": "Adresár",
|
||||||
"select_address_book": "Vyberte adresár...",
|
"select_address_book": "Vyberte adresár...",
|
||||||
"section_identity": "Meno a identita",
|
"section_identity": "Meno a identita",
|
||||||
|
"contact_type": "Typ kontaktu",
|
||||||
|
"type_person": "Osoba",
|
||||||
|
"type_organization": "Organizácia",
|
||||||
"section_work": "Práca a organizácia",
|
"section_work": "Práca a organizácia",
|
||||||
"prefix": "Titul",
|
"prefix": "Titul",
|
||||||
"prefix_placeholder": "Dr., Pán, Pani",
|
"prefix_placeholder": "Dr., Pán, Pani",
|
||||||
@@ -2460,7 +2463,7 @@
|
|||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
"creating": "Vytváranie...",
|
"creating": "Vytváranie...",
|
||||||
"updating": "Aktualizovanie...",
|
"updating": "Aktualizovanie...",
|
||||||
"name_required": "Je potrebné aspoň meno alebo priezvisko",
|
"name_required": "Zadajte meno, priezvisko alebo organizáciu",
|
||||||
"email_invalid": "Zadajte platnú e-mailovú adresu",
|
"email_invalid": "Zadajte platnú e-mailovú adresu",
|
||||||
"email_error_inline": "Neplatný formát e-mailovej adresy",
|
"email_error_inline": "Neplatný formát e-mailovej adresy",
|
||||||
"save_failed": "Uloženie kontaktu zlyhalo",
|
"save_failed": "Uloženie kontaktu zlyhalo",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Dizin",
|
"section_address_book": "Dizin",
|
||||||
"select_address_book": "Bir dizin seçin...",
|
"select_address_book": "Bir dizin seçin...",
|
||||||
"section_identity": "Ad ve Kimlik",
|
"section_identity": "Ad ve Kimlik",
|
||||||
|
"contact_type": "Kişi türü",
|
||||||
|
"type_person": "Kişi",
|
||||||
|
"type_organization": "Kuruluş",
|
||||||
"section_work": "İş ve Kuruluş",
|
"section_work": "İş ve Kuruluş",
|
||||||
"prefix": "Ön Ek",
|
"prefix": "Ön Ek",
|
||||||
"prefix_placeholder": "Dr., Bay, Bayan",
|
"prefix_placeholder": "Dr., Bay, Bayan",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"creating": "Oluşturuluyor...",
|
"creating": "Oluşturuluyor...",
|
||||||
"updating": "Güncelleniyor...",
|
"updating": "Güncelleniyor...",
|
||||||
"name_required": "En az bir ad veya soyadı gereklidir",
|
"name_required": "Bir ad, soyad veya kuruluş girin",
|
||||||
"email_invalid": "Lütfen geçerli bir e-posta adresi girin",
|
"email_invalid": "Lütfen geçerli bir e-posta adresi girin",
|
||||||
"email_error_inline": "Geçersiz e-posta biçimi",
|
"email_error_inline": "Geçersiz e-posta biçimi",
|
||||||
"save_failed": "Kişi kaydedilemedi",
|
"save_failed": "Kişi kaydedilemedi",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "Довідник",
|
"section_address_book": "Довідник",
|
||||||
"select_address_book": "Виберіть каталог...",
|
"select_address_book": "Виберіть каталог...",
|
||||||
"section_identity": "Ім'я та ідентифікація",
|
"section_identity": "Ім'я та ідентифікація",
|
||||||
|
"contact_type": "Тип контакту",
|
||||||
|
"type_person": "Людина",
|
||||||
|
"type_organization": "організація",
|
||||||
"section_work": "Робота та організація",
|
"section_work": "Робота та організація",
|
||||||
"prefix": "Префікс",
|
"prefix": "Префікс",
|
||||||
"prefix_placeholder": "доктор, пан, місіс",
|
"prefix_placeholder": "доктор, пан, місіс",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "Скасувати",
|
"cancel": "Скасувати",
|
||||||
"creating": "Створення...",
|
"creating": "Створення...",
|
||||||
"updating": "Оновлення...",
|
"updating": "Оновлення...",
|
||||||
"name_required": "Потрібне принаймні ім’я або прізвище",
|
"name_required": "Вкажіть ім'я, прізвище або організацію",
|
||||||
"email_invalid": "Введіть дійсну електронну адресу",
|
"email_invalid": "Введіть дійсну електронну адресу",
|
||||||
"email_error_inline": "Недійсний формат електронної пошти",
|
"email_error_inline": "Недійсний формат електронної пошти",
|
||||||
"save_failed": "Не вдалося зберегти контакт",
|
"save_failed": "Не вдалося зберегти контакт",
|
||||||
|
|||||||
@@ -2375,6 +2375,9 @@
|
|||||||
"section_address_book": "地址簿",
|
"section_address_book": "地址簿",
|
||||||
"select_address_book": "选择地址簿...",
|
"select_address_book": "选择地址簿...",
|
||||||
"section_identity": "姓名和身份",
|
"section_identity": "姓名和身份",
|
||||||
|
"contact_type": "联系人类型",
|
||||||
|
"type_person": "个人",
|
||||||
|
"type_organization": "组织",
|
||||||
"section_work": "工作与组织",
|
"section_work": "工作与组织",
|
||||||
"prefix": "前缀",
|
"prefix": "前缀",
|
||||||
"prefix_placeholder": "博士、先生、女士",
|
"prefix_placeholder": "博士、先生、女士",
|
||||||
@@ -2459,7 +2462,7 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"creating": "创建中...",
|
"creating": "创建中...",
|
||||||
"updating": "更新中...",
|
"updating": "更新中...",
|
||||||
"name_required": "至少需要名字或姓氏",
|
"name_required": "请输入名字、姓氏或组织",
|
||||||
"email_invalid": "请输入有效的邮箱地址",
|
"email_invalid": "请输入有效的邮箱地址",
|
||||||
"email_error_inline": "邮箱地址格式无效",
|
"email_error_inline": "邮箱地址格式无效",
|
||||||
"save_failed": "保存联系人失败",
|
"save_failed": "保存联系人失败",
|
||||||
|
|||||||
Reference in New Issue
Block a user