Skip to content

Commit ee0be3b

Browse files
committed
feat: add i18n and spanish and catalan translations
1 parent 6a9cb78 commit ee0be3b

File tree

14 files changed

+373
-52
lines changed

14 files changed

+373
-52
lines changed

app/components/chat/Artifact.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ActionState } from '~/lib/runtime/action-runner';
77
import { workbenchStore } from '~/lib/stores/workbench';
88
import { classNames } from '~/utils/classNames';
99
import { cubicEasingFn } from '~/utils/easings';
10+
import { useTranslation } from 'react-i18next';
1011

1112
const highlighterOptions = {
1213
langs: ['shell'],
@@ -25,6 +26,7 @@ interface ArtifactProps {
2526
}
2627

2728
export const Artifact = memo(({ messageId }: ArtifactProps) => {
29+
const { t } = useTranslation();
2830
const userToggledActions = useRef(false);
2931
const [showActions, setShowActions] = useState(false);
3032

@@ -60,7 +62,7 @@ export const Artifact = memo(({ messageId }: ArtifactProps) => {
6062
>
6163
<div className="px-5 p-3.5 w-full text-left">
6264
<div className="w-full text-bolt-elements-textPrimary font-medium leading-5 text-sm">{artifact?.title}</div>
63-
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">Click to open Workbench</div>
65+
<div className="w-full w-full text-bolt-elements-textSecondary text-xs mt-0.5">{t('artifact.openWorkbench')}</div>
6466
</div>
6567
</button>
6668
<div className="bg-bolt-elements-artifacts-borderColor w-[1px]" />
@@ -130,6 +132,8 @@ const actionVariants = {
130132
};
131133

132134
const ActionList = memo(({ actions }: ActionListProps) => {
135+
const { t } = useTranslation();
136+
133137
return (
134138
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
135139
<ul className="list-none space-y-2.5">
@@ -162,14 +166,14 @@ const ActionList = memo(({ actions }: ActionListProps) => {
162166
</div>
163167
{type === 'file' ? (
164168
<div>
165-
Create{' '}
169+
{t('artifact.createFile')}{' '}
166170
<code className="bg-bolt-elements-artifacts-inlineCode-background text-bolt-elements-artifacts-inlineCode-text px-1.5 py-1 rounded-md">
167171
{action.filePath}
168172
</code>
169173
</div>
170174
) : type === 'shell' ? (
171175
<div className="flex items-center w-full min-h-[28px]">
172-
<span className="flex-1">Run command</span>
176+
<span className="flex-1">{t('artifact.runCommand')}</span>
173177
</div>
174178
) : null}
175179
</div>

app/components/chat/BaseChat.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Workbench } from '~/components/workbench/Workbench.client';
77
import { classNames } from '~/utils/classNames';
88
import { Messages } from './Messages.client';
99
import { SendButton } from './SendButton.client';
10+
import { useTranslation } from 'react-i18next';
1011

1112
import styles from './BaseChat.module.scss';
1213

@@ -27,14 +28,6 @@ interface BaseChatProps {
2728
enhancePrompt?: () => void;
2829
}
2930

30-
const EXAMPLE_PROMPTS = [
31-
{ text: 'Build a todo app in React using Tailwind' },
32-
{ text: 'Build a simple blog using Astro' },
33-
{ text: 'Create a cookie consent form using Material UI' },
34-
{ text: 'Make a space invaders game' },
35-
{ text: 'How do I center a div?' },
36-
];
37-
3831
const TEXTAREA_MIN_HEIGHT = 76;
3932

4033
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -57,7 +50,15 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
5750
},
5851
ref,
5952
) => {
53+
const { t } = useTranslation();
6054
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
55+
const EXAMPLE_PROMPTS = [
56+
{ text: t('chat.examplePrompts.todoApp') },
57+
{ text: t('chat.examplePrompts.astroBlog') },
58+
{ text: t('chat.examplePrompts.cookieConsent') },
59+
{ text: t('chat.examplePrompts.spaceInvaders') },
60+
{ text: t('chat.examplePrompts.centerDiv') },
61+
];
6162

6263
return (
6364
<div
@@ -74,10 +75,10 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
7475
{!chatStarted && (
7576
<div id="intro" className="mt-[26vh] max-w-chat mx-auto">
7677
<h1 className="text-5xl text-center font-bold text-bolt-elements-textPrimary mb-2">
77-
Where ideas begin
78+
{t('title')}
7879
</h1>
7980
<p className="mb-4 text-center text-bolt-elements-textSecondary">
80-
Bring ideas to life in seconds or get help on existing projects.
81+
{t('subtitle')}
8182
</p>
8283
</div>
8384
)}
@@ -130,7 +131,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
130131
minHeight: TEXTAREA_MIN_HEIGHT,
131132
maxHeight: TEXTAREA_MAX_HEIGHT,
132133
}}
133-
placeholder="How can Bolt help you today?"
134+
placeholder={t('messagePlaceholder')}
134135
translate="no"
135136
/>
136137
<ClientOnly>
@@ -152,7 +153,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
152153
<div className="flex justify-between text-sm p-4 pt-2">
153154
<div className="flex gap-1 items-center">
154155
<IconButton
155-
title="Enhance prompt"
156+
title={t('chat.enhancePrompt')}
156157
disabled={input.length === 0 || enhancingPrompt}
157158
className={classNames({
158159
'opacity-100!': enhancingPrompt,
@@ -164,19 +165,19 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
164165
{enhancingPrompt ? (
165166
<>
166167
<div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl"></div>
167-
<div className="ml-1.5">Enhancing prompt...</div>
168+
<div className="ml-1.5">{t('chat.enhancingPrompt')}</div>
168169
</>
169170
) : (
170171
<>
171172
<div className="i-bolt:stars text-xl"></div>
172-
{promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
173+
{promptEnhanced && <div className="ml-1.5">{t('chat.promptEnhanced')}</div>}
173174
</>
174175
)}
175176
</IconButton>
176177
</div>
177178
{input.length > 3 ? (
178179
<div className="text-xs text-bolt-elements-textTertiary">
179-
Use <kbd className="kdb">Shift</kbd> + <kbd className="kdb">Return</kbd> for a new line
180+
{t('chat.use')} <kbd className="kdb">{t('chat.shiftKey')}</kbd> + <kbd className="kdb">{t('chat.returnKey')}</kbd> {t('chat.forNewLine')}
180181
</div>
181182
) : null}
182183
</div>
@@ -210,4 +211,4 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
210211
</div>
211212
);
212213
},
213-
);
214+
);

app/components/sidebar/Menu.client.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { cubicEasingFn } from '~/utils/easings';
99
import { logger } from '~/utils/logger';
1010
import { HistoryItem } from './HistoryItem';
1111
import { binDates } from './date-binning';
12+
import { useTranslation } from 'react-i18next';
1213

1314
const menuVariants = {
1415
closed: {
@@ -34,6 +35,7 @@ const menuVariants = {
3435
type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
3536

3637
export function Menu() {
38+
const { t } = useTranslation();
3739
const menuRef = useRef<HTMLDivElement>(null);
3840
const [list, setList] = useState<ChatHistoryItem[]>([]);
3941
const [open, setOpen] = useState(false);
@@ -115,12 +117,12 @@ export function Menu() {
115117
className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
116118
>
117119
<span className="inline-block i-bolt:chat scale-110" />
118-
Start new chat
120+
{t('startNewChat')}
119121
</a>
120122
</div>
121-
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">Your Chats</div>
123+
<div className="text-bolt-elements-textPrimary font-medium pl-6 pr-5 my-2">{t('yourChats')}</div>
122124
<div className="flex-1 overflow-scroll pl-4 pr-5 pb-5">
123-
{list.length === 0 && <div className="pl-2 text-bolt-elements-textTertiary">No previous conversations</div>}
125+
{list.length === 0 && <div className="pl-2 text-bolt-elements-textTertiary">{t('noPreviousConversations')}</div>}
124126
<DialogRoot open={dialogContent !== null}>
125127
{binDates(list).map(({ category, items }) => (
126128
<div key={category} className="mt-4 first:mt-0 space-y-1">
@@ -139,14 +141,14 @@ export function Menu() {
139141
<DialogDescription asChild>
140142
<div>
141143
<p>
142-
You are about to delete <strong>{dialogContent.item.description}</strong>.
144+
{t('aboutToDelete')} <strong>{dialogContent.item.description}</strong>.
143145
</p>
144-
<p className="mt-1">Are you sure you want to delete this chat?</p>
146+
<p className="mt-1">{t('deleteChatConfirmation')}</p>
145147
</div>
146148
</DialogDescription>
147149
<div className="px-5 pb-4 bg-bolt-elements-background-depth-2 flex gap-2 justify-end">
148150
<DialogButton type="secondary" onClick={closeDialog}>
149-
Cancel
151+
{t('cancel')}
150152
</DialogButton>
151153
<DialogButton
152154
type="danger"
@@ -155,7 +157,7 @@ export function Menu() {
155157
closeDialog();
156158
}}
157159
>
158-
Delete
160+
{t('delete')}
159161
</DialogButton>
160162
</div>
161163
</>
@@ -167,7 +169,7 @@ export function Menu() {
167169
<a href="/logout">
168170
<IconButton className="p-1.5 gap-1.5">
169171
<>
170-
Logout <span className="i-ph:sign-out text-lg" />
172+
{t('logout')} <span className="i-ph:sign-out text-lg" />
171173
</>
172174
</IconButton>
173175
</a>

app/components/workbench/EditorPanel.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { isMobile } from '~/utils/mobile';
2323
import { FileBreadcrumb } from './FileBreadcrumb';
2424
import { FileTree } from './FileTree';
2525
import { Terminal, type TerminalRef } from './terminal/Terminal';
26+
import { useTranslation } from 'react-i18next';
2627

2728
interface EditorPanelProps {
2829
files?: FileMap;
@@ -80,6 +81,8 @@ export const EditorPanel = memo(
8081
return editorDocument !== undefined && unsavedFiles?.has(editorDocument.filePath);
8182
}, [editorDocument, unsavedFiles]);
8283

84+
const { t } = useTranslation();
85+
8386
useEffect(() => {
8487
const unsubscribeFromEventEmitter = shortcutEventEmitter.on('toggleTerminal', () => {
8588
terminalToggledByShortcut.current = true;
@@ -130,7 +133,7 @@ export const EditorPanel = memo(
130133
<div className="flex flex-col border-r border-bolt-elements-borderColor h-full">
131134
<PanelHeader>
132135
<div className="i-ph:tree-structure-duotone shrink-0" />
133-
Files
136+
{t('workbench.files')}
134137
</PanelHeader>
135138
<FileTree
136139
className="h-full"
@@ -153,11 +156,11 @@ export const EditorPanel = memo(
153156
<div className="flex gap-1 ml-auto -mr-1.5">
154157
<PanelHeaderButton onClick={onFileSave}>
155158
<div className="i-ph:floppy-disk-duotone" />
156-
Save
159+
{t('workbench.save')}
157160
</PanelHeaderButton>
158161
<PanelHeaderButton onClick={onFileReset}>
159162
<div className="i-ph:clock-counter-clockwise-duotone" />
160-
Reset
163+
{t('workbench.reset')}
161164
</PanelHeaderButton>
162165
</div>
163166
)}
@@ -216,15 +219,15 @@ export const EditorPanel = memo(
216219
onClick={() => setActiveTerminal(index)}
217220
>
218221
<div className="i-ph:terminal-window-duotone text-lg" />
219-
Terminal {terminalCount > 1 && index + 1}
222+
{t('Terminal')} {terminalCount > 1 && index + 1} {/* Translate "Terminal" */}
220223
</button>
221224
);
222225
})}
223226
{terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
224227
<IconButton
225228
className="ml-auto"
226229
icon="i-ph:caret-down"
227-
title="Close"
230+
title={t('workbench.closeWorkbench')}
228231
size="md"
229232
onClick={() => workbenchStore.toggleTerminal(false)}
230233
/>
@@ -253,4 +256,4 @@ export const EditorPanel = memo(
253256
</PanelGroup>
254257
);
255258
},
256-
);
259+
);

app/components/workbench/Preview.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
33
import { IconButton } from '~/components/ui/IconButton';
44
import { workbenchStore } from '~/lib/stores/workbench';
55
import { PortDropdown } from './PortDropdown';
6+
import { useTranslation } from 'react-i18next';
67

78
export const Preview = memo(() => {
89
const iframeRef = useRef<HTMLIFrameElement>(null);
@@ -71,6 +72,8 @@ export const Preview = memo(() => {
7172
}
7273
};
7374

75+
const { t } = useTranslation();
76+
7477
return (
7578
<div className="w-full h-full flex flex-col">
7679
{isPortDropdownOpen && (
@@ -116,7 +119,7 @@ export const Preview = memo(() => {
116119
{activePreview ? (
117120
<iframe ref={iframeRef} className="border-none w-full h-full bg-white" src={iframeUrl} />
118121
) : (
119-
<div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
122+
<div className="flex w-full h-full justify-center items-center bg-white">{t('preview.noPreview')}</div>
120123
)}
121124
</div>
122125
</div>

app/components/workbench/Workbench.client.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { motion, type HTMLMotionProps, type Variants } from 'framer-motion';
33
import { computed } from 'nanostores';
44
import { memo, useCallback, useEffect } from 'react';
55
import { toast } from 'react-toastify';
6+
import { useTranslation } from 'react-i18next';
67
import {
78
type OnChangeCallback as OnEditorChange,
89
type OnScrollCallback as OnEditorScroll,
@@ -24,17 +25,6 @@ interface WorkspaceProps {
2425

2526
const viewTransition = { ease: cubicEasingFn };
2627

27-
const sliderOptions: SliderOptions<WorkbenchViewType> = {
28-
left: {
29-
value: 'code',
30-
text: 'Code',
31-
},
32-
right: {
33-
value: 'preview',
34-
text: 'Preview',
35-
},
36-
};
37-
3828
const workbenchVariants = {
3929
closed: {
4030
width: 0,
@@ -54,6 +44,17 @@ const workbenchVariants = {
5444

5545
export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {
5646
renderLogger.trace('Workbench');
47+
const { t } = useTranslation();
48+
const sliderOptions: SliderOptions<WorkbenchViewType> = {
49+
left: {
50+
value: 'code',
51+
text: t('workbench.code'),
52+
},
53+
right: {
54+
value: 'preview',
55+
text: t('workbench.preview'),
56+
},
57+
};
5758

5859
const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0));
5960
const showWorkbench = useStore(workbenchStore.showWorkbench);
@@ -129,7 +130,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
129130
}}
130131
>
131132
<div className="i-ph:terminal" />
132-
Toggle Terminal
133+
{t('workbench.toggleTerminal')}
133134
</PanelHeaderButton>
134135
)}
135136
<IconButton
@@ -139,6 +140,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) =>
139140
onClick={() => {
140141
workbenchStore.showWorkbench.set(false);
141142
}}
143+
title={t('workbench.closeWorkbench')}
142144
/>
143145
</div>
144146
<div className="relative flex-1 overflow-hidden">

app/i18n.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import i18n from 'i18next';
2+
import { initReactI18next } from 'react-i18next';
3+
import LanguageDetector from 'i18next-browser-languagedetector';
4+
import Backend from 'i18next-http-backend';
5+
6+
export function initI18n() {
7+
if (!i18n.isInitialized) {
8+
i18n
9+
.use(Backend)
10+
.use(LanguageDetector)
11+
.use(initReactI18next)
12+
.init({
13+
lng: 'en',
14+
fallbackLng: 'en',
15+
supportedLngs: ['ca', 'es', 'en'],
16+
load: 'languageOnly',
17+
debug: true,
18+
interpolation: {
19+
escapeValue: false,
20+
},
21+
});
22+
}
23+
return i18n;
24+
}
25+
26+
export default i18n;

0 commit comments

Comments
 (0)