strapi-plugin-mcp-chat 0.1.0 → 0.3.1

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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * i18n leve para as PÁGINAS DO PLUGIN no menu esquerdo (Home + Provisionar).
3
+ *
4
+ * Não usa o sistema de traduções do admin de propósito: estas páginas são
5
+ * autossuficientes e o idioma é escolhido por um seletor próprio, persistido em
6
+ * `localStorage` sob a MESMA chave do chat flutuante (`mcp-chat-lang`), para que
7
+ * o plugin inteiro siga um único idioma. Default: inglês.
8
+ */
9
+ import { useEffect, useState } from 'react';
10
+
11
+ export type Lang = 'pt' | 'en';
12
+
13
+ const LS_KEY = 'mcp-chat-lang';
14
+
15
+ export const getLang = (): Lang => {
16
+ try {
17
+ return (localStorage.getItem(LS_KEY) as Lang) === 'pt' ? 'pt' : 'en';
18
+ } catch {
19
+ return 'en';
20
+ }
21
+ };
22
+
23
+ /** Hook: idioma atual + setter persistido + sincronização entre abas/componentes. */
24
+ export const useLang = (): [Lang, (l: Lang) => void] => {
25
+ const [lang, setLangState] = useState<Lang>(getLang);
26
+
27
+ useEffect(() => {
28
+ const onStorage = (e: StorageEvent) => {
29
+ if (e.key === LS_KEY) setLangState(getLang());
30
+ };
31
+ // evento custom para sincronizar componentes na MESMA aba
32
+ const onLocal = () => setLangState(getLang());
33
+ window.addEventListener('storage', onStorage);
34
+ window.addEventListener('mcp-chat-lang-change', onLocal);
35
+ return () => {
36
+ window.removeEventListener('storage', onStorage);
37
+ window.removeEventListener('mcp-chat-lang-change', onLocal);
38
+ };
39
+ }, []);
40
+
41
+ const setLang = (l: Lang) => {
42
+ try { localStorage.setItem(LS_KEY, l); } catch { /* noop */ }
43
+ setLangState(l);
44
+ try { window.dispatchEvent(new Event('mcp-chat-lang-change')); } catch { /* noop */ }
45
+ };
46
+
47
+ return [lang, setLang];
48
+ };
49
+
50
+ type Dict = Record<string, string>;
51
+
52
+ export const STRINGS: Record<Lang, Dict> = {
53
+ en: {
54
+ // Home
55
+ 'home.title': 'MCP Chat',
56
+ 'home.subtitle': 'AI via MCP',
57
+ 'home.seeingScreen': '• seeing your screen',
58
+ 'home.voiceOn': '• voice ON',
59
+ 'home.voiceBtnOn': 'Voice: ON',
60
+ 'home.voiceBtnOff': 'Voice: OFF',
61
+ 'home.pubOn': '🚀 Publish: ON',
62
+ 'home.pubOff': '📝 Draft mode',
63
+ 'home.pubTitle': 'Auto-publish OFF = the AI only saves a draft (you review and publish). ON = publishes straight to the site.',
64
+ 'home.shareStop': 'Stop screen',
65
+ 'home.shareStart': 'Share screen',
66
+ 'home.provision': 'Provision frontend',
67
+ 'home.previewOn': 'Live Preview: ON',
68
+ 'home.previewOff': 'Live Preview: OFF',
69
+ 'home.empty': 'Type, speak (🎤) or share your screen. E.g.: “Which content-types exist?”.',
70
+ 'home.you': 'You',
71
+ 'home.ai': 'AI',
72
+ 'home.processing': 'Processing…',
73
+ 'home.rec': '🎤 Speak',
74
+ 'home.recStop': '⏹ Stop',
75
+ 'home.placeholder': 'Type… (Cmd/Ctrl+Enter sends)',
76
+ 'home.send': 'Send',
77
+ 'home.reload': 'Reload',
78
+ 'home.previewUrlLabel': 'Preview URL',
79
+ 'home.errShare': 'Could not start screen sharing.',
80
+ 'home.errMic': 'Could not access the microphone.',
81
+ 'home.errStt': 'Transcription error.',
82
+ 'home.errAudioEmpty': 'I could not understand the audio.',
83
+ 'home.errChat': 'Error talking to the AI.',
84
+ 'home.noReply': '(no reply)',
85
+ 'home.tour': '❓ Tour',
86
+ 'common.lang': '🌐 English',
87
+
88
+ // Provision
89
+ 'prov.title': 'Provision frontend',
90
+ 'prov.subtitle': 'Upload your frontend .zip (Figma/Lovable, Next or TanStack) — the AI infers the content model, you review it, and the plugin creates everything in Strapi.',
91
+ 'prov.back': '← Back to chat',
92
+ 'prov.supported': 'Supported stacks',
93
+ 'prov.step1': '1. Choose the frontend .zip',
94
+ 'prov.step1desc': 'No strapi.manifest.json needed: if it is missing, the AI creates one by analyzing the code (e.g. src/data/*.ts).',
95
+ 'prov.selectFile': 'Select file…',
96
+ 'prov.noFile': 'No file selected',
97
+ 'prov.analyze': 'Analyze project',
98
+ 'prov.analyzing': 'Analyzing…',
99
+ 'prov.analyzingDesc': 'Reading the code and inferring the content model (content-types + seed)…',
100
+ 'prov.step2': '2. Review the content model',
101
+ 'prov.inferred': '🤖 Inferred by the AI',
102
+ 'prov.fromManifest': '✓ Project manifest',
103
+ 'prov.framework': 'framework',
104
+ 'prov.analyzed': 'Analyzed',
105
+ 'prov.editJson': 'Edit the JSON to adjust names, types or seeded content before creating.',
106
+ 'prov.provision': 'Provision',
107
+ 'prov.restart': 'Restart',
108
+ 'prov.provisioning': 'Provisioning…',
109
+ 'prov.provisioningTitle': 'Setting everything up — this takes a few seconds',
110
+ 'prov.provisioningDesc': 'Strapi is restarting to recognize the content-types, then it seeds content, opens public read access and wires the preview. Do not close this page.',
111
+ 'prov.invalidJson': 'The manifest is not valid JSON. Fix the syntax.',
112
+ 'prov.analyzeFail': 'Failed to analyze the project.',
113
+ 'prov.provisionFail': 'Provisioning failed.',
114
+ 'prov.analyzeWarn': 'Analysis warning:',
115
+ 'prov.doneTitle': '✅ All set! You can see the preview now.',
116
+ 'prov.typesCreated': 'Content-types created:',
117
+ 'prov.seeded': 'Seeded content:',
118
+ 'prov.frontendAt': 'Frontend at:',
119
+ 'prov.runFrontend': 'To see the preview, run the frontend (once):',
120
+ 'prov.relinkDesc': 'Relink the frontend to Strapi (snapshot): swaps the hardcoded data for Strapi data, keeping the images. Components do not change.',
121
+ 'prov.relink': 'Relink data to Strapi',
122
+ 'prov.relinking': 'Relinking…',
123
+ 'prov.open': 'Open',
124
+ 'prov.provisionAnother': 'Provision another',
125
+ 'prov.relinkOk': '✅ Relinked! Files updated: {files}. Reload the preview to see Strapi data. (Original saved as .bak.)',
126
+ 'prov.relinkFail': '⚠️ Could not relink: {err}.',
127
+ 'prov.relinkErr': 'Failed to relink.',
128
+ 'prov.noData': 'no data file',
129
+ },
130
+ pt: {
131
+ // Home
132
+ 'home.title': 'MCP Chat',
133
+ 'home.subtitle': 'IA via MCP',
134
+ 'home.seeingScreen': '• vendo sua tela',
135
+ 'home.voiceOn': '• voz ON',
136
+ 'home.voiceBtnOn': 'Voz: ON',
137
+ 'home.voiceBtnOff': 'Voz: OFF',
138
+ 'home.pubOn': '🚀 Publicar: ON',
139
+ 'home.pubOff': '📝 Rascunho',
140
+ 'home.pubTitle': 'Auto-publicar OFF = a IA só salva rascunho (você revisa e publica). ON = publica direto no site.',
141
+ 'home.shareStop': 'Parar tela',
142
+ 'home.shareStart': 'Compartilhar tela',
143
+ 'home.provision': 'Provisionar frontend',
144
+ 'home.previewOn': 'Live Preview: ON',
145
+ 'home.previewOff': 'Live Preview: OFF',
146
+ 'home.empty': 'Escreva, fale (🎤) ou compartilhe a tela. Ex.: “Quais content-types existem?”.',
147
+ 'home.you': 'Você',
148
+ 'home.ai': 'IA',
149
+ 'home.processing': 'Processando…',
150
+ 'home.rec': '🎤 Falar',
151
+ 'home.recStop': '⏹ Parar',
152
+ 'home.placeholder': 'Escreva… (Cmd/Ctrl+Enter envia)',
153
+ 'home.send': 'Enviar',
154
+ 'home.reload': 'Recarregar',
155
+ 'home.previewUrlLabel': 'URL do preview',
156
+ 'home.errShare': 'Não foi possível iniciar o compartilhamento de tela.',
157
+ 'home.errMic': 'Não foi possível acessar o microfone.',
158
+ 'home.errStt': 'Erro na transcrição.',
159
+ 'home.errAudioEmpty': 'Não consegui entender o áudio.',
160
+ 'home.errChat': 'Erro ao falar com a IA.',
161
+ 'home.noReply': '(sem resposta)',
162
+ 'home.tour': '❓ Tour',
163
+ 'common.lang': '🌐 PT-BR',
164
+
165
+ // Provision
166
+ 'prov.title': 'Provisionar frontend',
167
+ 'prov.subtitle': 'Suba o .zip do seu frontend (Figma/Lovable, Next ou TanStack) — a IA infere o modelo de conteúdo, você revisa, e o plugin cria tudo no Strapi.',
168
+ 'prov.back': '← Voltar ao chat',
169
+ 'prov.supported': 'Stacks suportados',
170
+ 'prov.step1': '1. Escolha o .zip do frontend',
171
+ 'prov.step1desc': 'Não precisa de strapi.manifest.json: se ele não existir, a IA cria um analisando os dados do código (ex.: src/data/*.ts).',
172
+ 'prov.selectFile': 'Selecionar arquivo…',
173
+ 'prov.noFile': 'Nenhum arquivo selecionado',
174
+ 'prov.analyze': 'Analisar projeto',
175
+ 'prov.analyzing': 'Analisando…',
176
+ 'prov.analyzingDesc': 'Lendo o código e inferindo o modelo de conteúdo (content-types + seed)…',
177
+ 'prov.step2': '2. Revise o modelo de conteúdo',
178
+ 'prov.inferred': '🤖 Inferido pela IA',
179
+ 'prov.fromManifest': '✓ Manifest do projeto',
180
+ 'prov.framework': 'framework',
181
+ 'prov.analyzed': 'Analisou',
182
+ 'prov.editJson': 'Edite o JSON se quiser ajustar nomes, tipos ou o conteúdo semeado antes de criar.',
183
+ 'prov.provision': 'Provisionar',
184
+ 'prov.restart': 'Recomeçar',
185
+ 'prov.provisioning': 'Provisionando…',
186
+ 'prov.provisioningTitle': 'Configurando tudo — isso leva alguns segundos',
187
+ 'prov.provisioningDesc': 'A Strapi está reiniciando para reconhecer as content-types, depois semeia o conteúdo, libera leitura pública e liga o preview. Não feche esta página.',
188
+ 'prov.invalidJson': 'O manifest não é um JSON válido. Corrija a sintaxe.',
189
+ 'prov.analyzeFail': 'Falha ao analisar o projeto.',
190
+ 'prov.provisionFail': 'Falha na provisão.',
191
+ 'prov.analyzeWarn': 'Aviso da análise:',
192
+ 'prov.doneTitle': '✅ Tudo pronto! Você já pode ver o preview.',
193
+ 'prov.typesCreated': 'Content-types criadas:',
194
+ 'prov.seeded': 'Conteúdo semeado:',
195
+ 'prov.frontendAt': 'Frontend em:',
196
+ 'prov.runFrontend': 'Para ver o preview, rode o frontend (uma vez):',
197
+ 'prov.relinkDesc': 'Religar o frontend ao Strapi (snapshot): troca os dados hardcoded pelos do Strapi, mantendo as imagens. Os componentes não mudam.',
198
+ 'prov.relink': 'Religar dados ao Strapi',
199
+ 'prov.relinking': 'Religando…',
200
+ 'prov.open': 'Abrir',
201
+ 'prov.provisionAnother': 'Provisionar outro',
202
+ 'prov.relinkOk': '✅ Religado! Arquivos atualizados: {files}. Recarregue o preview para ver os dados do Strapi. (Original salvo como .bak.)',
203
+ 'prov.relinkFail': '⚠️ Não consegui religar: {err}.',
204
+ 'prov.relinkErr': 'Falha ao religar.',
205
+ 'prov.noData': 'sem arquivo de dados',
206
+ },
207
+ };
208
+
209
+ /** Tradutor com interpolação simples de {chaves}. */
210
+ export const makeT = (lang: Lang) => (key: string, vars?: Record<string, string>) => {
211
+ let s = STRINGS[lang][key] ?? STRINGS.en[key] ?? key;
212
+ if (vars) for (const k of Object.keys(vars)) s = s.replace(`{${k}}`, vars[k]);
213
+ return s;
214
+ };
@@ -1,7 +1,10 @@
1
- import { useRef, useState } from 'react';
1
+ import { useRef, useState, useEffect } from 'react';
2
2
  import { Box, Flex, Typography, Button, Textarea, TextInput } from '@strapi/design-system';
