@djangocfg/ext-knowbase 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { List, Maximize2, MessageSquare, Minimize2, Plus, X } from 'lucide-react';
9
9
  import { usePathname } from 'next/navigation';
10
- import React, { useCallback, useEffect, useState } from 'react';
10
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
11
11
  import { createPortal } from 'react-dom';
12
12
 
13
+ import { useKnowbaseT } from '../../i18n';
13
14
  import {
14
15
  Button, Card, CardContent, CardHeader,
15
16
  } from '@djangocfg/ui-nextjs';
@@ -37,6 +38,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
37
38
  onToggle,
38
39
  onMessage,
39
40
  }) => {
41
+ const kt = useKnowbaseT();
40
42
  const { sendQuery, getChatHistory } = useKnowbaseChatContext();
41
43
  const { createSession, getSession } = useKnowbaseSessionsContext();
42
44
  const {
@@ -48,9 +50,21 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
48
50
  toggleTimestamps,
49
51
  } = useChatUI();
50
52
 
53
+ const labels = useMemo(() => ({
54
+ title: kt('chat.title'),
55
+ titleShort: kt('chat.titleShort'),
56
+ placeholder: kt('chat.placeholder'),
57
+ openChat: kt('chat.openChat'),
58
+ sessions: kt('actions.sessions'),
59
+ newChat: kt('sessions.newChat'),
60
+ collapse: kt('actions.collapse'),
61
+ expand: kt('actions.expand'),
62
+ close: kt('actions.close'),
63
+ }), [kt]);
64
+
51
65
  const pathname = usePathname();
52
66
  const isMobile = useIsMobile();
53
-
67
+
54
68
  const [isLoading, setIsLoading] = useState(false);
55
69
  const [showSessions, setShowSessions] = useState(false);
56
70
  const [mounted, setMounted] = useState(false);
@@ -233,7 +247,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
233
247
  <div className="flex items-center justify-between p-4 border-b bg-background shadow-sm">
234
248
  <div className="flex items-center gap-2">
235
249
  <MessageSquare className="h-5 w-5 text-primary" />
236
- <h2 className="font-semibold text-foreground">Knowledge Assistant</h2>
250
+ <h2 className="font-semibold text-foreground">{labels.title}</h2>
237
251
  </div>
238
252
 
239
253
  <div className="flex items-center gap-2">
@@ -242,7 +256,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
242
256
  size="sm"
243
257
  onClick={() => setShowSessions(true)}
244
258
  className="text-muted-foreground hover:text-foreground"
245
- title="Sessions"
259
+ title={labels.sessions}
246
260
  >
247
261
  <List className="h-5 w-5" />
248
262
  </Button>
@@ -252,7 +266,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
252
266
  size="sm"
253
267
  onClick={handleNewChat}
254
268
  className="text-muted-foreground hover:text-foreground"
255
- title="New Chat"
269
+ title={labels.newChat}
256
270
  >
257
271
  <Plus className="h-5 w-5" />
258
272
  </Button>
@@ -282,7 +296,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
282
296
  <MessageInput
283
297
  onSend={handleSendMessage}
284
298
  isLoading={isLoading}
285
- placeholder="Ask me anything..."
299
+ placeholder={labels.placeholder}
286
300
  />
287
301
  </div>
288
302
  </div>
@@ -342,7 +356,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
342
356
  <CardHeader className="flex-row items-center justify-between space-y-0 pb-3 border-b">
343
357
  <div className="flex items-center gap-2">
344
358
  <MessageSquare className="h-5 w-5 text-primary" />
345
- <h3 className="font-semibold text-foreground">Support</h3>
359
+ <h3 className="font-semibold text-foreground">{labels.titleShort}</h3>
346
360
  </div>
347
361
 
348
362
  <div className="flex items-center gap-1">
@@ -351,7 +365,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
351
365
  size="sm"
352
366
  onClick={() => setShowSessions(true)}
353
367
  className="h-8 w-8 p-0"
354
- title="Sessions"
368
+ title={labels.sessions}
355
369
  >
356
370
  <List className="h-4 w-4" />
357
371
  </Button>
@@ -361,7 +375,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
361
375
  size="sm"
362
376
  onClick={handleNewChat}
363
377
  className="h-8 w-8 p-0"
364
- title="New Chat"
378
+ title={labels.newChat}
365
379
  >
366
380
  <Plus className="h-4 w-4" />
367
381
  </Button>
@@ -371,7 +385,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
371
385
  size="sm"
372
386
  onClick={uiState.isExpanded ? collapseChat : expandChat}
373
387
  className="h-8 w-8 p-0"
374
- title={uiState.isExpanded ? 'Collapse' : 'Expand'}
388
+ title={uiState.isExpanded ? labels.collapse : labels.expand}
375
389
  >
376
390
  {uiState.isExpanded ? (
377
391
  <Minimize2 className="h-4 w-4" />
@@ -385,7 +399,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
385
399
  size="sm"
386
400
  onClick={toggleChat}
387
401
  className="h-8 w-8 p-0"
388
- title="Close"
402
+ title={labels.close}
389
403
  >
390
404
  <X className="h-4 w-4" />
391
405
  </Button>
@@ -406,7 +420,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
406
420
  <MessageInput
407
421
  onSend={handleSendMessage}
408
422
  isLoading={isLoading}
409
- placeholder="Ask me anything..."
423
+ placeholder={labels.placeholder}
410
424
  />
411
425
  </CardContent>
412
426
  </Card>
@@ -439,8 +453,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
439
453
  `}</style>
440
454
  <Button
441
455
  onClick={toggleChat}
442
- className="w-full h-full rounded-full shadow-2xl
443
- hover:scale-110 hover:rotate-12 active:scale-95
456
+ className="w-full h-full rounded-full shadow-2xl
457
+ hover:scale-110 hover:rotate-12 active:scale-95
444
458
  transition-all duration-300 ease-out
445
459
  flex items-center justify-center
446
460
  group"
@@ -449,8 +463,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
449
463
  borderRadius: '50%',
450
464
  padding: 0,
451
465
  }}
452
- title="Open Support Chat"
453
- aria-label="Open Support Chat"
466
+ title={labels.openChat}
467
+ aria-label={labels.openChat}
454
468
  >
455
469
  <MessageSquare
456
470
  className="h-7 w-7 text-primary-foreground group-hover:scale-110 transition-transform duration-300"
@@ -6,8 +6,9 @@
6
6
  'use client';
7
7
 
8
8
  import { Loader2, Send } from 'lucide-react';
9
- import React, { useCallback, useEffect, useRef, useState } from 'react';
9
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
10
 
11
+ import { useKnowbaseT } from '../../../i18n';
11
12
  import { Button, Textarea } from '@djangocfg/ui-nextjs';
12
13
 
13
14
  import { chatLogger } from '../../../utils/logger';
@@ -22,13 +23,19 @@ export const MessageInput: React.FC<MessageInputProps> = ({
22
23
  onSend,
23
24
  isLoading = false,
24
25
  disabled = false,
25
- placeholder = 'Ask me anything...',
26
+ placeholder,
26
27
  className = '',
27
28
  }) => {
29
+ const kt = useKnowbaseT();
28
30
  const [message, setMessage] = useState('');
29
31
  const [rows, setRows] = useState(1);
30
32
  const textareaRef = useRef<HTMLTextAreaElement>(null);
31
33
 
34
+ const labels = useMemo(() => ({
35
+ placeholder: placeholder || kt('chat.placeholder'),
36
+ enterToSend: kt('chat.enterToSend'),
37
+ }), [kt, placeholder]);
38
+
32
39
  // Auto-resize textarea based on content
33
40
  useEffect(() => {
34
41
  if (textareaRef.current) {
@@ -95,19 +102,19 @@ export const MessageInput: React.FC<MessageInputProps> = ({
95
102
  value={message}
96
103
  onChange={(e) => setMessage(e.target.value)}
97
104
  onKeyDown={handleKeyDown}
98
- placeholder={placeholder}
105
+ placeholder={labels.placeholder}
99
106
  disabled={isDisabled}
100
107
  rows={rows}
101
- className="resize-none min-h-[40px] max-h-[120px] transition-all duration-200
108
+ className="resize-none min-h-[40px] max-h-[120px] transition-all duration-200
102
109
  focus:ring-2 focus:ring-primary/20"
103
110
  style={{ resize: 'none' }}
104
111
  />
105
-
112
+
106
113
  <Button
107
114
  type="submit"
108
115
  size="icon"
109
116
  disabled={!canSend}
110
- className="shrink-0 h-10 w-10 transition-all duration-200
117
+ className="shrink-0 h-10 w-10 transition-all duration-200
111
118
  hover:scale-110 active:scale-95 disabled:scale-100"
112
119
  >
113
120
  {isLoading ? (
@@ -117,9 +124,9 @@ export const MessageInput: React.FC<MessageInputProps> = ({
117
124
  )}
118
125
  </Button>
119
126
  </div>
120
-
127
+
121
128
  <p className="text-xs text-muted-foreground mt-2">
122
- Press Enter to send, Shift+Enter for new line
129
+ {labels.enterToSend}
123
130
  </p>
124
131
  </form>
125
132
  );
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { Bot, ExternalLink, Loader2, User } from 'lucide-react';
9
9
  import moment from 'moment';
10
- import React, { useEffect, useRef } from 'react';
10
+ import React, { useEffect, useMemo, useRef } from 'react';
11
11
 
12
12
  import { useAuth } from '@djangocfg/api/auth';
13
+ import { useKnowbaseT } from '../../../i18n';
13
14
  import {
14
15
  Avatar, AvatarFallback, AvatarImage, Badge, Card, CardContent, ScrollArea
15
16
  } from '@djangocfg/ui-nextjs';
@@ -29,9 +30,17 @@ export const MessageList: React.FC<MessageListProps> = ({
29
30
  autoScroll = true,
30
31
  className = '',
31
32
  }) => {
33
+ const kt = useKnowbaseT();
32
34
  const scrollRef = useRef<HTMLDivElement>(null);
33
35
  const { user } = useAuth();
34
36
 
37
+ const labels = useMemo(() => ({
38
+ startConversation: kt('chat.startConversation'),
39
+ startConversationDescription: kt('chat.startConversationDescription'),
40
+ source: kt('chat.source'),
41
+ thinking: kt('chat.thinking'),
42
+ }), [kt]);
43
+
35
44
  // Auto-scroll to bottom on new messages
36
45
  useEffect(() => {
37
46
  if (autoScroll && scrollRef.current) {
@@ -58,10 +67,10 @@ export const MessageList: React.FC<MessageListProps> = ({
58
67
  <div className="flex flex-col items-center justify-center h-full text-center py-12">
59
68
  <Bot className="h-12 w-12 text-muted-foreground mb-4" />
60
69
  <h3 className="text-lg font-semibold text-foreground mb-2">
61
- Start a Conversation
70
+ {labels.startConversation}
62
71
  </h3>
63
72
  <p className="text-sm text-muted-foreground max-w-md">
64
- Ask me anything about the documentation, features, or get help with your project.
73
+ {labels.startConversationDescription}
65
74
  </p>
66
75
  </div>
67
76
  ) : (
@@ -123,12 +132,12 @@ export const MessageList: React.FC<MessageListProps> = ({
123
132
  <div key={idx}>
124
133
  <Badge
125
134
  variant="secondary"
126
- className="text-xs flex items-center gap-1 cursor-pointer
127
- hover:bg-secondary/80 hover:scale-105 active:scale-95
135
+ className="text-xs flex items-center gap-1 cursor-pointer
136
+ hover:bg-secondary/80 hover:scale-105 active:scale-95
128
137
  transition-all duration-200 animate-in fade-in zoom-in-95"
129
138
  style={{ animationDelay: `${(idx + 1) * 100}ms` }}
130
139
  >
131
- {source.document_title || `Source ${idx + 1}`}
140
+ {source.document_title || labels.source.replace('{index}', String(idx + 1))}
132
141
  <ExternalLink className="h-3 w-3" />
133
142
  </Badge>
134
143
  </div>
@@ -163,7 +172,7 @@ export const MessageList: React.FC<MessageListProps> = ({
163
172
  <CardContent className="p-3">
164
173
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
165
174
  <Loader2 className="h-4 w-4 animate-spin" />
166
- <span className="animate-pulse">Thinking...</span>
175
+ <span className="animate-pulse">{labels.thinking}</span>
167
176
  </div>
168
177
  </CardContent>
169
178
  </Card>
@@ -7,8 +7,9 @@
7
7
 
8
8
  import { Archive, Clock, Loader2, MessageSquare, Trash2 } from 'lucide-react';
9
9
  import moment from 'moment';
10
- import React, { useCallback, useEffect, useRef } from 'react';
10
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
11
11
 
12
+ import { useKnowbaseT } from '../../../i18n';
12
13
  import {
13
14
  Badge, Button, ScrollArea, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle
14
15
  } from '@djangocfg/ui-nextjs';
@@ -28,6 +29,7 @@ export const SessionList: React.FC<SessionListProps> = ({
28
29
  onSelectSession,
29
30
  className = '',
30
31
  }) => {
32
+ const kt = useKnowbaseT();
31
33
  const { deleteSession, archiveSession } = useKnowbaseSessionsContext();
32
34
  const {
33
35
  sessions,
@@ -36,7 +38,20 @@ export const SessionList: React.FC<SessionListProps> = ({
36
38
  hasMore,
37
39
  loadMore,
38
40
  } = useInfiniteSessions();
39
-
41
+
42
+ const labels = useMemo(() => ({
43
+ title: kt('sessions.title'),
44
+ description: kt('sessions.description'),
45
+ noSessions: kt('sessions.noSessions'),
46
+ noSessionsDescription: kt('sessions.noSessionsDescription'),
47
+ untitled: kt('sessions.untitled'),
48
+ active: kt('sessions.active'),
49
+ archive: kt('sessions.archive'),
50
+ delete: kt('sessions.delete'),
51
+ loadingMore: kt('sessions.loadingMore'),
52
+ noMore: kt('sessions.noMore'),
53
+ }), [kt]);
54
+
40
55
  const scrollRef = useRef<HTMLDivElement>(null);
41
56
 
42
57
  // Handle scroll to load more
@@ -72,9 +87,9 @@ export const SessionList: React.FC<SessionListProps> = ({
72
87
  <Sheet open={isOpen} onOpenChange={onClose}>
73
88
  <SheetContent side="left" className={`w-[400px] sm:w-[540px] ${className}`}>
74
89
  <SheetHeader>
75
- <SheetTitle>Chat Sessions</SheetTitle>
90
+ <SheetTitle>{labels.title}</SheetTitle>
76
91
  <SheetDescription>
77
- View and manage your chat history
92
+ {labels.description}
78
93
  </SheetDescription>
79
94
  </SheetHeader>
80
95
 
@@ -87,10 +102,10 @@ export const SessionList: React.FC<SessionListProps> = ({
87
102
  <div className="flex flex-col items-center justify-center py-12 text-center">
88
103
  <MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
89
104
  <h3 className="text-lg font-semibold text-foreground mb-2">
90
- No Sessions Yet
105
+ {labels.noSessions}
91
106
  </h3>
92
107
  <p className="text-sm text-muted-foreground max-w-md">
93
- Start a new conversation to create your first session.
108
+ {labels.noSessionsDescription}
94
109
  </p>
95
110
  </div>
96
111
  ) : (
@@ -117,11 +132,11 @@ export const SessionList: React.FC<SessionListProps> = ({
117
132
  <div className="flex-1 min-w-0">
118
133
  <div className="flex items-start justify-between gap-2 mb-1">
119
134
  <h4 className="font-medium text-sm truncate">
120
- {session.title || 'Untitled Session'}
135
+ {session.title || labels.untitled}
121
136
  </h4>
122
137
  {session.is_active && (
123
138
  <Badge variant="default" className="shrink-0 text-xs">
124
- Active
139
+ {labels.active}
125
140
  </Badge>
126
141
  )}
127
142
  </div>
@@ -143,20 +158,20 @@ export const SessionList: React.FC<SessionListProps> = ({
143
158
  e.stopPropagation();
144
159
  archiveSession(session.id, {});
145
160
  }}
146
- title="Archive"
161
+ title={labels.archive}
147
162
  >
148
163
  <Archive className="h-4 w-4" />
149
164
  </Button>
150
165
  <Button
151
166
  variant="ghost"
152
167
  size="sm"
153
- className="h-8 w-8 p-0 text-destructive hover:text-destructive
168
+ className="h-8 w-8 p-0 text-destructive hover:text-destructive
154
169
  hover:scale-110 active:scale-95 transition-transform duration-200"
155
170
  onClick={(e) => {
156
171
  e.stopPropagation();
157
172
  deleteSession(session.id);
158
173
  }}
159
- title="Delete"
174
+ title={labels.delete}
160
175
  >
161
176
  <Trash2 className="h-4 w-4" />
162
177
  </Button>
@@ -168,14 +183,14 @@ export const SessionList: React.FC<SessionListProps> = ({
168
183
  {isLoadingMore && (
169
184
  <div className="flex items-center justify-center py-6">
170
185
  <Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
171
- <span className="text-sm text-muted-foreground">Loading more...</span>
186
+ <span className="text-sm text-muted-foreground">{labels.loadingMore}</span>
172
187
  </div>
173
188
  )}
174
-
189
+
175
190
  {/* No more sessions indicator */}
176
191
  {!hasMore && sessions.length > 0 && (
177
192
  <div className="text-center py-4">
178
- <span className="text-xs text-muted-foreground">No more sessions</span>
193
+ <span className="text-xs text-muted-foreground">{labels.noMore}</span>
179
194
  </div>
180
195
  )}
181
196
  </div>
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Knowbase Extension I18n
3
+ *
4
+ * Self-contained translations - no app configuration needed.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { useKnowbaseT } from '@djangocfg/ext-knowbase/i18n';
9
+ *
10
+ * function MyComponent() {
11
+ * const t = useKnowbaseT();
12
+ * return <span>{t('chat.title')}</span>;
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ // Self-contained hook (recommended)
18
+ export { useKnowbaseT } from './useKnowbaseT';
19
+
20
+ // Types
21
+ export type { KnowbaseTranslations, KnowbaseLocalKeys } from './types';
22
+
23
+ // Locales (for direct access if needed)
24
+ export { en } from './locales/en';
25
+ export { ru } from './locales/ru';
26
+ export { ko } from './locales/ko';
@@ -0,0 +1,36 @@
1
+ import type { KnowbaseTranslations } from '../types';
2
+
3
+ export const en: KnowbaseTranslations = {
4
+ chat: {
5
+ title: 'Knowledge Assistant',
6
+ titleShort: 'Support',
7
+ placeholder: 'Ask me anything...',
8
+ enterToSend: 'Press Enter to send, Shift+Enter for new line',
9
+ thinking: 'Thinking...',
10
+ source: 'Source {index}',
11
+ startConversation: 'Start a Conversation',
12
+ startConversationDescription: 'Ask me anything about the documentation, features, or get help with your project.',
13
+ openChat: 'Open Support Chat',
14
+ },
15
+
16
+ sessions: {
17
+ title: 'Chat Sessions',
18
+ description: 'View and manage your chat history',
19
+ noSessions: 'No Sessions Yet',
20
+ noSessionsDescription: 'Start a new conversation to create your first session.',
21
+ untitled: 'Untitled Session',
22
+ active: 'Active',
23
+ archive: 'Archive',
24
+ delete: 'Delete',
25
+ loadingMore: 'Loading more...',
26
+ noMore: 'No more sessions',
27
+ newChat: 'New Chat',
28
+ },
29
+
30
+ actions: {
31
+ sessions: 'Sessions',
32
+ collapse: 'Collapse',
33
+ expand: 'Expand',
34
+ close: 'Close',
35
+ },
36
+ };
@@ -0,0 +1,36 @@
1
+ import type { KnowbaseTranslations } from '../types';
2
+
3
+ export const ko: KnowbaseTranslations = {
4
+ chat: {
5
+ title: '지식 도우미',
6
+ titleShort: '지원',
7
+ placeholder: '무엇이든 물어보세요...',
8
+ enterToSend: 'Enter로 전송, Shift+Enter로 줄바꿈',
9
+ thinking: '생각 중...',
10
+ source: '출처 {index}',
11
+ startConversation: '대화 시작하기',
12
+ startConversationDescription: '문서, 기능에 대해 질문하거나 프로젝트 도움을 받으세요.',
13
+ openChat: '지원 채팅 열기',
14
+ },
15
+
16
+ sessions: {
17
+ title: '채팅 세션',
18
+ description: '채팅 기록 보기 및 관리',
19
+ noSessions: '세션 없음',
20
+ noSessionsDescription: '새 대화를 시작하여 첫 번째 세션을 만드세요.',
21
+ untitled: '제목 없음',
22
+ active: '활성',
23
+ archive: '보관',
24
+ delete: '삭제',
25
+ loadingMore: '불러오는 중...',
26
+ noMore: '더 이상 세션 없음',
27
+ newChat: '새 채팅',
28
+ },
29
+
30
+ actions: {
31
+ sessions: '세션',
32
+ collapse: '축소',
33
+ expand: '확장',
34
+ close: '닫기',
35
+ },
36
+ };
@@ -0,0 +1,36 @@
1
+ import type { KnowbaseTranslations } from '../types';
2
+
3
+ export const ru: KnowbaseTranslations = {
4
+ chat: {
5
+ title: 'База знаний',
6
+ titleShort: 'Поддержка',
7
+ placeholder: 'Задайте вопрос...',
8
+ enterToSend: 'Enter для отправки, Shift+Enter для новой строки',
9
+ thinking: 'Думаю...',
10
+ source: 'Источник {index}',
11
+ startConversation: 'Начать разговор',
12
+ startConversationDescription: 'Задайте любой вопрос о документации, функциях или получите помощь с проектом.',
13
+ openChat: 'Открыть чат поддержки',
14
+ },
15
+
16
+ sessions: {
17
+ title: 'Сессии чата',
18
+ description: 'Просмотр и управление историей чата',
19
+ noSessions: 'Нет сессий',
20
+ noSessionsDescription: 'Начните новый разговор, чтобы создать первую сессию.',
21
+ untitled: 'Без названия',
22
+ active: 'Активная',
23
+ archive: 'Архивировать',
24
+ delete: 'Удалить',
25
+ loadingMore: 'Загрузка...',
26
+ noMore: 'Больше нет сессий',
27
+ newChat: 'Новый чат',
28
+ },
29
+
30
+ actions: {
31
+ sessions: 'Сессии',
32
+ collapse: 'Свернуть',
33
+ expand: 'Развернуть',
34
+ close: 'Закрыть',
35
+ },
36
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Knowbase Extension I18n Types
3
+ */
4
+
5
+ /**
6
+ * Helper type to get dot-notation paths from nested object
7
+ */
8
+ type PathKeys<T, Prefix extends string = ''> = T extends object
9
+ ? {
10
+ [K in keyof T]: K extends string
11
+ ? T[K] extends object
12
+ ? PathKeys<T[K], `${Prefix}${K}.`>
13
+ : `${Prefix}${K}`
14
+ : never;
15
+ }[keyof T]
16
+ : never;
17
+
18
+ /**
19
+ * Keys for knowbase translations
20
+ */
21
+ export type KnowbaseLocalKeys = PathKeys<KnowbaseTranslations>;
22
+
23
+ export interface KnowbaseTranslations {
24
+ /** Chat widget */
25
+ chat: {
26
+ title: string;
27
+ titleShort: string;
28
+ placeholder: string;
29
+ enterToSend: string;
30
+ thinking: string;
31
+ source: string;
32
+ startConversation: string;
33
+ startConversationDescription: string;
34
+ openChat: string;
35
+ };
36
+
37
+ /** Sessions */
38
+ sessions: {
39
+ title: string;
40
+ description: string;
41
+ noSessions: string;
42
+ noSessionsDescription: string;
43
+ untitled: string;
44
+ active: string;
45
+ archive: string;
46
+ delete: string;
47
+ loadingMore: string;
48
+ noMore: string;
49
+ newChat: string;
50
+ };
51
+
52
+ /** Actions */
53
+ actions: {
54
+ sessions: string;
55
+ collapse: string;
56
+ expand: string;
57
+ close: string;
58
+ };
59
+ }
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Self-contained translation hook for ext-knowbase
5
+ *
6
+ * Uses built-in translations, no app configuration needed.
7
+ */
8
+
9
+ import { useLocale } from 'next-intl';
10
+ import { useMemo, useCallback } from 'react';
11
+
12
+ import type { KnowbaseTranslations, KnowbaseLocalKeys } from './types';
13
+ import { en } from './locales/en';
14
+ import { ru } from './locales/ru';
15
+ import { ko } from './locales/ko';
16
+
17
+ const translations: Record<string, KnowbaseTranslations> = { en, ru, ko };
18
+
19
+ /**
20
+ * Get nested value from object by dot-notation path
21
+ */
22
+ function getNestedValue(obj: Record<string, unknown>, path: string): string {
23
+ const keys = path.split('.');
24
+ let result: unknown = obj;
25
+
26
+ for (const key of keys) {
27
+ if (result && typeof result === 'object' && key in result) {
28
+ result = (result as Record<string, unknown>)[key];
29
+ } else {
30
+ return path; // Return key if not found
31
+ }
32
+ }
33
+
34
+ return typeof result === 'string' ? result : path;
35
+ }
36
+
37
+ /**
38
+ * Self-contained translation hook for knowbase extension
39
+ *
40
+ * Uses built-in translations based on current locale from next-intl.
41
+ * No need to add translations to app's i18n config.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * function ChatWidget() {
46
+ * const t = useKnowbaseT();
47
+ * return <h1>{t('chat.title')}</h1>;
48
+ * }
49
+ * ```
50
+ */
51
+ export function useKnowbaseT(): (key: KnowbaseLocalKeys) => string {
52
+ const locale = useLocale();
53
+
54
+ const t = useMemo(() => translations[locale] || translations.en, [locale]);
55
+
56
+ return useCallback(
57
+ (key: KnowbaseLocalKeys): string => getNestedValue(t as unknown as Record<string, unknown>, key),
58
+ [t]
59
+ );
60
+ }