diff --git a/messages/en.json b/messages/en.json
index 79d156c..ad35618 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -249,5 +249,201 @@
"notes": "Notes",
"deals": "Deals"
}
+ },
+ "Contacts": {
+ "page": {
+ "title": "Contacts",
+ "subtitle": "Manage your contact list. {count} total contacts.",
+ "subtitleZero": "Manage your contact list.",
+ "customFieldsBtn": "Custom fields",
+ "importBtn": "Import",
+ "addContactBtn": "Add Contact",
+ "searchPlaceholder": "Search by name, phone, or email...",
+ "filterByTags": "Filter by tags",
+ "clearAll": "Clear all",
+ "noTagsYet": "No tags yet.",
+ "selectedCount": "{count} selected",
+ "clearSelection": "Clear",
+ "deleteSelected": "Delete selected",
+ "tableColumns": {
+ "name": "Name",
+ "phone": "Phone",
+ "email": "Email",
+ "company": "Company",
+ "tags": "Tags",
+ "createdAt": "Created"
+ },
+ "loading": "Loading contacts...",
+ "noContactsMatch": "No contacts match your filters.",
+ "noContactsYet": "No contacts yet.",
+ "addFirstContact": "Add your first contact",
+ "unnamed": "Unnamed",
+ "editAction": "Edit",
+ "deleteAction": "Delete",
+ "showingPagination": "Showing {start}-{end} of {total}",
+ "pageCount": "Page {page} of {total}",
+ "deleteContactTitle": "Delete Contact",
+ "deleteContactDesc": "Are you sure you want to delete {name}? This action cannot be undone.",
+ "cancel": "Cancel",
+ "deleteBtn": "Delete",
+ "deleteBulkTitle": "Delete Contacts",
+ "deleteBulkDesc": "Are you sure you want to delete {count} selected contacts? This action cannot be undone.",
+ "toastFailedLoad": "Failed to load contacts",
+ "toastFailedDelete": "Failed to delete contact",
+ "toastDeleted": "Contact deleted",
+ "toastBulkFailedDelete": "Failed to delete contacts",
+ "toastBulkDeleted": "{count} contacts deleted"
+ },
+ "form": {
+ "addTitle": "Add Contact",
+ "addDesc": "Create a new contact. Name is required.",
+ "editTitle": "Edit Contact",
+ "editDesc": "Update contact details and tags.",
+ "nameLabel": "Name",
+ "namePlaceholder": "Jane Doe",
+ "phoneLabel": "Phone Number",
+ "phonePlaceholder": "+1234567890",
+ "phoneHint": "Include country code, e.g. +1 for US",
+ "emailLabel": "Email",
+ "emailPlaceholder": "jane@example.com",
+ "companyLabel": "Company",
+ "companyPlaceholder": "Acme Corp",
+ "tagsLabel": "Tags",
+ "tagsPlaceholder": "Select tags...",
+ "noTagsFound": "No tags found.",
+ "createTagAction": "Create tag \"{value}\"",
+ "cancel": "Cancel",
+ "save": "Save Contact",
+ "update": "Update",
+ "create": "Create",
+ "toastSuccessAdd": "Contact created successfully",
+ "toastSuccessEdit": "Contact updated successfully",
+ "toastError": "Failed to save contact",
+ "toastConflict": "A contact with this phone number already exists",
+ "viewExisting": "View {name}",
+ "dupExact": "A contact with this phone number already exists.",
+ "dupSimilar": "A contact with a very similar number already exists.",
+ "loadingTags": "Loading tags...",
+ "noTagsAvailable": "No tags available. Create tags in Settings.",
+ "phoneRequired": "Phone number is required"
+ },
+ "detailView": {
+ "tabs": {
+ "details": "Details",
+ "activity": "Activity",
+ "notes": "Notes",
+ "custom": "Custom Fields",
+ "deals": "Deals"
+ },
+ "actions": {
+ "edit": "Edit",
+ "message": "Message",
+ "delete": "Delete"
+ },
+ "contactInfo": "Contact Information",
+ "unnamed": "Unknown",
+ "contactDetailsDesc": "Contact details",
+ "phone": "Phone",
+ "email": "Email",
+ "company": "Company",
+ "createdAt": "Created At",
+ "customFields": "Custom Fields",
+ "noCustomFields": "No custom fields defined. Create them in Settings.",
+ "manageFields": "Manage fields",
+ "enterCustomField": "Enter {name}...",
+ "activityTab": {
+ "title": "Activity History",
+ "noActivity": "No activity yet."
+ },
+ "notesTab": {
+ "title": "Internal Notes",
+ "placeholder": "Write a note...",
+ "save": "Add Note",
+ "noNotes": "No notes yet."
+ },
+ "dealsTab": {
+ "noDeals": "No deals yet"
+ },
+ "tagsTab": {
+ "clickTagDesc": "Click a tag to add or remove it from this contact.",
+ "noTagsAvailable": "No tags available. Create tags in Settings."
+ },
+ "sendTemplateBtn": "Send template",
+ "saveChangesBtn": "Save Changes",
+ "saveCustomFieldsBtn": "Save Custom Fields",
+ "toastPhoneRequired": "Phone number is required",
+ "toastUpdateFailed": "Failed to update contact",
+ "toastUpdated": "Contact updated",
+ "toastNotAuthenticated": "Not authenticated",
+ "toastNoteAddFailed": "Failed to add note",
+ "toastNoteAdded": "Note added",
+ "toastNoteDeleteFailed": "Failed to delete note",
+ "toastNoteDeleted": "Note deleted",
+ "toastCustomFieldsSaved": "Custom fields saved",
+ "toastCustomFieldsFailed": "Failed to save custom fields",
+ "toastTemplateSent": "Template \"{name}\" sent",
+ "toastTemplateFailed": "Failed to send template: {reason}"
+ },
+ "importModal": {
+ "title": "Import Contacts",
+ "desc": "Upload a CSV with a required phone column. Optional: name, email, company, tags (comma-separated; quote multi-tag cells).",
+ "uploadDropzone": "Click to choose a CSV file",
+ "uploadHint": ".csv up to your browser limit",
+ "rowsReady": "{count} row ready",
+ "rowsReady_plural": "{count} rows ready",
+ "preview": "Preview · first {count}",
+ "previewTags": "{tags} tag · {contacts} contact",
+ "previewTags_plural": "{tags} tags · {contacts} contacts",
+ "moreRows": "+ {count} more row not shown",
+ "moreRows_plural": "+ {count} more rows not shown",
+ "importComplete": "Import complete",
+ "resultImported": "{count} imported",
+ "resultTags": "{count} tag assigned",
+ "resultTags_plural": "{count} tags assigned",
+ "resultSkipped": "{count} skipped",
+ "resultFailed": "{count} failed",
+ "willBeCreated": "{name} (will be created on import)",
+ "columns": {
+ "phone": "Phone",
+ "name": "Name",
+ "email": "Email",
+ "company": "Company",
+ "tags": "Tags"
+ },
+ "cancel": "Cancel",
+ "close": "Close",
+ "importBtn": "Import {count} contact",
+ "importBtn_plural": "Import {count} contacts",
+ "toastNoValidRows": "No valid rows found. Ensure CSV has a \"phone\" column header.",
+ "toastTagsWarning": "Contacts imported, but some tag assignments failed.",
+ "toastImported": "{count} contact imported",
+ "toastImported_plural": "{count} contacts imported",
+ "toastTagsAssigned": "{count} tag assignment applied",
+ "toastTagsAssigned_plural": "{count} tag assignments applied",
+ "toastTagsSkipped": "Unknown tags skipped (create them in Settings first): {sample}{more}",
+ "toastSkipped": "{count} duplicate skipped",
+ "toastSkipped_plural": "{count} duplicates skipped",
+ "toastFailed": "{count} contact failed to import",
+ "toastFailed_plural": "{count} contacts failed to import",
+ "toastError": "Import failed"
+ },
+ "customFields": {
+ "title": "Custom fields",
+ "desc": "Define extra contact fields (e.g. ZIP code, lead source). They appear on every contact and in the “Update Contact Field” automation action.",
+ "addField": "Add",
+ "fieldName": "New field name…",
+ "loading": "Loading…",
+ "empty": "No custom fields yet.",
+ "renameAria": "Rename {name}",
+ "deleteTitle": "Delete field",
+ "deleteConfirm": "Delete \"{name}\"? This also removes its stored value on every contact. This cannot be undone.",
+ "toastNoAccount": "Your profile is not linked to an account.",
+ "toastDuplicate": "A field named \"{name}\" already exists.",
+ "toastCreateFailed": "Could not create field. You may not have permission.",
+ "toastCreated": "Created \"{name}\".",
+ "toastRenameFailed": "Could not rename field.",
+ "toastDeleteFailed": "Could not delete field.",
+ "toastDeleted": "Deleted \"{name}\"."
+ }
}
}
diff --git a/src/app/(dashboard)/contacts/page.tsx b/src/app/(dashboard)/contacts/page.tsx
index 5d0bc9c..0facba6 100644
--- a/src/app/(dashboard)/contacts/page.tsx
+++ b/src/app/(dashboard)/contacts/page.tsx
@@ -14,6 +14,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
+import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -55,7 +56,7 @@ import { ImportModal } from '@/components/contacts/import-modal';
import { CustomFieldsManager } from '@/components/contacts/custom-fields-manager';
import { useCan } from '@/hooks/use-can';
import { GatedButton } from '@/components/ui/gated-button';
-import { Checkbox } from '@/components/ui/checkbox';
+import { useTranslations } from 'next-intl';
const PAGE_SIZE = 25;
@@ -64,6 +65,7 @@ interface ContactWithTags extends Contact {
}
export default function ContactsPage() {
+ const t = useTranslations('Contacts.page');
const supabase = createClient();
const canEdit = useCan('send-messages');
const canEditSettings = useCan('edit-settings');
@@ -144,7 +146,7 @@ export default function ContactsPage() {
});
if (seq !== fetchSeq.current) return; // superseded by a newer fetch
if (error) {
- toast.error('Failed to load contacts');
+ toast.error(t('toastFailedLoad'));
setLoading(false);
return;
}
@@ -166,7 +168,7 @@ export default function ContactsPage() {
const { data, count: exactCount, error } = await query;
if (seq !== fetchSeq.current) return; // superseded by a newer fetch
if (error) {
- toast.error('Failed to load contacts');
+ toast.error(t('toastFailedLoad'));
setLoading(false);
return;
}
@@ -205,7 +207,7 @@ export default function ContactsPage() {
setContacts(enriched);
setLoading(false);
- }, [supabase, page, search, selectedTagIds, tagsMap]);
+ }, [supabase, page, search, selectedTagIds, tagsMap, t]);
// Load-once-on-mount-ish data fetches. Each setter inside runs
// inside an async promise completion (Supabase await), not
@@ -257,9 +259,9 @@ export default function ContactsPage() {
.eq('id', deleteTarget.id);
if (error) {
- toast.error('Failed to delete contact');
+ toast.error(t('toastFailedDelete'));
} else {
- toast.success('Contact deleted');
+ toast.success(t('toastDeleted'));
fetchContacts();
}
@@ -301,9 +303,9 @@ export default function ContactsPage() {
const { error } = await supabase.from('contacts').delete().in('id', ids);
if (error) {
- toast.error('Failed to delete contacts');
+ toast.error(t('toastBulkFailedDelete'));
} else {
- toast.success(`${ids.length} contact${ids.length === 1 ? '' : 's'} deleted`);
+ toast.success(t('toastBulkDeleted', { count: ids.length }));
setSelected(new Set());
fetchContacts();
}
@@ -342,9 +344,9 @@ export default function ContactsPage() {
{/* Header */}
-
Contacts
+
{t('title')}
- Manage your contact list. {totalCount > 0 && `${totalCount} total contacts.`}
+ {totalCount > 0 ? t('subtitle', { count: totalCount }) : t('subtitleZero')}
@@ -355,7 +357,7 @@ export default function ContactsPage() {
className="border-border text-muted-foreground hover:bg-muted"
>
- Custom fields
+ {t('customFieldsBtn')}
)}
- Import
+ {t('importBtn')}
- Add Contact
+ {t('addContactBtn')}
@@ -393,7 +395,7 @@ export default function ContactsPage() {
// set shrinks/grows, page N may no longer be valid.
setPage(0);
}}
- placeholder="Search by name, phone, or email..."
+ placeholder={t('searchPlaceholder')}
className="pl-8 bg-card border-border text-foreground placeholder:text-muted-foreground"
/>
@@ -408,7 +410,7 @@ export default function ContactsPage() {
}
>
- Filter by tags
+ {t('filterByTags')}
{selectedTagIds.length > 0 && (
{selectedTagIds.length}
@@ -418,20 +420,20 @@ export default function ContactsPage() {
- Filter by tags
+ {t('filterByTags')}
{selectedTagIds.length > 0 && (
)}
{allTags.length === 0 ? (
- No tags yet.
+ {t('noTagsYet')}
) : (
@@ -490,7 +492,7 @@ export default function ContactsPage() {
onClick={clearTagFilters}
className="text-xs text-muted-foreground hover:text-foreground px-1"
>
- Clear all
+ {t('clearAll')}
)}
@@ -500,8 +502,7 @@ export default function ContactsPage() {
{selected.size > 0 && (
- {selected.size}{' '}
- {selected.size === 1 ? 'contact' : 'contacts'} selected
+ {t('selectedCount', { count: selected.size })}
setBulkDeleteOpen(true)}
>
- Delete selected
+ {t('deleteSelected')}
@@ -540,12 +541,12 @@ export default function ContactsPage() {
aria-label="Select all contacts on this page"
/>
- Name
- Phone
- Email
- Company
- Tags
- Created
+ {t('tableColumns.name')}
+ {t('tableColumns.phone')}
+ {t('tableColumns.email')}
+ {t('tableColumns.company')}
+ {t('tableColumns.tags')}
+ {t('tableColumns.createdAt')}
@@ -555,7 +556,7 @@ export default function ContactsPage() {
-
Loading contacts...
+
{t('loading')}
@@ -566,8 +567,8 @@ export default function ContactsPage() {
{hasActiveFilters
- ? 'No contacts match your filters.'
- : 'No contacts yet.'}
+ ? t('noContactsMatch')
+ : t('noContactsYet')}
{!hasActiveFilters && (
- Add your first contact
+ {t('addFirstContact')}
)}
@@ -600,7 +601,7 @@ export default function ContactsPage() {
/>
- {contact.name || Unnamed}
+ {contact.name || {t('unnamed')}}
{contact.phone}
@@ -669,7 +670,7 @@ export default function ContactsPage() {
className="text-popover-foreground focus:bg-muted focus:text-foreground"
>
- Edit
+ {t('editAction')}
- Delete
+ {t('deleteAction')}
@@ -696,8 +697,11 @@ export default function ContactsPage() {
{totalPages > 1 && (
- Showing {page * PAGE_SIZE + 1}-{Math.min((page + 1) * PAGE_SIZE, totalCount)} of{' '}
- {totalCount}
+ {t('showingPagination', {
+ start: page * PAGE_SIZE + 1,
+ end: Math.min((page + 1) * PAGE_SIZE, totalCount),
+ total: totalCount
+ })}
- Page {page + 1} of {totalPages}
+ {t('pageCount', { page: page + 1, total: totalPages })}
@@ -802,14 +802,10 @@ export default function ContactsPage() {
- Delete {selected.size} {selected.size === 1 ? 'Contact' : 'Contacts'}
+ {t('deleteBulkTitle')}
- Are you sure you want to delete{' '}
-
- {selected.size} {selected.size === 1 ? 'contact' : 'contacts'}
-
- ? This action cannot be undone.
+ {t('deleteBulkDesc', { count: selected.size })}
@@ -818,7 +814,7 @@ export default function ContactsPage() {
onClick={() => setBulkDeleteOpen(false)}
className="border-border text-muted-foreground hover:bg-muted"
>
- Cancel
+ {t('cancel')}
diff --git a/src/components/contacts/contact-detail-view.tsx b/src/components/contacts/contact-detail-view.tsx
index 35a3982..5d50424 100644
--- a/src/components/contacts/contact-detail-view.tsx
+++ b/src/components/contacts/contact-detail-view.tsx
@@ -39,6 +39,7 @@ import {
DollarSign,
LayoutTemplate,
} from 'lucide-react';
+import { useTranslations } from 'next-intl';
interface ContactDetailViewProps {
open: boolean;
@@ -53,6 +54,7 @@ export function ContactDetailView({
contactId,
onUpdated,
}: ContactDetailViewProps) {
+ const t = useTranslations('Contacts.detailView');
const supabase = createClient();
const { accountId, defaultCurrency } = useAuth();
@@ -196,7 +198,7 @@ export function ContactDetailView({
async function saveDetails() {
if (!contactId || !editPhone.trim()) {
- toast.error('Phone number is required');
+ toast.error(t('toastPhoneRequired'));
return;
}
@@ -213,9 +215,9 @@ export function ContactDetailView({
.eq('id', contactId);
if (error) {
- toast.error('Failed to update contact');
+ toast.error(t('toastUpdateFailed'));
} else {
- toast.success('Contact updated');
+ toast.success(t('toastUpdated'));
fetchContact();
onUpdated();
}
@@ -259,7 +261,7 @@ export function ContactDetailView({
} = await supabase.auth.getSession();
const user = session?.user;
if (!user || !accountId) {
- toast.error('Not authenticated');
+ toast.error(t('toastNotAuthenticated'));
setSavingNote(false);
return;
}
@@ -272,11 +274,11 @@ export function ContactDetailView({
});
if (error) {
- toast.error('Failed to add note');
+ toast.error(t('toastNoteAddFailed'));
} else {
setNewNote('');
fetchNotes();
- toast.success('Note added');
+ toast.success(t('toastNoteAdded'));
}
setSavingNote(false);
}
@@ -288,10 +290,10 @@ export function ContactDetailView({
.eq('id', noteId);
if (error) {
- toast.error('Failed to delete note');
+ toast.error(t('toastNoteDeleteFailed'));
} else {
setNotes((prev) => prev.filter((n) => n.id !== noteId));
- toast.success('Note deleted');
+ toast.success(t('toastNoteDeleted'));
}
}
@@ -321,9 +323,9 @@ export function ContactDetailView({
if (error) throw error;
}
- toast.success('Custom fields saved');
+ toast.success(t('toastCustomFieldsSaved'));
} catch {
- toast.error('Failed to save custom fields');
+ toast.error(t('toastCustomFieldsFailed'));
}
setSavingCustom(false);
}
@@ -357,11 +359,11 @@ export function ContactDetailView({
const payload = await res.json().catch(() => ({}));
if (!res.ok) {
const reason = payload?.error || `HTTP ${res.status}`;
- toast.error(`Failed to send template: ${reason}`);
+ toast.error(t('toastTemplateFailed', { reason }));
return;
}
- toast.success(`Template "${template.name}" sent`);
+ toast.success(t('toastTemplateSent', { name: template.name }));
} catch (err) {
const reason = err instanceof Error ? err.message : 'network error';
toast.error(`Failed to send template: ${reason}`);
@@ -403,10 +405,10 @@ export function ContactDetailView({
- {contact.name || 'Unknown'}
+ {contact.name || t('unnamed')}
- Contact details
+ {t('contactDetailsDesc')}
)}
- Send template
+ {t('sendTemplateBtn')}
@@ -460,31 +462,31 @@ export function ContactDetailView({
value="details"
className="data-active:bg-muted data-active:text-primary text-muted-foreground"
>
- Details
+ {t('tabs.details')}
- Tags
+ {t('tabs.tags', { fallback: 'Tags' })}
- Notes
+ {t('tabs.notes')}
- Custom Fields
+ {t('tabs.custom')}
- Deals
+ {t('tabs.deals')}
@@ -492,7 +494,7 @@ export function ContactDetailView({
-
+
setEditName(e.target.value)}
@@ -501,7 +503,7 @@ export function ContactDetailView({
-
+
setEditEmail(e.target.value)}
@@ -518,7 +520,7 @@ export function ContactDetailView({
/>
-
+
setEditCompany(e.target.value)}
@@ -536,7 +538,7 @@ export function ContactDetailView({
) : (
)}
- Save Changes
+ {t('saveChangesBtn')}
@@ -545,11 +547,11 @@ export function ContactDetailView({
- Click a tag to add or remove it from this contact.
+ {t('tagsTab.clickTagDesc')}
{allTags.length === 0 ? (
- No tags available. Create tags in Settings.
+ {t('tagsTab.noTagsAvailable')}
) : (
@@ -586,7 +588,7 @@ export function ContactDetailView({
@@ -611,7 +613,7 @@ export function ContactDetailView({
) : notes.length === 0 ? (
- No notes yet.
+ {t('notesTab.noNotes')}
) : (
notes.map((note) => (
@@ -653,7 +655,7 @@ export function ContactDetailView({
) : customFields.length === 0 ? (
- No custom fields defined. Create them in Settings.
+ {t('noCustomFields')}
) : (
@@ -670,7 +672,7 @@ export function ContactDetailView({
[field.id]: e.target.value,
}))
}
- placeholder={`Enter ${field.field_name}...`}
+ placeholder={t('enterCustomField', { name: field.field_name })}
className="bg-muted border-border text-foreground h-8 text-sm placeholder:text-muted-foreground"
/>
@@ -686,7 +688,7 @@ export function ContactDetailView({
) : (
)}
- Save Custom Fields
+ {t('saveCustomFieldsBtn')}
)}
@@ -699,7 +701,7 @@ export function ContactDetailView({
) : deals.length === 0 ? (
-
No deals yet
+
{t('dealsTab.noDeals')}
) : (
{deals.map((deal) => (
diff --git a/src/components/contacts/contact-form.tsx b/src/components/contacts/contact-form.tsx
index b555be3..8c287f8 100644
--- a/src/components/contacts/contact-form.tsx
+++ b/src/components/contacts/contact-form.tsx
@@ -24,6 +24,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Loader2, AlertTriangle } from 'lucide-react';
+import { useTranslations } from 'next-intl';
interface ContactFormProps {
open: boolean;
@@ -44,6 +45,7 @@ export function ContactForm({
onSaved,
onViewExisting,
}: ContactFormProps) {
+ const t = useTranslations('Contacts.form');
const supabase = createClient();
const { accountId } = useAuth();
const isEdit = !!contact;
@@ -123,14 +125,14 @@ export function ContactForm({
e.preventDefault();
if (!phone.trim()) {
- toast.error('Phone number is required');
+ toast.error(t('phoneRequired'));
return;
}
// Hard-block an exact duplicate on create (the DB unique index is
// the real backstop; this avoids a round-trip + a raw error toast).
if (!isEdit && dupMatch?.exact) {
- toast.error('A contact with this phone number already exists');
+ toast.error(t('toastConflict'));
return;
}
@@ -194,7 +196,7 @@ export function ContactForm({
}
}
- toast.success(isEdit ? 'Contact updated' : 'Contact created');
+ toast.success(isEdit ? t('toastSuccessEdit') : t('toastSuccessAdd'));
onOpenChange(false);
onSaved();
} catch (err: unknown) {
@@ -203,7 +205,7 @@ export function ContactForm({
// normalizes equal). Surface it as the friendly duplicate notice
// and, for new contacts, point the user at the existing record.
if (isUniqueViolation(err)) {
- toast.error('A contact with this phone number already exists');
+ toast.error(t('toastConflict'));
if (!isEdit && accountId) {
const existing = await findExistingContact(
supabase,
@@ -214,7 +216,7 @@ export function ContactForm({
}
return;
}
- const message = err instanceof Error ? err.message : 'Failed to save contact';
+ const message = err instanceof Error ? err.message : t('toastError');
toast.error(message);
} finally {
setSaving(false);
@@ -226,32 +228,32 @@ export function ContactForm({
- {isEdit ? 'Edit Contact' : 'Add Contact'}
+ {isEdit ? t('editTitle') : t('addTitle')}
{isEdit
- ? 'Update the contact details below.'
- : 'Fill in the details to create a new contact.'}
+ ? t('editDesc')
+ : t('addDesc')}
setEmail(e.target.value)}
- placeholder="john@example.com"
+ placeholder={t('emailPlaceholder')}
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/>
setCompany(e.target.value)}
- placeholder="Acme Inc."
+ placeholder={t('companyPlaceholder')}
className="bg-muted border-border text-foreground placeholder:text-muted-foreground"
/>
-
+
{loadingTags ? (
- Loading tags...
+ {t('loadingTags')}
) : tags.length === 0 ? (
- No tags available. Create tags in Settings.
+ {t('noTagsAvailable')}
) : (
@@ -370,7 +372,7 @@ export function ContactForm({
onClick={() => onOpenChange(false)}
className="border-border text-muted-foreground hover:bg-muted"
>
- Cancel
+ {t('cancel')}
diff --git a/src/components/contacts/custom-fields-manager.tsx b/src/components/contacts/custom-fields-manager.tsx
index e46f960..df59737 100644
--- a/src/components/contacts/custom-fields-manager.tsx
+++ b/src/components/contacts/custom-fields-manager.tsx
@@ -15,6 +15,7 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Loader2, Plus, Trash2 } from 'lucide-react';
+import { useTranslations } from 'next-intl';
interface CustomFieldsManagerProps {
open: boolean;
@@ -31,15 +32,14 @@ export function CustomFieldsManager({
open,
onOpenChange,
}: CustomFieldsManagerProps) {
+ const t = useTranslations('Contacts.customFields');
return (
@@ -206,11 +207,11 @@ export function CustomFieldsPanel() {
{loading ? (
- Loading…
+ {t('loading')}
) : fields.length === 0 ? (
- No custom fields yet.
+ {t('empty')}
) : (
@@ -243,6 +244,7 @@ function FieldRow({
onRename: (field: CustomField, name: string) => Promise;
onDelete: (field: CustomField) => void;
}) {
+ const t = useTranslations('Contacts.customFields');
const [name, setName] = useState(field.field_name);
async function commit() {
@@ -264,7 +266,7 @@ function FieldRow({
onKeyDown={(e) => {
if (e.key === 'Enter') e.currentTarget.blur();
}}
- aria-label={`Rename ${field.field_name}`}
+ aria-label={t('renameAria', { name: field.field_name })}
className="focus:border-primary h-8 border-transparent bg-transparent text-foreground hover:border-border"
/>