Skip to content

Commit 0644482

Browse files
feat(i18n): add translations for API tokens page (en/es)
All hardcoded strings replaced with useTranslations('dashboard.admin.apiTokens'). Sidebar nav items now use t('apiTokens'). Date formatting uses locale-aware toLocaleDateString(undefined, ...). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent eee96b4 commit 0644482

4 files changed

Lines changed: 175 additions & 52 deletions

File tree

components/app-sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export function AppSidebar({ userRole, ...props }: AppSidebarProps) {
8383
{ title: t('transactions'), href: "/dashboard/admin/transactions", icon: IconCurrencyDollar },
8484
{ title: t('billing'), href: "/dashboard/admin/billing", icon: IconCreditCard },
8585
{ title: 'Landing Page', href: "/dashboard/admin/landing-page", icon: IconLayout },
86-
{ title: 'API Tokens', href: "/dashboard/admin/api-tokens", icon: IconKey },
86+
{ title: t('apiTokens'), href: "/dashboard/admin/api-tokens", icon: IconKey },
8787
],
8888
},
8989
teacher: {
@@ -93,7 +93,7 @@ export function AppSidebar({ userRole, ...props }: AppSidebarProps) {
9393
content: [
9494
{ title: t('myCourses'), href: "/dashboard/teacher/courses", icon: IconBook },
9595
{ title: t('createCourse'), href: "/dashboard/teacher/courses/new", icon: IconPlus },
96-
{ title: 'API Tokens', href: "/dashboard/teacher/api-tokens", icon: IconKey },
96+
{ title: t('apiTokens'), href: "/dashboard/teacher/api-tokens", icon: IconKey },
9797
],
9898
business: [
9999
{ title: t('revenue'), href: "/dashboard/teacher/revenue", icon: IconCurrencyDollar },

components/dashboard/api-tokens-page.tsx

Lines changed: 49 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useState, useTransition } from 'react'
4+
import { useTranslations } from 'next-intl'
45
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
56
import { Button } from '@/components/ui/button'
67
import { Input } from '@/components/ui/input'
@@ -12,7 +13,6 @@ import {
1213
DialogFooter,
1314
DialogHeader,
1415
DialogTitle,
15-
DialogTrigger,
1616
} from '@/components/ui/dialog'
1717
import {
1818
Select,
@@ -31,6 +31,7 @@ interface ApiTokensPageProps {
3131
}
3232

3333
export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
34+
const t = useTranslations('dashboard.admin.apiTokens')
3435
const [createOpen, setCreateOpen] = useState(false)
3536
const [revealedToken, setRevealedToken] = useState<string | null>(null)
3637
const [tokenName, setTokenName] = useState('')
@@ -42,7 +43,7 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
4243

4344
const handleCreate = () => {
4445
if (!tokenName.trim()) {
45-
toast.error('Please enter a token name')
46+
toast.error(t('toasts.nameRequired'))
4647
return
4748
}
4849
startTransition(async () => {
@@ -52,9 +53,9 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
5253
setRevealedToken(result.token)
5354
setTokenName('')
5455
setExpiration('never')
55-
toast.success('Token created successfully')
56+
toast.success(t('toasts.created'))
5657
} catch (err) {
57-
toast.error(err instanceof Error ? err.message : 'Failed to create token')
58+
toast.error(err instanceof Error ? err.message : t('toasts.createError'))
5859
}
5960
})
6061
}
@@ -63,9 +64,9 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
6364
startTransition(async () => {
6465
try {
6566
await revokeMcpToken(tokenId)
66-
toast.success('Token revoked')
67+
toast.success(t('toasts.revoked'))
6768
} catch (err) {
68-
toast.error(err instanceof Error ? err.message : 'Failed to revoke token')
69+
toast.error(err instanceof Error ? err.message : t('toasts.revokeError'))
6970
}
7071
})
7172
}
@@ -74,9 +75,9 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
7475
startTransition(async () => {
7576
try {
7677
await deleteMcpToken(tokenId)
77-
toast.success('Token deleted')
78+
toast.success(t('toasts.deleted'))
7879
} catch (err) {
79-
toast.error(err instanceof Error ? err.message : 'Failed to delete token')
80+
toast.error(err instanceof Error ? err.message : t('toasts.deleteError'))
8081
}
8182
})
8283
}
@@ -104,7 +105,7 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
104105
}, null, 2)
105106