3
3
  import { useFetchClient } from '@strapi/strapi/admin';
4
4
  import { Link } from 'react-router-dom';
5
+ import { useLang, makeT } from '../i18n';
6
+ import { LangSwitcher } from '../components/LangSwitcher';
7
+ import { Onboarding, tourWasSeen } from '../components/Onboarding';
5
8
 
6
9
  type Msg = { role: 'user' | 'assistant'; content: string; image?: string | null };
7
10
 
@@ -16,6 +19,8 @@ const DEFAULT_PREVIEW_URL = (() => {
16
19
 
17
20
  const HomePage = () => {
18
21
  const { post } = useFetchClient();
22
+ const [lang] = useLang();
23
+ const t = makeT(lang);
19
24
 
20
25
  const [messages, setMessages] = useState<Msg[]>([]);
21
26
  const [input, setInput] = useState('');
@@ -23,6 +28,10 @@ const HomePage = () => {
23
28
  const [sharing, setSharing] = useState(false);
24
29
  const [error, setError] = useState<string | null>(null);
25
30
 
31
+ // Onboarding (mini-curso): abre na 1ª vez; reabrível pelo botão Tour.
32
+ const [tourOpen, setTourOpen] = useState(false);
33
+ useEffect(() => { if (!tourWasSeen()) setTourOpen(true); }, []);
34
+
26
35
  // Live preview
27
36
  const [previewOn, setPreviewOn] = useState(false);
28
37
  const [previewUrl, setPreviewUrl] = useState(DEFAULT_PREVIEW_URL);
@@ -32,6 +41,14 @@ const HomePage = () => {
32
41
  const [voiceOn, setVoiceOn] = useState(false);
33
42
  const [recording, setRecording] = useState(false);
34
43
 
44
+ // Draft-first: a IA só salva rascunho por padrão; publica só com isto ON.
45
+ const [autoPublish, setAutoPublish] = useState<boolean>(() => {
46
+ try { return localStorage.getItem('mcp-chat-autopublish') === '1'; } catch { return false; }
47
+ });
48
+ useEffect(() => {
49
+ try { localStorage.setItem('mcp-chat-autopublish', autoPublish ? '1' : '0'); } catch { /* noop */ }
50
+ }, [autoPublish]);
51
+
35
52
  const videoRef = useRef<HTMLVideoElement | null>(null);
36
53
  const streamRef = useRef<MediaStream | null>(null);
37
54
  const recorderRef = useRef<MediaRecorder | null>(null);
@@ -50,7 +67,7 @@ const HomePage = () => {
50
67
  stream.getVideoTracks()[0].addEventListener('ended', stopShare);
51
68
  setSharing(true);
52
69
  } catch {
53
- setError('Não foi possível iniciar o compartilhamento de tela.');
70
+ setError(t('home.errShare'));
54
71
  }
55
72
  };
56
73
  const stopShare = () => {
@@ -98,15 +115,18 @@ const HomePage = () => {
98
115
  const payload = {
99
116
  messages: next.map((m) => ({ role: m.role, content: m.content })),
100
117
  image,
118
+ lang,
119
+ previewUrl: previewOn ? previewUrl : null,
120
+ autoPublish,
101
121
  };
102
122
  const { data } = await post('/mcp-chat/message', payload);
103
- const reply = data?.reply || '(sem resposta)';
123
+ const reply = data?.reply || t('home.noReply');
104
124
  setMessages((cur) => [...cur, { role: 'assistant', content: reply }]);
105
125
  if (previewOn) setIframeKey((k) => k + 1);
106
126
  if (voiceOn) playTTS(reply);
107
127
  } catch (e: any) {
108
128
  const detail =
109
- e?.response?.data?.error?.message || e?.message || 'Erro ao falar com a IA.';
129
+ e?.response?.data?.error?.message || e?.message || t('home.errChat');
110
130
  setError(detail);
111
131
  } finally {
112
132
  setLoading(false);
@@ -144,7 +164,7 @@ const HomePage = () => {
144
164
  recorder.start();
145
165
  setRecording(true);
146
166
  } catch {
147
- setError('Não foi possível acessar o microfone.');
167
+ setError(t('home.errMic'));
148
168
  }
149
169
  };
150
170
  const stopRecording = () => {
@@ -157,14 +177,15 @@ const HomePage = () => {
157
177
  const ext = blob.type.includes('mp4') ? 'mp4' : 'webm';
158
178
  const form = new FormData();
159
179
  form.append('audio', blob, `audio.${ext}`);
160
- const { data } = await post('/mcp-chat/stt', form);
180
+ form.append('language', lang);
181
+ const { data } = await post(`/mcp-chat/stt?language=${lang}`, form);
161
182
  const text = (data?.text || '').trim();
162
183
  setLoading(false);
163
184
  if (text) sendMessage(text);
164
- else setError('Não consegui entender o áudio.');
185
+ else setError(t('home.errAudioEmpty'));
165
186
  } catch (e: any) {
166
187
  setLoading(false);
167
- setError(e?.response?.data?.error?.message || 'Erro na transcrição.');
188
+ setError(e?.response?.data?.error?.message || t('home.errStt'));
168
189
  }
169
190
  };
170
191
 
@@ -173,9 +194,9 @@ const HomePage = () => {
173
194
  <Flex direction="column" alignItems="stretch" gap={4} height="100%">
174
195
  <Flex justifyContent="space-between" alignItems="center">
175
196
  <Box>
176
- <Typography variant="beta" tag="h1">MCP Chat</Typography>
197
+ <Typography variant="beta" tag="h1">{t('home.title')}</Typography>
177
198
  <Typography variant="pi" textColor="neutral600">
178
- IA via MCP {sharing ? '• vendo sua tela' : ''} {voiceOn ? '• voz ON' : ''}
199
+ {t('home.subtitle')} {sharing ? t('home.seeingScreen') : ''} {voiceOn ? t('home.voiceOn') : ''}
179
200
  </Typography>
180
201
  </Box>
181
202
  <Flex gap={1}>
@@ -184,14 +205,22 @@ const HomePage = () => {
184
205
  variant={voiceOn ? 'success-light' : 'tertiary'}
185
206
  onClick={() => setVoiceOn((v) => !v)}
186
207
  >
187
- {voiceOn ? 'Voz: ON' : 'Voz: OFF'}
208
+ {voiceOn ? t('home.voiceBtnOn') : t('home.voiceBtnOff')}
209
+ </Button>
210
+ <Button
211
+ size="S"
212
+ variant={autoPublish ? 'danger-light' : 'tertiary'}
213
+ onClick={() => setAutoPublish((v) => !v)}
214
+ title={t('home.pubTitle')}
215
+ >
216
+ {autoPublish ? t('home.pubOn') : t('home.pubOff')}
188
217
  </Button>
189
218
  <Button
190
219
  size="S"
191
220
  variant={sharing ? 'danger-light' : 'secondary'}
192
221
  onClick={sharing ? stopShare : startShare}
193
222
  >
194
- {sharing ? 'Parar tela' : 'Compartilhar tela'}
223
+ {sharing ? t('home.shareStop') : t('home.shareStart')}
195
224
  </Button>
196
225
  </Flex>
197
226
  </Flex>
@@ -205,9 +234,7 @@ const HomePage = () => {
205
234
  style={{ overflowY: 'auto', minHeight: 240 }}
206
235
  >
207
236
  {messages.length === 0 && (
208
- <Typography textColor="neutral500">
209
- Escreva, fale (🎤) ou compartilhe a tela. Ex.: “Quais content-types existem?”.
210
- </Typography>
237
+ <Typography textColor="neutral500">{t('home.empty')}</Typography>
211
238
  )}
212
239
  <Flex direction="column" alignItems="stretch" gap={3}>
213
240
  {messages.map((m, i) => (
@@ -218,7 +245,7 @@ const HomePage = () => {
218
245
  background={m.role === 'user' ? 'primary100' : 'neutral100'}
219
246
  >
220
247
  <Typography variant="sigma" textColor={m.role === 'user' ? 'primary600' : 'neutral600'}>
221
- {m.role === 'user' ? 'Você' : 'IA'}
248
+ {m.role === 'user' ? t('home.you') : t('home.ai')}
222
249
  </Typography>
223
250
  <Box paddingTop={1}>
224
251
  <Typography style={{ whiteSpace: 'pre-wrap' }}>{m.content}</Typography>
@@ -234,7 +261,7 @@ const HomePage = () => {
234
261
  )}
235
262
  </Box>
236
263
  ))}
237
- {loading && <Typography textColor="neutral500">Processando…</Typography>}
264
+ {loading && <Typography textColor="neutral500">{t('home.processing')}</Typography>}
238
265
  </Flex>
239
266
  </Box>
240
267
 
@@ -249,12 +276,12 @@ const HomePage = () => {
249
276
  variant={recording ? 'danger-light' : 'tertiary'}
250
277
  onClick={recording ? stopRecording : startRecording}
251
278
  >
252
- {recording ? '⏹ Parar' : '🎤 Falar'}
279
+ {recording ? t('home.recStop') : t('home.rec')}
253
280
  </Button>
254
281
  <Box grow={1}>
255
282
  <Textarea
256
283
  name="message"
257
- placeholder="Escreva… (Cmd/Ctrl+Enter envia)"
284
+ placeholder={t('home.placeholder')}
258
285
  value={input}
259
286
  onChange={(e: any) => setInput(e.target.value)}
260
287
  onKeyDown={(e: any) => {
@@ -263,7 +290,7 @@ const HomePage = () => {
263
290
  />
264
291
  </Box>
265
292
  <Button onClick={send} loading={loading} disabled={!input.trim()}>
266
- Enviar
293
+ {t('home.send')}
267
294
  </Button>
268
295
  </Flex>
269
296
  </Flex>
@@ -274,13 +301,13 @@ const HomePage = () => {
274
301
  <Flex gap={2} alignItems="center">
275
302
  <Box grow={1}>
276
303
  <TextInput
277
- aria-label="URL do preview"
304
+ aria-label={t('home.previewUrlLabel')}
278
305
  value={previewUrl}
279
306
  onChange={(e: any) => setPreviewUrl(e.target.value)}
280
307
  />
281
308
  </Box>
282
309
  <Button size="S" variant="tertiary" onClick={() => setIframeKey((k) => k + 1)}>
283
- Recarregar
310
+ {t('home.reload')}
284
311
  </Button>
285
312
  </Flex>
286
313
  <Box
@@ -304,15 +331,19 @@ const HomePage = () => {
304
331
  <Box padding={6} background="neutral100" style={{ minHeight: '100vh' }}>
305
332
  <video ref={videoRef} autoPlay muted style={{ display: 'none' }} />
306
333
 
334
+ <Onboarding lang={lang} open={tourOpen} onClose={() => setTourOpen(false)} />
335
+
307
336
  <Flex justifyContent="flex-end" gap={2} paddingBottom={4}>
337
+ <Button variant="tertiary" onClick={() => setTourOpen(true)}>{t('home.tour')}</Button>
338
+ <LangSwitcher />
308
339
  <Link to="provision">
309
- <Button variant="secondary">Provisionar frontend</Button>
340
+ <Button variant="secondary">{t('home.provision')}</Button>
310
341
  </Link>
311
342
  <Button
312
343
  variant={previewOn ? 'success-light' : 'default'}
313
344
  onClick={() => setPreviewOn((v) => !v)}
314
345
  >
315
- {previewOn ? 'Live Preview: ON' : 'Live Preview: OFF'}
346
+ {previewOn ? t('home.previewOn') : t('home.previewOff')}
316
347
  </Button>
317
348
  </Flex>
318
349