@djangocfg/ext-support 1.0.20 → 1.0.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ext-support",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Support ticket system extension for DjangoCFG",
5
5
  "keywords": [
6
6
  "django",
@@ -46,6 +46,11 @@
46
46
  "types": "./dist/config.d.ts",
47
47
  "import": "./dist/config.js",
48
48
  "require": "./dist/config.cjs"
49
+ },
50
+ "./i18n": {
51
+ "types": "./dist/i18n.d.ts",
52
+ "import": "./dist/i18n.js",
53
+ "require": "./dist/i18n.cjs"
49
54
  }
50
55
  },
51
56
  "files": [
@@ -59,9 +64,10 @@
59
64
  "check": "tsc --noEmit"
60
65
  },
61
66
  "peerDependencies": {
62
- "@djangocfg/api": "^2.1.109",
63
- "@djangocfg/ext-base": "^1.0.15",
64
- "@djangocfg/ui-core": "^2.1.109",
67
+ "@djangocfg/api": "^2.1.111",
68
+ "@djangocfg/ext-base": "^1.0.17",
69
+ "@djangocfg/i18n": "^2.1.111",
70
+ "@djangocfg/ui-core": "^2.1.111",
65
71
  "consola": "^3.4.2",
66
72
  "lucide-react": "^0.545.0",
67
73
  "moment": "^2.30.1",
@@ -74,10 +80,11 @@
74
80
  "zod": "^4.3.4"
75
81
  },
