11'use client'
22
33import { useState , useTransition } from 'react'
4+ import { useTranslations } from 'next-intl'
45import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card'
56import { Button } from '@/components/ui/button'
67import { Input } from '@/components/ui/input'
@@ -12,7 +13,6 @@ import {
1213 DialogFooter ,
1314 DialogHeader ,
1415 DialogTitle ,
15- DialogTrigger ,
1616} from '@/components/ui/dialog'
1717import {
1818 Select ,
@@ -31,6 +31,7 @@ interface ApiTokensPageProps {
3131}
3232
3333export 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'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 >
0 commit comments