106107
const formatDate = (dateStr: string) => {
107-
return new Date(dateStr).toLocaleDateString('en-US', {
108+
return new Date(dateStr).toLocaleDateString(undefined, {
108109
year: 'numeric',
109110
month: 'short',
110111
day: 'numeric',
@@ -121,11 +122,15 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
121122
{/* Header */}
122123
<div className="flex items-center justify-between">
123124
<div>
124-
<h1 className="text-2xl font-bold tracking-tight">API Tokens</h1>
125+
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
125126
<p className="text-sm text-muted-foreground mt-0.5">
126-
Generate tokens to connect Claude Desktop or other MCP clients to the LMS.
127+
{t('description')}
127128
</p>
128129
</div>
130+
<Button onClick={() => setCreateOpen(true)}>
131+
<IconPlus className="size-4 mr-1.5" />
132+
{t('createToken')}
133+
</Button>
129134
<Dialog open={createOpen} onOpenChange={(open) => {
130135
setCreateOpen(open)
131136
if (!open) {
@@ -134,17 +139,13 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
134139
setExpiration('never')
135140
}
136141
}}>
137-
<DialogTrigger render={<Button />}>
138-
<IconPlus className="size-4 mr-1.5" />
139-
Create Token
140-
</DialogTrigger>
141142
<DialogContent className="sm:max-w-md">
142143
{revealedToken ? (
143144
<>
144145
<DialogHeader>
145-
<DialogTitle>Token Created</DialogTitle>
146+
<DialogTitle>{t('revealDialog.title')}</DialogTitle>
146147
<DialogDescription>
147-
Copy this token now. It won&apos;t be shown again.
148+
{t('revealDialog.description')}
148149
</DialogDescription>
149150
</DialogHeader>
150151
<div className="space-y-4">
@@ -163,7 +164,7 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
163164
</Button>
164165
</div>
165166
<div>
166-
<Label className="text-xs text-muted-foreground mb-1.5 block">Claude Desktop config</Label>
167+
<Label className="text-xs text-muted-foreground mb-1.5 block">{t('revealDialog.configLabel')}</Label>
167168
<div className="relative">
168169
<pre className="bg-muted rounded-md p-3 text-xs overflow-x-auto">
169170
{configSnippet(revealedToken)}
@@ -180,45 +181,45 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
180181
</div>
181182
</div>
182183
<DialogFooter>
183-
<Button onClick={() => setCreateOpen(false)}>Done</Button>
184+
<Button onClick={() => setCreateOpen(false)}>{t('revealDialog.done')}</Button>
184185
</DialogFooter>
185186
</>
186187
) : (
187188
<>
188189
<DialogHeader>
189-
<DialogTitle>Create API Token</DialogTitle>
190+
<DialogTitle>{t('createDialog.title')}</DialogTitle>
190191
<DialogDescription>
191-
Generate a token for MCP client access.
192+
{t('createDialog.description')}
192193
</DialogDescription>
193194
</DialogHeader>
194195
<div className="space-y-4">
195196
<div className="space-y-2">
196-
<Label htmlFor="token-name">Token Name</Label>
197+
<Label htmlFor="token-name">{t('createDialog.nameLabel')}</Label>
197198
<Input
198199
id="token-name"
199-
placeholder="e.g., Claude Desktop"
200+
placeholder={t('createDialog.namePlaceholder')}
200201
value={tokenName}
201202
onChange={(e) => setTokenName(e.target.value)}
202203
/>
203204
</div>
204205
<div className="space-y-2">
205-
<Label htmlFor="expiration">Expiration</Label>
206+
<Label htmlFor="expiration">{t('createDialog.expirationLabel')}</Label>
206207
<Select value={expiration} onValueChange={(v) => v && setExpiration(v)}>
207208
<SelectTrigger id="expiration">
208209
<SelectValue />
209210
</SelectTrigger>
210211
<SelectContent>
211-
<SelectItem value="7">7 days</SelectItem>
212-
<SelectItem value="30">30 days</SelectItem>
213-
<SelectItem value="90">90 days</SelectItem>
214-
<SelectItem value="never">Never</SelectItem>
212+
<SelectItem value="7">{t('createDialog.expiration7d')}</SelectItem>
213+
<SelectItem value="30">{t('createDialog.expiration30d')}</SelectItem>
214+
<SelectItem value="90">{t('createDialog.expiration90d')}</SelectItem>
215+
<SelectItem value="never">{t('createDialog.expirationNever')}</SelectItem>
215216
</SelectContent>
216217
</Select>
217218
</div>
218219
</div>
219220
<DialogFooter>
220221
<Button onClick={handleCreate} disabled={isPending}>
221-
{isPending ? 'Creating...' : 'Create Token'}
222+
{isPending ? t('createDialog.creating') : t('createDialog.create')}
222223
</Button>
223224
</DialogFooter>
224225
</>
@@ -235,9 +236,9 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
235236
>
236237
<div className="flex items-center justify-between">
237238
<div>
238-
<CardTitle className="text-sm">How to Connect</CardTitle>
239+
<CardTitle className="text-sm">{t('howToConnect.title')}</CardTitle>
239240
<CardDescription>
240-
Set up Claude Desktop or other MCP clients to access your LMS data.
241+
{t('howToConnect.description')}
241242
</CardDescription>
242243
</div>
243244
{showInstructions
@@ -249,10 +250,10 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
249250
{showInstructions && (
250251
<CardContent className="text-sm space-y-3">
251252
<ol className="list-decimal list-inside space-y-2 text-muted-foreground">
252-
<li>Create a token above and copy the Claude Desktop config snippet.</li>
253-
<li>Open Claude Desktop settings and find the MCP servers configuration file.</li>
254-
<li>Paste the config snippet and save.</li>
255-
<li>Restart Claude Desktop — you should see the LMS tools available.</li>
253+
<li>{t('howToConnect.step1')}</li>
254+
<li>{t('howToConnect.step2')}</li>
255+
<li>{t('howToConnect.step3')}</li>
256+
<li>{t('howToConnect.step4')}</li>
256257
</ol>
257258
</CardContent>
258259
)}
@@ -261,11 +262,13 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
261262
{/* Token List */}
262263
<Card>
263264
<CardHeader>
264-
<CardTitle className="text-sm">Your Tokens</CardTitle>
265+
<CardTitle className="text-sm">{t('yourTokens.title')}</CardTitle>
265266
<CardDescription>
266267
{tokens.length === 0
267-
? 'No tokens yet. Create one to get started.'
268-
: `${tokens.length} token${tokens.length === 1 ? '' : 's'}`
268+
? t('yourTokens.empty')
269+
: tokens.length === 1
270+
? t('yourTokens.count', { count: tokens.length })
271+
: t('yourTokens.countPlural', { count: tokens.length })
269272
}
270273
</CardDescription>
271274
</CardHeader>
@@ -282,24 +285,24 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
282285
<span className="font-medium text-sm">{token.name}</span>
283286
{inactive && (
284287
<span className="text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded">
285-
Revoked
288+
{t('token.revoked')}
286289
</span>
287290
)}
288291
{!inactive && expired && (
289292
<span className="text-xs bg-destructive/10 text-destructive px-1.5 py-0.5 rounded">
290-
Expired
293+
{t('token.expired')}
291294
</span>
292295
)}
293296
{!inactive && !expired && (
294297
<span className="text-xs bg-green-500/10 text-green-600 dark:text-green-400 px-1.5 py-0.5 rounded">
295-
Active
298+
{t('token.active')}
296299
</span>
297300
)}
298301
</div>
299302
<div className="flex gap-3 text-xs text-muted-foreground">
300-
<span>Created {formatDate(token.created_at)}</span>
301-
{token.last_used_at && <span>Last used {formatDate(token.last_used_at)}</span>}
302-
{token.expires_at && <span>Expires {formatDate(token.expires_at)}</span>}
303+
<span>{t('token.created', { date: formatDate(token.created_at) })}</span>
304+
{token.last_used_at && <span>{t('token.lastUsed', { date: formatDate(token.last_used_at) })}</span>}
305+
{token.expires_at && <span>{t('token.expires', { date: formatDate(token.expires_at) })}</span>}
303306
</div>
304307
</div>
305308
<div className="flex items-center gap-1">
@@ -309,7 +312,7 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
309312
size="icon-sm"
310313
onClick={() => handleRevoke(token.id)}
311314
disabled={isPending}
312-
title="Revoke token"
315+
title={t('actions.revoke')}
313316
>
314317
<IconBan className="size-4" />
315318
</Button>
@@ -319,7 +322,7 @@ export default function ApiTokensPage({ tokens, domain }: ApiTokensPageProps) {
319322
size="icon-sm"
320323
onClick={() => handleDelete(token.id)}
321324
disabled={isPending}
322-
title="Delete token"
325+
title={t('actions.delete')}
323326
>
324327
<IconTrash className="size-4" />
325328
</Button>

messages/en.json

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,65 @@
16601660
"updateSuccess": "Product updated successfully"
16611661
}
16621662
}
1663+
},
1664+
"apiTokens": {
1665+
"title": "API Tokens",
1666+
"description": "Generate tokens to connect Claude Desktop or other MCP clients to the LMS.",
1667+
"createToken": "Create Token",
1668+
"howToConnect": {
1669+
"title": "How to Connect",
1670+
"description": "Set up Claude Desktop or other MCP clients to access your LMS data.",
1671+
"step1": "Create a token above and copy the Claude Desktop config snippet.",
1672+
"step2": "Open Claude Desktop settings and find the MCP servers configuration file.",
1673+
"step3": "Paste the config snippet and save.",
1674+
"step4": "Restart Claude Desktop — you should see the LMS tools available."
1675+
},
1676+
"yourTokens": {
1677+
"title": "Your Tokens",
1678+
"empty": "No tokens yet. Create one to get started.",
1679+
"count": "{count} token",
1680+
"countPlural": "{count} tokens"
1681+
},
1682+
"token": {
1683+
"active": "Active",
1684+
"revoked": "Revoked",
1685+
"expired": "Expired",
1686+
"created": "Created {date}",
1687+
"lastUsed": "Last used {date}",
1688+
"expires": "Expires {date}"
1689+
},
1690+
"createDialog": {
1691+
"title": "Create API Token",
1692+
"description": "Generate a token for MCP client access.",
1693+
"nameLabel": "Token Name",
1694+
"namePlaceholder": "e.g., Claude Desktop",
1695+
"expirationLabel": "Expiration",
1696+
"expiration7d": "7 days",
1697+
"expiration30d": "30 days",
1698+
"expiration90d": "90 days",
1699+
"expirationNever": "Never",
1700+
"creating": "Creating...",
1701+
"create": "Create Token"
1702+
},
1703+
"revealDialog": {
1704+
"title": "Token Created",
1705+
"description": "Copy this token now. It won't be shown again.",
1706+
"configLabel": "Claude Desktop config",
1707+
"done": "Done"
1708+
},
1709+
"toasts": {
1710+
"created": "Token created successfully",
1711+
"revoked": "Token revoked",
1712+
"deleted": "Token deleted",
1713+
"createError": "Failed to create token",
1714+
"revokeError": "Failed to revoke token",
1715+
"deleteError": "Failed to delete token",
1716+
"nameRequired": "Please enter a token name"
1717+
},
1718+
"actions": {
1719+
"revoke": "Revoke token",
1720+
"delete": "Delete token"
1721+
}
16631722
}
16641723
}
16651724
},
@@ -1973,7 +2032,8 @@
19732032
"management": "Management",
19742033
"business": "Business",
19752034
"revenue": "Revenue",
1976-
"billing": "Billing"
2035+
"billing": "Billing",
2036+
"apiTokens": "API Tokens"
19772037
},
19782038
"userNav": {
19792039
"profile": "Profile",
@@ -3080,4 +3140,4 @@
30803140
"termsOfService": "Terms of Service"
30813141
}
30823142
}
3083-
}
3143+
}

0 commit comments

Comments
 (0)