76
82
  "devDependencies": {
77
- "@djangocfg/api": "^2.1.109",
78
- "@djangocfg/ext-base": "^1.0.15",
79
- "@djangocfg/ui-core": "^2.1.109",
80
- "@djangocfg/typescript-config": "^2.1.109",
83
+ "@djangocfg/api": "^2.1.111",
84
+ "@djangocfg/ext-base": "^1.0.17",
85
+ "@djangocfg/i18n": "^2.1.111",
86
+ "@djangocfg/ui-core": "^2.1.111",
87
+ "@djangocfg/typescript-config": "^2.1.111",
81
88
  "@types/node": "^24.7.2",
82
89
  "@types/react": "^19.0.0",
83
90
  "consola": "^3.4.2",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Support Extension I18n
3
+ */
4
+
5
+ import { createExtensionI18n } from '@djangocfg/ext-base/i18n';
6
+ import type { SupportTranslations } from './types';
7
+ import { en } from './locales/en';
8
+ import { ru } from './locales/ru';
9
+ import { ko } from './locales/ko';
10
+
11
+ /** Support extension namespace */
12
+ export const SUPPORT_NAMESPACE = 'support' as const;
13
+
14
+ export const supportI18n = createExtensionI18n<SupportTranslations>({
15
+ namespace: SUPPORT_NAMESPACE,
16
+ defaultLocale: 'en',
17
+ locales: { en, ru, ko },
18
+ });
19
+
20
+ export const supportTranslations = supportI18n.getAllTranslations();
21
+
22
+ // Types
23
+ export type { SupportTranslations, SupportKeys, SupportLocalKeys } from './types';
@@ -0,0 +1,76 @@
1
+ import type { SupportTranslations } from '../types';
2
+
3
+ export const en: SupportTranslations = {
4
+ layout: {
5
+ title: 'Support Center',
6
+ titleShort: 'Support',
7
+ subtitle: 'Get help from our support team',
8
+ newTicket: 'New Ticket',
9
+ },
10
+
11
+ status: {
12
+ open: 'Open',
13
+ waitingForUser: 'Waiting for you',
14
+ waitingForAdmin: 'Waiting for support',
15
+ resolved: 'Resolved',
16
+ closed: 'Closed',
17
+ },
18
+
19
+ ticketList: {
20
+ noTickets: 'No tickets yet',
21
+ noTicketsDescription: 'Create your first support ticket to get help',
22
+ loadingMore: 'Loading more...',
23
+ loadMore: 'Load more',
24
+ allLoaded: 'All {count} tickets loaded',
25
+ },
26
+
27
+ createTicket: {
28
+ title: 'New Support Ticket',
29
+ description: 'Describe your issue and we will help you',
30
+ subjectLabel: 'Subject',
31
+ subjectPlaceholder: 'Brief description of your issue',
32
+ messageLabel: 'Message',
33
+ messagePlaceholder: 'Describe your issue in detail...',
34
+ cancel: 'Cancel',
35
+ creating: 'Creating...',
36
+ create: 'Create Ticket',
37
+ },
38
+
39
+ validation: {
40
+ subjectRequired: 'Subject is required',
41
+ subjectTooLong: 'Subject is too long (max 200 characters)',
42
+ messageRequired: 'Message is required',
43
+ messageTooLong: 'Message is too long (max 5000 characters)',
44
+ },
45
+
46
+ messages: {
47
+ ticketCreated: 'Ticket created successfully',
48
+ ticketCreateFailed: 'Failed to create ticket',
49
+ messageSent: 'Message sent',
50
+ messageSendFailed: 'Failed to send message',
51
+ },
52
+
53
+ messageInput: {
54
+ placeholder: 'Type your message...',
55
+ ticketClosed: 'Ticket closed',
56
+ ticketClosedDescription: 'This ticket has been closed. Create a new ticket if you need further assistance.',
57
+ },
58
+
59
+ messageList: {
60
+ noTicketSelected: 'No ticket selected',
61
+ noTicketSelectedDescription: 'Select a ticket from the list to view messages',
62
+ noMessages: 'No messages yet',
63
+ noMessagesDescription: 'Start the conversation by sending a message',
64
+ loadingOlder: 'Loading older messages...',
65
+ loadOlder: 'Load older messages',
66
+ supportTeam: 'Support Team',
67
+ staff: 'Staff',
68
+ },
69
+
70
+ time: {
71
+ justNow: 'Just now',
72
+ minutesAgo: '{count} min ago',
73
+ hoursAgo: '{count}h ago',
74
+ daysAgo: '{count}d ago',
75
+ },
76
+ };
@@ -0,0 +1,76 @@
1
+ import type { SupportTranslations } from '../types';
2
+
3
+ export const ko: SupportTranslations = {
4
+ layout: {
5
+ title: '고객지원 센터',
6
+ titleShort: '고객지원',
7
+ subtitle: '고객지원팀의 도움을 받으세요',
8
+ newTicket: '새 문의',
9
+ },
10
+
11
+ status: {
12
+ open: '열림',
13
+ waitingForUser: '회신 대기',
14
+ waitingForAdmin: '지원팀 대기',
15
+ resolved: '해결됨',
16
+ closed: '종료',
17
+ },
18
+
19
+ ticketList: {
20
+ noTickets: '문의 내역 없음',
21
+ noTicketsDescription: '첫 번째 문의를 등록하여 도움을 받으세요',
22
+ loadingMore: '불러오는 중...',
23
+ loadMore: '더 보기',
24
+ allLoaded: '총 {count}개의 문의가 로드됨',
25
+ },
26
+
27
+ createTicket: {
28
+ title: '새 문의',
29
+ description: '문제를 설명해 주시면 도움을 드리겠습니다',
30
+ subjectLabel: '제목',
31
+ subjectPlaceholder: '문제에 대한 간략한 설명',
32
+ messageLabel: '내용',
33
+ messagePlaceholder: '문제를 자세히 설명해 주세요...',
34
+ cancel: '취소',
35
+ creating: '생성 중...',
36
+ create: '문의 등록',
37
+ },
38
+
39
+ validation: {
40
+ subjectRequired: '제목을 입력해 주세요',
41
+ subjectTooLong: '제목이 너무 깁니다 (최대 200자)',
42
+ messageRequired: '내용을 입력해 주세요',
43
+ messageTooLong: '내용이 너무 깁니다 (최대 5000자)',
44
+ },
45
+
46
+ messages: {
47
+ ticketCreated: '문의가 등록되었습니다',
48
+ ticketCreateFailed: '문의 등록에 실패했습니다',
49
+ messageSent: '메시지가 전송되었습니다',
50
+ messageSendFailed: '메시지 전송에 실패했습니다',
51
+ },
52
+
53
+ messageInput: {
54
+ placeholder: '메시지를 입력하세요...',
55
+ ticketClosed: '문의 종료됨',
56
+ ticketClosedDescription: '이 문의는 종료되었습니다. 추가 도움이 필요하시면 새 문의를 등록해 주세요.',
57
+ },
58
+
59
+ messageList: {
60
+ noTicketSelected: '문의가 선택되지 않음',
61
+ noTicketSelectedDescription: '메시지를 보려면 목록에서 문의를 선택하세요',
62
+ noMessages: '메시지 없음',
63
+ noMessagesDescription: '메시지를 보내 대화를 시작하세요',
64
+ loadingOlder: '이전 메시지 불러오는 중...',
65
+ loadOlder: '이전 메시지 보기',
66
+ supportTeam: '고객지원팀',
67
+ staff: '담당자',
68
+ },
69
+
70
+ time: {
71
+ justNow: '방금 전',
72
+ minutesAgo: '{count}분 전',
73
+ hoursAgo: '{count}시간 전',
74
+ daysAgo: '{count}일 전',
75
+ },
76
+ };
@@ -0,0 +1,76 @@
1
+ import type { SupportTranslations } from '../types';
2
+
3
+ export const ru: SupportTranslations = {
4
+ layout: {
5
+ title: 'Центр поддержки',
6
+ titleShort: 'Поддержка',
7
+ subtitle: 'Получите помощь от нашей команды поддержки',
8
+ newTicket: 'Новое обращение',
9
+ },
10
+
11
+ status: {
12
+ open: 'Открыт',
13
+ waitingForUser: 'Ожидает вас',
14
+ waitingForAdmin: 'Ожидает поддержку',
15
+ resolved: 'Решён',
16
+ closed: 'Закрыт',
17
+ },
18
+
19
+ ticketList: {
20
+ noTickets: 'Нет обращений',
21
+ noTicketsDescription: 'Создайте первое обращение, чтобы получить помощь',
22
+ loadingMore: 'Загрузка...',
23
+ loadMore: 'Загрузить ещё',
24
+ allLoaded: 'Все {count} обращений загружено',
25
+ },
26
+
27
+ createTicket: {
28
+ title: 'Новое обращение',
29
+ description: 'Опишите вашу проблему, и мы поможем вам',
30
+ subjectLabel: 'Тема',
31
+ subjectPlaceholder: 'Краткое описание проблемы',
32
+ messageLabel: 'Сообщение',
33
+ messagePlaceholder: 'Опишите вашу проблему подробно...',
34
+ cancel: 'Отмена',
35
+ creating: 'Создание...',
36
+ create: 'Создать обращение',
37
+ },
38
+
39
+ validation: {
40
+ subjectRequired: 'Тема обязательна',
41
+ subjectTooLong: 'Тема слишком длинная (макс. 200 символов)',
42
+ messageRequired: 'Сообщение обязательно',
43
+ messageTooLong: 'Сообщение слишком длинное (макс. 5000 символов)',
44
+ },
45
+
46
+ messages: {
47
+ ticketCreated: 'Обращение создано',
48
+ ticketCreateFailed: 'Не удалось создать обращение',
49
+ messageSent: 'Сообщение отправлено',
50
+ messageSendFailed: 'Не удалось отправить сообщение',
51
+ },
52
+
53
+ messageInput: {
54
+ placeholder: 'Введите сообщение...',
55
+ ticketClosed: 'Обращение закрыто',
56
+ ticketClosedDescription: 'Это обращение закрыто. Создайте новое, если вам нужна помощь.',
57
+ },
58
+
59
+ messageList: {
60
+ noTicketSelected: 'Обращение не выбрано',
61
+ noTicketSelectedDescription: 'Выберите обращение из списка для просмотра сообщений',
62
+ noMessages: 'Нет сообщений',
63
+ noMessagesDescription: 'Начните диалог, отправив сообщение',
64
+ loadingOlder: 'Загрузка старых сообщений...',
65
+ loadOlder: 'Загрузить старые',
66
+ supportTeam: 'Служба поддержки',
67
+ staff: 'Сотрудник',
68
+ },
69
+
70
+ time: {
71
+ justNow: 'Только что',
72
+ minutesAgo: '{count} мин назад',
73
+ hoursAgo: '{count}ч назад',
74
+ daysAgo: '{count}д назад',
75
+ },
76
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Support Extension I18n Types
3
+ */
4
+
5
+ import type { ExtensionKeys, PathKeys } from '@djangocfg/ext-base/i18n';
6
+
7
+ /**
8
+ * All valid keys for support translations (with namespace)
9
+ */
10
+ export type SupportKeys = ExtensionKeys<'support', SupportTranslations>;
11
+
12
+ /**
13
+ * Keys without namespace prefix (for createTypedExtensionT)
14
+ */
15
+ export type SupportLocalKeys = PathKeys<SupportTranslations>;
16
+
17
+ export interface SupportTranslations {
18
+ /** Layout */
19
+ layout: {
20
+ title: string;
21
+ titleShort: string;
22
+ subtitle: string;
23
+ newTicket: string;
24
+ };
25
+
26
+ /** Ticket statuses */
27
+ status: {
28
+ open: string;
29
+ waitingForUser: string;
30
+ waitingForAdmin: string;
31
+ resolved: string;
32
+ closed: string;
33
+ };
34
+
35
+ /** Ticket list */
36
+ ticketList: {
37
+ noTickets: string;
38
+ noTicketsDescription: string;
39
+ loadingMore: string;
40
+ loadMore: string;
41
+ allLoaded: string;
42
+ };
43
+
44
+ /** Create ticket dialog */
45
+ createTicket: {
46
+ title: string;
47
+ description: string;
48
+ subjectLabel: string;
49
+ subjectPlaceholder: string;
50
+ messageLabel: string;
51
+ messagePlaceholder: string;
52
+ cancel: string;
53
+ creating: string;
54
+ create: string;
55
+ };
56
+
57
+ /** Validation */
58
+ validation: {
59
+ subjectRequired: string;
60
+ subjectTooLong: string;
61
+ messageRequired: string;
62
+ messageTooLong: string;
63
+ };
64
+
65
+ /** Messages */
66
+ messages: {
67
+ ticketCreated: string;
68
+ ticketCreateFailed: string;
69
+ messageSent: string;
70
+ messageSendFailed: string;
71
+ };
72
+
73
+ /** Message input */
74
+ messageInput: {
75
+ placeholder: string;
76
+ ticketClosed: string;
77
+ ticketClosedDescription: string;
78
+ };
79
+
80
+ /** Message list */
81
+ messageList: {
82
+ noTicketSelected: string;
83
+ noTicketSelectedDescription: string;
84
+ noMessages: string;
85
+ noMessagesDescription: string;
86
+ loadingOlder: string;
87
+ loadOlder: string;
88
+ supportTeam: string;
89
+ staff: string;
90
+ };
91
+
92
+ /** Time labels */
93
+ time: {
94
+ justNow: string;
95
+ minutesAgo: string;
96
+ hoursAgo: string;
97
+ daysAgo: string;
98
+ };
99
+ }
@@ -7,8 +7,11 @@
7
7
  'use client';
8
8
 
9
9
  import { ArrowLeft, LifeBuoy, Plus } from 'lucide-react';
10
- import React from 'react';
10
+ import React, { useMemo } from 'react';
11
11
 
12
+ import { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
13
+ import { useT } from '@djangocfg/i18n';
14
+ import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../i18n';
12
15
  import { Button, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@djangocfg/ui-core';
13
16
 
14
17
  import { SupportProvider } from '../../contexts/SupportContext';
@@ -20,10 +23,19 @@ import { SupportLayoutProvider, useSupportLayoutContext } from './context';
20
23
  // ─────────────────────────────────────────────────────────────────────────
21
24
 
22
25
  const SupportLayoutContent: React.FC = () => {
26
+ const baseT = useT();
27
+ const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
23
28
  const { selectedTicket, selectTicket, openCreateDialog, getUnreadCount } =
24
29
  useSupportLayoutContext();
25
30
  const [isMobile, setIsMobile] = React.useState(false);
26
-
31
+
32
+ const labels = useMemo(() => ({
33
+ title: st('layout.title'),
34
+ titleShort: st('layout.titleShort'),
35
+ subtitle: st('layout.subtitle'),
36
+ newTicket: st('layout.newTicket'),
37
+ }), [st]);
38
+
27
39
  React.useEffect(() => {
28
40
  const checkMobile = () => setIsMobile(window.innerWidth <= 768);
29
41
  checkMobile();
@@ -53,7 +65,7 @@ const SupportLayoutContent: React.FC = () => {
53
65
  <LifeBuoy className="h-6 w-6 text-primary" />
54
66
  )}
55
67
  <h1 className="text-xl font-semibold">
56
- {selectedTicket ? selectedTicket.subject : 'Support'}
68
+ {selectedTicket ? selectedTicket.subject : labels.titleShort}
57
69
  </h1>
58
70
  {unreadCount > 0 && !selectedTicket && (
59
71
  <div className="h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
@@ -64,7 +76,7 @@ const SupportLayoutContent: React.FC = () => {
64
76
  {!selectedTicket && (
65
77
  <Button onClick={openCreateDialog} size="sm">
66
78
  <Plus className="h-4 w-4 mr-2" />
67
- New Ticket
79
+ {labels.newTicket}
68
80
  </Button>
69
81
  )}
70
82
  </div>
@@ -101,8 +113,8 @@ const SupportLayoutContent: React.FC = () => {
101
113
  <div className="flex items-center gap-3">
102
114
  <LifeBuoy className="h-7 w-7 text-primary" />
103
115
  <div>
104
- <h1 className="text-2xl font-bold">Support Center</h1>
105
- <p className="text-sm text-muted-foreground">Get help from our support team</p>
116
+ <h1 className="text-2xl font-bold">{labels.title}</h1>
117
+ <p className="text-sm text-muted-foreground">{labels.subtitle}</p>
106
118
  </div>
107
119
  {unreadCount > 0 && (
108
120
  <div className="h-6 w-6 bg-red-500 text-white text-sm rounded-full flex items-center justify-center">
@@ -113,7 +125,7 @@ const SupportLayoutContent: React.FC = () => {
113
125
 
114
126
  <Button onClick={openCreateDialog}>
115
127
  <Plus className="h-4 w-4 mr-2" />
116
- New Ticket
128
+ {labels.newTicket}
117
129
  </Button>
118
130
  </div>
119
131
 
@@ -7,10 +7,13 @@
7
7
  'use client';
8
8
 
9
9
  import { Loader2, Plus } from 'lucide-react';
10
- import React from 'react';
10
+ import React, { useMemo } from 'react';
11
11
  import { useForm } from 'react-hook-form';
12
12
  import { z } from 'zod';
13
13
 
14
+ import { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
15
+ import { useT } from '@djangocfg/i18n';
16
+ import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
14
17
  import {
15
18
  Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Form, FormControl,
16
19
  FormField, FormItem, FormLabel, FormMessage, Input, Textarea, useToast
@@ -22,16 +25,38 @@ import { useSupportLayoutContext } from '../context';
22
25
 
23
26
  import type { TicketFormData } from '../types';
24
27
 
25
- const createTicketSchema = z.object({
26
- subject: z.string().min(1, 'Subject is required').max(200, 'Subject too long'),
27
- message: z.string().min(1, 'Message is required').max(5000, 'Message too long'),
28
- });
29
-
30
28
  export const CreateTicketDialog: React.FC = () => {
29
+ const baseT = useT();
30
+ const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
31
31
  const { uiState, createTicket, closeCreateDialog } = useSupportLayoutContext();
32
32
  const { toast } = useToast();
33
33
  const [isSubmitting, setIsSubmitting] = React.useState(false);
34
34
 
35
+ const labels = useMemo(() => ({
36
+ title: st('createTicket.title'),
37
+ description: st('createTicket.description'),
38
+ subjectLabel: st('createTicket.subjectLabel'),
39
+ subjectPlaceholder: st('createTicket.subjectPlaceholder'),
40
+ messageLabel: st('createTicket.messageLabel'),
41
+ messagePlaceholder: st('createTicket.messagePlaceholder'),
42
+ cancel: st('createTicket.cancel'),
43
+ creating: st('createTicket.creating'),
44
+ create: st('createTicket.create'),
45
+ ticketCreated: st('messages.ticketCreated'),
46
+ ticketCreateFailed: st('messages.ticketCreateFailed'),
47
+ validation: {
48
+ subjectRequired: st('validation.subjectRequired'),
49
+ subjectTooLong: st('validation.subjectTooLong'),
50
+ messageRequired: st('validation.messageRequired'),
51
+ messageTooLong: st('validation.messageTooLong'),
52
+ },
53
+ }), [st]);
54
+
55
+ const createTicketSchema = useMemo(() => z.object({
56
+ subject: z.string().min(1, labels.validation.subjectRequired).max(200, labels.validation.subjectTooLong),
57
+ message: z.string().min(1, labels.validation.messageRequired).max(5000, labels.validation.messageTooLong),
58
+ }), [labels.validation]);
59
+
35
60
  const form = useForm<TicketFormData>({
36
61
  resolver: zodResolver(createTicketSchema),
37
62
  defaultValues: {
@@ -45,10 +70,10 @@ export const CreateTicketDialog: React.FC = () => {
45
70
  try {
46
71
  await createTicket(data);
47
72
  form.reset();
48
- toast.success('Support ticket created successfully');
73
+ toast.success(labels.ticketCreated);
49
74
  } catch (error) {
50
75
  supportLogger.error('Failed to create ticket:', error);
51
- toast.error('Failed to create ticket. Please try again.');
76
+ toast.error(labels.ticketCreateFailed);
52
77
  } finally {
53
78
  setIsSubmitting(false);
54
79
  }
@@ -65,10 +90,10 @@ export const CreateTicketDialog: React.FC = () => {
65
90
  <DialogHeader>
66
91
  <DialogTitle className="flex items-center gap-2">
67
92
  <Plus className="h-5 w-5" />
68
- Create Support Ticket
93
+ {labels.title}
69
94
  </DialogTitle>
70
95
  <DialogDescription>
71
- Describe your issue and we'll help you resolve it as quickly as possible.
96
+ {labels.description}
72
97
  </DialogDescription>
73
98
  </DialogHeader>
74
99
 
@@ -79,9 +104,9 @@ export const CreateTicketDialog: React.FC = () => {
79
104
  name="subject"
80
105
  render={({ field }) => (
81
106
  <FormItem>
82
- <FormLabel>Subject</FormLabel>
107
+ <FormLabel>{labels.subjectLabel}</FormLabel>
83
108
  <FormControl>
84
- <Input placeholder="Brief description of your issue..." {...field} />
109
+ <Input placeholder={labels.subjectPlaceholder} {...field} />
85
110
  </FormControl>
86
111
  <FormMessage />
87
112
  </FormItem>
@@ -93,10 +118,10 @@ export const CreateTicketDialog: React.FC = () => {
93
118
  name="message"
94
119
  render={({ field }) => (
95
120
  <FormItem>
96
- <FormLabel>Message</FormLabel>
121
+ <FormLabel>{labels.messageLabel}</FormLabel>
97
122
  <FormControl>
98
123
  <Textarea
99
- placeholder="Describe your issue in detail. Include any error messages, steps to reproduce, or relevant information..."
124
+ placeholder={labels.messagePlaceholder}
100
125
  className="min-h-[120px]"
101
126
  {...field}
102
127
  />
@@ -113,18 +138,18 @@ export const CreateTicketDialog: React.FC = () => {
113
138
  onClick={handleClose}
114
139
  disabled={isSubmitting}
115
140
  >
116
- Cancel
141
+ {labels.cancel}
117
142
  </Button>
118
143
  <Button type="submit" disabled={isSubmitting}>
119
144
  {isSubmitting ? (
120
145
  <>
121
146
  <Loader2 className="h-4 w-4 mr-2 animate-spin" />
122
- Creating...
147
+ {labels.creating}
123
148
  </>
124
149
  ) : (
125
150
  <>
126
151
  <Plus className="h-4 w-4 mr-2" />
127
- Create Ticket
152
+ {labels.create}
128
153
  </>
129
154
  )}
130
155
  </Button>
@@ -6,8 +6,11 @@
6
6
  'use client';
7
7
 
8
8
  import { Send } from 'lucide-react';
9
- import React, { useState } from 'react';
9
+ import React, { useState, useMemo } from 'react';
10
10
 
11
+ import { createTypedExtensionT } from '@djangocfg/ext-base/i18n';
12
+ import { useT } from '@djangocfg/i18n';
13
+ import { SUPPORT_NAMESPACE, type SupportTranslations } from '../../../i18n';
11
14
  import { Button, Textarea } from '@djangocfg/ui-core';
12
15
  import { useToast } from '@djangocfg/ui-core/hooks';
13
16
 
@@ -15,24 +18,34 @@ import { supportLogger } from '../../../utils/logger';
15
18
  import { useSupportLayoutContext } from '../context';
16
19
 
17
20
  export const MessageInput: React.FC = () => {
21
+ const baseT = useT();
22
+ const st = createTypedExtensionT<typeof SUPPORT_NAMESPACE, SupportTranslations>(baseT, SUPPORT_NAMESPACE);
18
23
  const { selectedTicket, sendMessage } = useSupportLayoutContext();
19
24
  const { toast } = useToast();
20
25
  const [message, setMessage] = useState('');
21
26
  const [isSending, setIsSending] = useState(false);
22
27
 
28
+ const labels = useMemo(() => ({
29
+ placeholder: st('messageInput.placeholder'),
30
+ ticketClosed: st('messageInput.ticketClosed'),
31
+ ticketClosedDescription: st('messageInput.ticketClosedDescription'),
32
+ messageSent: st('messages.messageSent'),
33
+ messageSendFailed: st('messages.messageSendFailed'),
34
+ }), [st]);
35
+
23
36
  const handleSubmit = async (e: React.FormEvent) => {
24
37
  e.preventDefault();
25
-
38
+
26
39
  if (!message.trim() || !selectedTicket) return;
27
40
 
28
41
  setIsSending(true);
29
42
  try {
30
43
  await sendMessage(message.trim());
31
44
  setMessage('');
32
- toast.success('Message sent successfully');
45
+ toast.success(labels.messageSent);
33
46
  } catch (error) {
34
47
  supportLogger.error('Failed to send message:', error);
35
- toast.error('Failed to send message');
48
+ toast.error(labels.messageSendFailed);
36
49
  } finally {
37
50
  setIsSending(false);
38
51
  }
@@ -58,12 +71,8 @@ export const MessageInput: React.FC = () => {
58
71
  value={message}
59
72
  onChange={(e) => setMessage(e.target.value)}
60
73
  onKeyDown={handleKeyDown}
61
- placeholder={
62
- canSendMessage
63
- ? 'Type your message... (Shift+Enter for new line)'
64
- : 'This ticket is closed'
65
- }
66
- className="min-h-[60px] max-h-[200px] transition-all duration-200
74
+ placeholder={canSendMessage ? labels.placeholder : labels.ticketClosed}
75
+ className="min-h-[60px] max-h-[200px] transition-all duration-200
67
76
  focus:ring-2 focus:ring-primary/20"
68
77
  disabled={!canSendMessage || isSending}
69
78
  />
@@ -79,7 +88,7 @@ export const MessageInput: React.FC = () => {
79
88
  </div>
80
89
  {!canSendMessage && (
81
90
  <p className="text-xs text-muted-foreground mt-2 animate-in fade-in slide-in-from-top-1 duration-200">
82
- This ticket is closed. You cannot send new messages.
91
+ {labels.ticketClosedDescription}
83
92
  </p>
84
93
  )}
85
94
  </form>