diff --git a/messages/en.json b/messages/en.json index bb441c9..859962b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1523,6 +1523,69 @@ "signingOut": "Signing out…", "signOutEverywhere": "Sign out everywhere", "signOutFailed": "Sign-out failed: {message}" + }, + "appearance": { + "title": "Appearance", + "description": "Set the mode and accent colour used across the app. Saved to this device — try it, it changes live.", + "mode": "Mode", + "useMode": "Use {mode} mode", + "active": "Active", + "accentColor": "Accent color", + "useTheme": "Use {name} theme" + }, + "security": { + "title": "Login & security", + "description": "Change your password and sign out of your devices. These keep your account safe." + }, + "apiKeys": { + "title": "API keys", + "description": "Keys authenticate the public REST API (/api/v1) so you can build your own automations. Send them as Authorization: Bearer <key>.", + "newApiKey": "New API key", + "noApiKeys": "No API keys yet.", + "createOneHint": "Click New API key to create one.", + "askAdminHint": "Ask an admin to create one.", + "revoked": "Revoked", + "expired": "Expired", + "noScopes": "No scopes", + "created": "Created {date}", + "lastUsed": "last used {date}", + "neverUsed": "never used", + "expires": "expires {date}", + "revoke": "Revoke", + "revokeSuccess": "Revoked \"{name}\"", + "revokeFailed": "Failed to revoke key", + "loadFailed": "Failed to load API keys", + "createError": "Failed to create key", + "nameRequired": "Give the key a name", + "nameLabel": "Name", + "namePlaceholder": "e.g. Zapier automation", + "scopesLabel": "Scopes", + "scopesHint": "A key with no scopes can still call GET /api/v1/me to verify it works.", + "cancel": "Cancel", + "creating": "Creating…", + "createKey": "Create key", + "copyTitle": "Copy your API key", + "copyDesc": "This is the only time the full key is shown. Store it somewhere safe — if you lose it, revoke it and create a new one.", + "apiKeyLabel": "API key", + "copy": "Copy", + "copySuccess": "API key copied", + "copyFailed": "Copy failed — select and copy manually", + "newKeyTitle": "New API key", + "newKeyDesc": "Name it after the integration that will use it, and grant only the scopes it needs.", + "done": "Done", + "networkError": "Could not reach the server" + }, + "deals": { + "title": "Deals & currency", + "description": "The currency used for new deals and for pipeline and dashboard totals.", + "defaultCurrency": "Default currency", + "defaultCurrencyDesc": "New deals default to this currency, and pipeline and dashboard totals are shown in it. Existing deals keep the currency they were saved with.", + "currencyLabel": "Currency", + "adminOnlyHint": "Only account admins can change the default currency.", + "save": "Save", + "saving": "Saving...", + "saveFailed": "Failed to save default currency", + "saveSuccess": "Default currency updated" } } } diff --git a/src/components/settings/api-keys-settings.tsx b/src/components/settings/api-keys-settings.tsx index 0efe9d3..22812af 100644 --- a/src/components/settings/api-keys-settings.tsx +++ b/src/components/settings/api-keys-settings.tsx @@ -40,6 +40,7 @@ import { SCOPE_DESCRIPTIONS, type ApiScope, } from '@/lib/api-keys/scopes'; +import { useTranslations } from 'next-intl'; import { SettingsPanelHead } from './settings-panel-head'; interface ApiKey { @@ -70,6 +71,7 @@ function keyStatus(k: ApiKey): 'active' | 'revoked' | 'expired' { export function ApiKeysSettings() { const { canEditSettings } = useAuth(); + const t = useTranslations('Settings.apiKeys'); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); @@ -81,14 +83,14 @@ export function ApiKeysSettings() { const res = await fetch('/api/account/api-keys', { cache: 'no-store' }); if (!res.ok) { const payload = await res.json().catch(() => ({})); - toast.error(payload.error || 'Failed to load API keys'); + toast.error(payload.error || t('loadFailed')); return; } const data = (await res.json()) as { keys: ApiKey[] }; setKeys(data.keys); } catch (err) { console.error('[ApiKeysSettings] load error:', err); - toast.error('Could not reach the server'); + toast.error(t('networkError')); } finally { setLoading(false); } @@ -106,10 +108,10 @@ export function ApiKeysSettings() { }); if (!res.ok) { const payload = await res.json().catch(() => ({})); - toast.error(payload.error || 'Failed to revoke key'); + toast.error(payload.error || t('revokeFailed')); return; } - toast.success(`Revoked "${key.name}"`); + toast.success(t('revokeSuccess', { name: key.name })); // Reflect the revoke locally without a refetch. setKeys((prev) => prev.map((k) => @@ -118,7 +120,7 @@ export function ApiKeysSettings() { ); } catch (err) { console.error('[ApiKeysSettings] revoke error:', err); - toast.error('Could not reach the server'); + toast.error(t('networkError')); } finally { setRevoking(null); } @@ -135,20 +137,18 @@ export function ApiKeysSettings() { return (
- Keys authenticate the public REST API ( - /api/v1) so you can build your own - automations. Send them as{' '} - Authorization: Bearer <key>. - + t.rich('description', { + apiCode: (chunks: React.ReactNode) => {chunks}, + headerCode: (chunks: React.ReactNode) => {chunks} + }) } action={ } @@ -159,16 +159,19 @@ export function ApiKeysSettings() {

- No API keys yet. + {t('noApiKeys')}

{canEditSettings ? (

- Click New API key to - create one. + {t.rich('createOneHint', { + bold: (chunks: React.ReactNode) => ( + {chunks} + ), + })}

) : (

- Ask an admin to create one. + {t('askAdminHint')}

)}
@@ -198,12 +201,12 @@ export function ApiKeysSettings() { {status === 'revoked' && ( - Revoked + {t('revoked')} )} {status === 'expired' && ( - Expired + {t('expired')} )} @@ -213,7 +216,7 @@ export function ApiKeysSettings() {
{k.scopes.length === 0 ? ( - No scopes + {t('noScopes')} ) : ( k.scopes.map((s) => ( @@ -227,13 +230,13 @@ export function ApiKeysSettings() { )}

- Created {fmtDate(k.created_at)} + {t('created', { date: fmtDate(k.created_at) })} {' · '} {k.last_used_at - ? `last used ${fmtDate(k.last_used_at)}` - : 'never used'} + ? t('lastUsed', { date: fmtDate(k.last_used_at) }) + : t('neverUsed')} {k.expires_at && status !== 'expired' - ? ` · expires ${fmtDate(k.expires_at)}` + ? ` · ${t('expires', { date: fmtDate(k.expires_at) })}` : ''}

@@ -252,7 +255,7 @@ export function ApiKeysSettings() { ) : ( )} - Revoke + {t('revoke')} )} @@ -286,6 +289,7 @@ function CreateKeyDialog({ onOpenChange: (open: boolean) => void; onCreated: () => void; }) { + const t = useTranslations('Settings.apiKeys'); const [name, setName] = useState(''); const [scopes, setScopes] = useState([]); const [submitting, setSubmitting] = useState(false); @@ -308,7 +312,7 @@ function CreateKeyDialog({ async function handleCreate() { const trimmed = name.trim(); if (!trimmed) { - toast.error('Give the key a name'); + toast.error(t('nameRequired')); return; } setSubmitting(true); @@ -320,14 +324,14 @@ function CreateKeyDialog({ }); const payload = await res.json().catch(() => ({})); if (!res.ok) { - toast.error(payload.error || 'Failed to create key'); + toast.error(payload.error || t('createError')); return; } setCreatedKey(payload.plaintext as string); onCreated(); } catch (err) { console.error('[CreateKeyDialog] create error:', err); - toast.error('Could not reach the server'); + toast.error(t('networkError')); } finally { setSubmitting(false); } @@ -337,9 +341,9 @@ function CreateKeyDialog({ if (!createdKey) return; try { await navigator.clipboard.writeText(createdKey); - toast.success('API key copied'); + toast.success(t('copySuccess')); } catch { - toast.error('Copy failed — select and copy manually'); + toast.error(t('copyFailed')); } } @@ -356,16 +360,15 @@ function CreateKeyDialog({ <> - Copy your API key + {t('copyTitle')} - This is the only time the full key is shown. Store it somewhere - safe — if you lose it, revoke it and create a new one. + {t('copyDesc')}
- +
@@ -387,7 +390,7 @@ function CreateKeyDialog({ onOpenChange(false); }} > - Done + {t('done')} @@ -395,30 +398,29 @@ function CreateKeyDialog({ <> - New API key + {t('newKeyTitle')} - Name it after the integration that will use it, and grant only - the scopes it needs. + {t('newKeyDesc')}
setName(e.target.value)} />
- +
{API_SCOPES.map((scope) => (
@@ -460,16 +464,16 @@ function CreateKeyDialog({ }} className="border-border text-muted-foreground hover:bg-muted" > - Cancel + {t('cancel')} diff --git a/src/components/settings/appearance-panel.tsx b/src/components/settings/appearance-panel.tsx index 0a83bd7..c9cf60a 100644 --- a/src/components/settings/appearance-panel.tsx +++ b/src/components/settings/appearance-panel.tsx @@ -5,6 +5,7 @@ import { Check, Moon, Palette, SunMoon, Sun } from "lucide-react"; import { useTheme } from "@/hooks/use-theme"; import { MODES, THEMES, type Mode, type ThemeId } from "@/lib/themes"; import { cn } from "@/lib/utils"; +import { useTranslations } from "next-intl"; import { SettingsPanelHead } from "./settings-panel-head"; /** @@ -21,17 +22,19 @@ import { SettingsPanelHead } from "./settings-panel-head"; */ export function AppearancePanel() { const { theme, setTheme, mode, setMode } = useTheme(); + const t = useTranslations("Settings.appearance"); + return (

- Mode + {t("mode")}

- Accent color + {t("accentColor")}

- {THEMES.map((t) => ( + {THEMES.map((tObj) => ( setTheme(t.id)} + key={tObj.id} + id={tObj.id} + name={tObj.name} + tagline={tObj.tagline} + swatch={tObj.swatch} + isActive={tObj.id === theme} + onPick={() => setTheme(tObj.id)} /> ))}
@@ -83,6 +86,7 @@ function ModeCard({ isActive: boolean; onPick: () => void; }) { + const t = useTranslations("Settings.appearance"); const isLight = mode === "light"; const Icon = isLight ? Sun : Moon; return ( @@ -91,7 +95,7 @@ function ModeCard({ role="radio" onClick={onPick} aria-checked={isActive} - aria-label={`Use ${mode} mode`} + aria-label={t("useMode", { mode })} className={cn( "flex items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors", isActive @@ -111,7 +115,7 @@ function ModeCard({ {isActive && ( - Active + {t("active")} )} @@ -133,12 +137,13 @@ function ThemeCard({ isActive: boolean; onPick: () => void; }) { + const t = useTranslations("Settings.appearance"); return (
diff --git a/src/components/settings/deals-settings.tsx b/src/components/settings/deals-settings.tsx index 1e8d1a4..f85a92a 100644 --- a/src/components/settings/deals-settings.tsx +++ b/src/components/settings/deals-settings.tsx @@ -16,6 +16,7 @@ import { CardTitle, CardDescription, } from "@/components/ui/card"; +import { useTranslations } from "next-intl"; import { SettingsPanelHead } from "./settings-panel-head"; /** @@ -39,6 +40,7 @@ export function DealsSettings() { const [selected, setSelected] = useState(defaultCurrency); const [saving, setSaving] = useState(false); + const t = useTranslations("Settings.deals"); // Keep the select in sync once the profile (and its account default) // resolves, and after a save round-trips through refreshProfile. @@ -56,7 +58,7 @@ export function DealsSettings() { .update({ default_currency: selected }) .eq("id", accountId); if (error) { - toast.error("Failed to save default currency"); + toast.error(t("saveFailed")); setSaving(false); return; } @@ -64,30 +66,28 @@ export function DealsSettings() { // and every total pick it up without a full reload. await refreshProfile(); setSaving(false); - toast.success("Default currency updated"); + toast.success(t("saveSuccess")); } return (
- Default currency + {t("defaultCurrency")} - New deals default to this currency, and pipeline and - dashboard totals are shown in it. Existing deals keep the - currency they were saved with. + {t("defaultCurrencyDesc")}
- + {!canEditSettings && (

- Only account admins can change the default currency. + {t("adminOnlyHint")}

)}
@@ -116,10 +116,10 @@ export function DealsSettings() { {saving ? ( <> - Saving... + {t("saving")} ) : ( - "Save" + t("save") )} )} diff --git a/src/components/settings/security-panel.tsx b/src/components/settings/security-panel.tsx index 948274a..d3477a6 100644 --- a/src/components/settings/security-panel.tsx +++ b/src/components/settings/security-panel.tsx @@ -3,17 +3,19 @@ import { PasswordForm } from './password-form'; import { SessionsCard } from './sessions-card'; import { SettingsPanelHead } from './settings-panel-head'; +import { useTranslations } from 'next-intl'; /** * "Login & security" section — groups the former Profile-tab password * and active-sessions cards into their own dedicated home. */ export function SecurityPanel() { + const t = useTranslations('Settings.security'); return (