suisuyy commited on
Commit
69ae4e4
·
1 Parent(s): 8769c74

Rename project to xpaintai_zh in package.json and metadata.json; integrate translation support in various components and hooks

Browse files
App.tsx CHANGED
@@ -6,6 +6,8 @@ import ZoomSlider from './components/ZoomSlider';
6
  import SettingsPanel from './components/SettingsPanel';
7
  import ConfirmModal from './components/ConfirmModal';
8
  import { DEFAULT_AI_API_ENDPOINT } from './constants';
 
 
9
 
10
  // Custom Hooks
11
  import { useToasts } from './hooks/useToasts';
@@ -16,6 +18,8 @@ import { useFullscreen } from './hooks/useFullscreen';
16
  import { useConfirmModal } from './hooks/useConfirmModal';
17
  import { useCanvasFileUtils } from './hooks/useCanvasFileUtils';
18
 
 
 
19
  const App: React.FC = () => {
20
  // Core UI States
21
  const [zoomLevel, setZoomLevel] = useState<number>(0.5);
@@ -53,8 +57,8 @@ const App: React.FC = () => {
53
  effectivePenColor,
54
  } = useDrawingTools();
55
 
56
- const { isFullscreenActive, toggleFullscreen } = useFullscreen(showToast);
57
- const { confirmModalInfo, requestConfirmation, closeConfirmModal } = useConfirmModal();
58
 
59
  const {
60
  handleLoadImageFile,
@@ -66,6 +70,7 @@ const App: React.FC = () => {
66
  historyActions: { updateCanvasState },
67
  uiActions: { showToast, setZoomLevel },
68
  confirmModalActions: { requestConfirmation },
 
69
  });
70
 
71
  const {
@@ -93,6 +98,7 @@ const App: React.FC = () => {
93
  aiDimensionsMode,
94
  currentCanvasWidth: canvasWidth, // Pass current canvas dimensions
95
  currentCanvasHeight: canvasHeight, // Pass current canvas dimensions
 
96
  });
97
 
98
  const handlePromptChange = (newPrompt: string) => {
@@ -116,8 +122,8 @@ const App: React.FC = () => {
116
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
117
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
118
  </svg>
119
- <p className="text-2xl font-semibold text-slate-700">Loading Your Canvas...</p>
120
- <p className="text-sm text-slate-500 mt-1">Getting things ready!</p>
121
  </div>
122
  </div>
123
  );
@@ -144,6 +150,7 @@ const App: React.FC = () => {
144
  isMagicUploading={isMagicUploading}
145
  canShare={canShare}
146
  onToggleSettings={handleToggleSettingsPanel}
 
147
  />
148
  <main className="flex-grow grid place-items-center p-4 md:p-6 w-full mt-4 mb-4 overflow-auto">
149
  <div
@@ -152,7 +159,7 @@ const App: React.FC = () => {
152
  transformOrigin: 'top left',
153
  transition: 'transform 0.1s ease-out'
154
  }}
155
- aria-label={`Canvas zoomed to ${Math.round(zoomLevel * 100)}%`}
156
  >
157
  {currentDataURL ? (
158
  <CanvasComponent
@@ -165,13 +172,14 @@ const App: React.FC = () => {
165
  canvasPhysicalWidth={canvasWidth}
166
  canvasPhysicalHeight={canvasHeight}
167
  zoomLevel={zoomLevel}
 
168
  />
169
  ) : (
170
  <div
171
  style={{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }}
172
  className="border-2 border-slate-300 rounded-lg shadow-xl bg-white flex justify-center items-center"
173
  >
174
- <p className="text-slate-500">Initializing Canvas...</p>
175
  </div>
176
  )}
177
  </div>
@@ -190,6 +198,7 @@ const App: React.FC = () => {
190
  isAsking={isAskingAi}
191
  error={aiEditError}
192
  askUrl={askUrl}
 
193
  />
194
  )}
195
 
@@ -197,6 +206,7 @@ const App: React.FC = () => {
197
  <ZoomSlider
198
  zoomLevel={zoomLevel}
199
  onZoomChange={setZoomLevel}
 
200
  />
201
  )}
202
 
@@ -217,6 +227,7 @@ const App: React.FC = () => {
217
  currentCanvasWidth={canvasWidth}
218
  currentCanvasHeight={canvasHeight}
219
  onCanvasSizeChange={handleCanvasSizeChange}
 
220
  />
221
  )}
222
 
 
6
  import SettingsPanel from './components/SettingsPanel';
7
  import ConfirmModal from './components/ConfirmModal';
8
  import { DEFAULT_AI_API_ENDPOINT } from './constants';
9
+ import { getTranslator } from './translations';
10
+ import { TFunction } from './types';
11
 
12
  // Custom Hooks
13
  import { useToasts } from './hooks/useToasts';
 
18
  import { useConfirmModal } from './hooks/useConfirmModal';
19
  import { useCanvasFileUtils } from './hooks/useCanvasFileUtils';
20
 
21
+ const t: TFunction = getTranslator();
22
+
23
  const App: React.FC = () => {
24
  // Core UI States
25
  const [zoomLevel, setZoomLevel] = useState<number>(0.5);
 
57
  effectivePenColor,
58
  } = useDrawingTools();
59
 
60
+ const { isFullscreenActive, toggleFullscreen } = useFullscreen(showToast, t);
61
+ const { confirmModalInfo, requestConfirmation, closeConfirmModal } = useConfirmModal(t);
62
 
63
  const {
64
  handleLoadImageFile,
 
70
  historyActions: { updateCanvasState },
71
  uiActions: { showToast, setZoomLevel },
72
  confirmModalActions: { requestConfirmation },
73
+ t,
74
  });
75
 
76
  const {
 
98
  aiDimensionsMode,
99
  currentCanvasWidth: canvasWidth, // Pass current canvas dimensions
100
  currentCanvasHeight: canvasHeight, // Pass current canvas dimensions
101
+ t,
102
  });
103
 
104
  const handlePromptChange = (newPrompt: string) => {
 
122
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
123
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
124
  </svg>
125
+ <p className="text-2xl font-semibold text-slate-700">{t('loadingCanvasMessage')}</p>
126
+ <p className="text-sm text-slate-500 mt-1">{t('loadingCanvasSubMessage')}</p>
127
  </div>
128
  </div>
129
  );
 
150
  isMagicUploading={isMagicUploading}
151
  canShare={canShare}
152
  onToggleSettings={handleToggleSettingsPanel}
153
+ t={t}
154
  />
155
  <main className="flex-grow grid place-items-center p-4 md:p-6 w-full mt-4 mb-4 overflow-auto">
156
  <div
 
159
  transformOrigin: 'top left',
160
  transition: 'transform 0.1s ease-out'
161
  }}
162
+ aria-label={t('zoomAria', { percentage: Math.round(zoomLevel * 100) })}
163
  >
164
  {currentDataURL ? (
165
  <CanvasComponent
 
172
  canvasPhysicalWidth={canvasWidth}
173
  canvasPhysicalHeight={canvasHeight}
174
  zoomLevel={zoomLevel}
175
+ t={t}
176
  />
177
  ) : (
178
  <div
179
  style={{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }}
180
  className="border-2 border-slate-300 rounded-lg shadow-xl bg-white flex justify-center items-center"
181
  >
182
+ <p className="text-slate-500">{t('initializingCanvas')}</p>
183
  </div>
184
  )}
185
  </div>
 
198
  isAsking={isAskingAi}
199
  error={aiEditError}
200
  askUrl={askUrl}
201
+ t={t}
202
  />
203
  )}
204
 
 
206
  <ZoomSlider
207
  zoomLevel={zoomLevel}
208
  onZoomChange={setZoomLevel}
209
+ t={t}
210
  />
211
  )}
212
 
 
227
  currentCanvasWidth={canvasWidth}
228
  currentCanvasHeight={canvasHeight}
229
  onCanvasSizeChange={handleCanvasSizeChange}
230
+ t={t}
231
  />
232
  )}
233
 
components/AiEditModal.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import React, { useEffect, useRef } from 'react';
2
  import { MagicSparkleIcon, CloseIcon, QuestionMarkIcon } from './icons';
 
3
 
4
  interface AiEditModalProps {
5
  isOpen: boolean;
@@ -13,6 +14,7 @@ interface AiEditModalProps {
13
  isAsking: boolean;
14
  error: string | null;
15
  askUrl: string | null;
 
16
  }
17
 
18
  const AiEditModal: React.FC<AiEditModalProps> = ({
@@ -27,6 +29,7 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
27
  isAsking,
28
  error,
29
  askUrl,
 
30
  }) => {
31
  const iframeContainerRef = useRef<HTMLDivElement>(null);
32
 
@@ -49,11 +52,11 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
49
  >
50
  <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-lg transform transition-all max-h-[calc(100vh-5rem)] overflow-y-auto">
51
  <div className="flex justify-between items-center mb-4">
52
- <h2 id="ai-edit-modal-title" className="text-xl font-semibold text-slate-700">Edit or Ask about Image with AI</h2>
53
  <button
54
  onClick={onClose}
55
  className="text-slate-400 hover:text-slate-600 transition-colors"
56
- aria-label="Close AI edit modal"
57
  disabled={isAnyLoading}
58
  >
59
  <CloseIcon className="w-6 h-6" />
@@ -70,13 +73,13 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
70
 
71
  <div className="mb-4">
72
  <label htmlFor="aiPrompt" className="block text-sm font-medium text-slate-700 mb-1">
73
- Describe what you want to change, or ask a question:
74
  </label>
75
  <textarea
76
  id="aiPrompt"
77
  value={prompt}
78
  onChange={(e) => onPromptChange(e.target.value)}
79
- placeholder="e.g., 'make the cat wear a party hat' or 'what is in this image?'"
80
  className="w-full p-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-xl h-[200px]"
81
  disabled={isAnyLoading}
82
  aria-describedby={error ? "ai-edit-error" : undefined}
@@ -85,7 +88,7 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
85
 
86
  {error && (
87
  <div id="ai-edit-error" className="mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-md text-sm" role="alert">
88
- <p className="font-semibold">Error:</p>
89
  <p>{error}</p>
90
  </div>
91
  )}
@@ -102,12 +105,12 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
102
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
103
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
104
  </svg>
105
- Asking...
106
  </>
107
  ) : (
108
  <>
109
  <QuestionMarkIcon className="w-4 h-4 mr-1" />
110
- Ask
111
  </>
112
  )}
113
  </button>
@@ -122,12 +125,12 @@ const AiEditModal: React.FC<AiEditModalProps> = ({
122
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
123
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
124
  </svg>
125
- Generating...
126
  </>
127
  ) : (
128
  <>
129
  <MagicSparkleIcon className="w-4 h-4 mr-1" />
130
- Generate Image
131
  </>
132
  )}
133
  </button>
 
1
  import React, { useEffect, useRef } from 'react';
2
  import { MagicSparkleIcon, CloseIcon, QuestionMarkIcon } from './icons';
3
+ import { TFunction } from '../types';
4
 
5
  interface AiEditModalProps {
6
  isOpen: boolean;
 
14
  isAsking: boolean;
15
  error: string | null;
16
  askUrl: string | null;
17
+ t: TFunction;
18
  }
19
 
20
  const AiEditModal: React.FC<AiEditModalProps> = ({
 
29
  isAsking,
30
  error,
31
  askUrl,
32
+ t,
33
  }) => {
34
  const iframeContainerRef = useRef<HTMLDivElement>(null);
35
 
 
52
  >
53
  <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-lg transform transition-all max-h-[calc(100vh-5rem)] overflow-y-auto">
54
  <div className="flex justify-between items-center mb-4">
55
+ <h2 id="ai-edit-modal-title" className="text-xl font-semibold text-slate-700">{t('aiModalTitle')}</h2>
56
  <button
57
  onClick={onClose}
58
  className="text-slate-400 hover:text-slate-600 transition-colors"
59
+ aria-label={t('closeAiModalAria')}
60
  disabled={isAnyLoading}
61
  >
62
  <CloseIcon className="w-6 h-6" />
 
73
 
74
  <div className="mb-4">
75
  <label htmlFor="aiPrompt" className="block text-sm font-medium text-slate-700 mb-1">
76
+ {t('aiPromptLabel')}
77
  </label>
78
  <textarea
79
  id="aiPrompt"
80
  value={prompt}
81
  onChange={(e) => onPromptChange(e.target.value)}
82
+ placeholder={t('aiPromptPlaceholder')}
83
  className="w-full p-2 border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-xl h-[200px]"
84
  disabled={isAnyLoading}
85
  aria-describedby={error ? "ai-edit-error" : undefined}
 
88
 
89
  {error && (
90
  <div id="ai-edit-error" className="mb-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-md text-sm" role="alert">
91
+ <p className="font-semibold">{t('errorPrefix')}</p>
92
  <p>{error}</p>
93
  </div>
94
  )}
 
105
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
106
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
107
  </svg>
108
+ {t('aiAsking')}
109
  </>
110
  ) : (
111
  <>
112
  <QuestionMarkIcon className="w-4 h-4 mr-1" />
113
+ {t('aiAsk')}
114
  </>
115
  )}
116
  </button>
 
125
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
126
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
127
  </svg>
128
+ {t('aiGenerating')}
129
  </>
130
  ) : (
131
  <>
132
  <MagicSparkleIcon className="w-4 h-4 mr-1" />
133
+ {t('aiGenerateImage')}
134
  </>
135
  )}
136
  </button>
components/CanvasComponent.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import React, { useRef, useEffect, useState, useCallback } from 'react';
 
2
 
3
  interface CanvasComponentProps {
4
  penColor: string;
@@ -9,6 +10,7 @@ interface CanvasComponentProps {
9
  canvasPhysicalWidth: number;
10
  canvasPhysicalHeight: number;
11
  zoomLevel: number; // New prop
 
12
  }
13
 
14
  const CanvasComponent: React.FC<CanvasComponentProps> = ({
@@ -20,6 +22,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({
20
  canvasPhysicalWidth,
21
  canvasPhysicalHeight,
22
  zoomLevel, // Use zoomLevel
 
23
  }) => {
24
  const canvasRef = useRef<HTMLCanvasElement>(null);
25
  const [isDrawing, setIsDrawing] = useState(false);
@@ -194,7 +197,7 @@ const CanvasComponent: React.FC<CanvasComponentProps> = ({
194
  // Style for imageRendering is good for pixel art, ensure it doesn't conflict with scaling needs.
195
  // Visual scaling is handled by the parent div's transform.
196
  style={{ imageRendering: 'pixelated' }}
197
- aria-label="Drawing canvas"
198
  />
199
  );
200
  };
 
1
  import React, { useRef, useEffect, useState, useCallback } from 'react';
2
+ import { TFunction } from '../types';
3
 
4
  interface CanvasComponentProps {
5
  penColor: string;
 
10
  canvasPhysicalWidth: number;
11
  canvasPhysicalHeight: number;
12
  zoomLevel: number; // New prop
13
+ t: TFunction;
14
  }
15
 
16
  const CanvasComponent: React.FC<CanvasComponentProps> = ({
 
22
  canvasPhysicalWidth,
23
  canvasPhysicalHeight,
24
  zoomLevel, // Use zoomLevel
25
+ t,
26
  }) => {
27
  const canvasRef = useRef<HTMLCanvasElement>(null);
28
  const [isDrawing, setIsDrawing] = useState(false);
 
197
  // Style for imageRendering is good for pixel art, ensure it doesn't conflict with scaling needs.
198
  // Visual scaling is handled by the parent div's transform.
199
  style={{ imageRendering: 'pixelated' }}
200
+ aria-label={t('drawingCanvas')}
201
  />
202
  );
203
  };
components/SettingsPanel.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import React from 'react';
2
  import { CloseIcon, FullscreenEnterIcon, FullscreenExitIcon } from './icons'; // Import fullscreen icons
3
  import { AiImageQuality, AiDimensionsMode } from '../hooks/useAiFeatures';
 
4
 
5
  interface SettingsPanelProps {
6
  isOpen: boolean;
@@ -18,6 +19,7 @@ interface SettingsPanelProps {
18
  currentCanvasWidth: number;
19
  currentCanvasHeight: number;
20
  onCanvasSizeChange: (width: number, height: number) => void;
 
21
  }
22
 
23
  const CANVAS_SIZE_OPTIONS = [
@@ -31,14 +33,14 @@ const CANVAS_SIZE_OPTIONS = [
31
 
32
  const AI_API_OPTIONS = [
33
  {
34
- label: 'Pollinations AI',
35
  value: 'https://rpfor.deno.dev/proxy?url=https://image.pollinations.ai/prompt/{prompt}?nologo=true&model=gptimage&image={imgurl.url}&quality={quality}{size_params}'
36
  },
37
  {
38
- label: 'Gemini Simple Get',
39
  value: 'https://geminisimpleget.deno.dev/prompt/{prompt}?image={imgurl.url}'
40
  }
41
- ];
42
 
43
 
44
  const SettingsPanel: React.FC<SettingsPanelProps> = ({
@@ -57,13 +59,14 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
57
  currentCanvasWidth,
58
  currentCanvasHeight,
59
  onCanvasSizeChange,
 
60
  }) => {
61
  if (!isOpen) return null;
62
 
63
  const qualityOptions: { label: string; value: AiImageQuality }[] = [
64
- { label: 'Low', value: 'low' },
65
- { label: 'Medium', value: 'medium' },
66
- { label: 'High (HD)', value: 'hd' },
67
  ];
68
 
69
  const handleSizeSelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
@@ -86,12 +89,12 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
86
  <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-md transform transition-all overflow-y-auto max-h-[90vh]">
87
  <div className="flex justify-between items-center mb-6">
88
  <h2 id="settings-panel-title" className="text-xl font-semibold text-slate-800">
89
- Application Settings
90
  </h2>
91
  <button
92
  onClick={onClose}
93
  className="text-slate-400 hover:text-slate-600 transition-colors"
94
- aria-label="Close settings panel"
95
  >
96
  <CloseIcon className="w-6 h-6" />
97
  </button>
@@ -99,12 +102,12 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
99
 
100
  {/* Interface Settings */}
101
  <div className="mb-6">
102
- <h3 className="text-md font-medium text-slate-700 mb-2">Interface</h3>
103
  <div className="space-y-3">
104
  {/* Zoom Slider Toggle */}
105
  <div className="flex items-center justify-between bg-slate-50 p-3 rounded-md border border-slate-200">
106
  <label htmlFor="showZoomSliderToggle" className="text-sm text-slate-600">
107
- Show Zoom Slider
108
  </label>
109
  <button
110
  id="showZoomSliderToggle"
@@ -115,7 +118,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
115
  showZoomSlider ? 'bg-blue-600' : 'bg-slate-300'
116
  }`}
117
  >
118
- <span className="sr-only">Toggle Zoom Slider Visibility</span>
119
  <span
120
  className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform duration-200 ease-in-out ${
121
  showZoomSlider ? 'translate-x-6' : 'translate-x-1'
@@ -129,14 +132,14 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
129
  <button
130
  onClick={onToggleFullscreen}
131
  className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
132
- aria-label={isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'}
133
  >
134
  {isFullscreenActive ? (
135
  <FullscreenExitIcon className="w-5 h-5 mr-2" />
136
  ) : (
137
  <FullscreenEnterIcon className="w-5 h-5 mr-2" />
138
  )}
139
- {isFullscreenActive ? 'Exit Fullscreen' : 'Enter Fullscreen'}
140
  </button>
141
  </div>
142
  </div>
@@ -144,10 +147,10 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
144
 
145
  {/* Canvas Dimensions Settings */}
146
  <div className="mb-6">
147
- <h3 className="text-md font-medium text-slate-700 mb-2">Canvas Dimensions</h3>
148
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200">
149
  <label htmlFor="canvasSizeSelect" className="block text-sm text-slate-600 mb-1">
150
- Canvas Size
151
  </label>
152
  <select
153
  id="canvasSizeSelect"
@@ -163,7 +166,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
163
  ))}
164
  </select>
165
  <p id="canvas-size-description" className="text-xs text-slate-500 mt-1">
166
- Changing size will clear the current canvas and history.
167
  </p>
168
  </div>
169
  </div>
@@ -171,11 +174,11 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
171
 
172
  {/* AI Image Generation Settings */}
173
  <div className="mb-6">
174
- <h3 className="text-md font-medium text-slate-700 mb-2">AI Image Generation</h3>
175
 
176
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4">
177
  <label htmlFor="aiApiEndpointSelect" className="block text-sm text-slate-600 mb-1">
178
- API Endpoint
179
  </label>
180
  <select
181
  id="aiApiEndpointSelect"
@@ -186,18 +189,18 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
186
  >
187
  {AI_API_OPTIONS.map((option) => (
188
  <option key={option.value} value={option.value}>
189
- {option.label}
190
  </option>
191
  ))}
192
  </select>
193
  <p id="ai-api-description" className="text-xs text-slate-500 mt-1">
194
- Select an API for image generation. Placeholders are filled automatically.
195
  </p>
196
  </div>
197
 
198
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4">
199
  <label htmlFor="aiDimensionsModeSelect" className="block text-sm text-slate-600 mb-1">
200
- AI Image Dimensions
201
  </label>
202
  <select
203
  id="aiDimensionsModeSelect"
@@ -206,18 +209,18 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
206
  className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm"
207
  aria-describedby="ai-dimensions-description"
208
  >
209
- <option value="api_default">API Default (no size)</option>
210
- <option value="match_canvas">Match Canvas Size</option>
211
- <option value="fixed_1024">Fixed 1024x1024</option>
212
  </select>
213
  <p id="ai-dimensions-description" className="text-xs text-slate-500 mt-1">
214
- Controls `width` & `height` params for APIs that support them (e.g., Pollinations AI).
215
  </p>
216
  </div>
217
 
218
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200">
219
  <label htmlFor="aiImageQualitySelect" className="block text-sm text-slate-600 mb-1">
220
- Image Quality
221
  </label>
222
  <select
223
  id="aiImageQualitySelect"
@@ -233,7 +236,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
233
  ))}
234
  </select>
235
  <p id="ai-quality-description" className="text-xs text-slate-500 mt-1">
236
- Used for APIs that support a 'quality' parameter (e.g., Pollinations AI).
237
  </p>
238
  </div>
239
  </div>
@@ -243,7 +246,7 @@ const SettingsPanel: React.FC<SettingsPanelProps> = ({
243
  onClick={onClose}
244
  className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-sm font-medium"
245
  >
246
- Done
247
  </button>
248
  </div>
249
  </div>
 
1
  import React from 'react';
2
  import { CloseIcon, FullscreenEnterIcon, FullscreenExitIcon } from './icons'; // Import fullscreen icons
3
  import { AiImageQuality, AiDimensionsMode } from '../hooks/useAiFeatures';
4
+ import { TFunction } from '../types';
5
 
6
  interface SettingsPanelProps {
7
  isOpen: boolean;
 
19
  currentCanvasWidth: number;
20
  currentCanvasHeight: number;
21
  onCanvasSizeChange: (width: number, height: number) => void;
22
+ t: TFunction;
23
  }
24
 
25
  const CANVAS_SIZE_OPTIONS = [
 
33
 
34
  const AI_API_OPTIONS = [
35
  {
36
+ labelKey: 'pollinationsApi',
37
  value: 'https://rpfor.deno.dev/proxy?url=https://image.pollinations.ai/prompt/{prompt}?nologo=true&model=gptimage&image={imgurl.url}&quality={quality}{size_params}'
38
  },
39
  {
40
+ labelKey: 'geminiSimpleGetApi',
41
  value: 'https://geminisimpleget.deno.dev/prompt/{prompt}?image={imgurl.url}'
42
  }
43
+ ] as const;
44
 
45
 
46
  const SettingsPanel: React.FC<SettingsPanelProps> = ({
 
59
  currentCanvasWidth,
60
  currentCanvasHeight,
61
  onCanvasSizeChange,
62
+ t,
63
  }) => {
64
  if (!isOpen) return null;
65
 
66
  const qualityOptions: { label: string; value: AiImageQuality }[] = [
67
+ { label: t('qualityLow'), value: 'low' },
68
+ { label: t('qualityMedium'), value: 'medium' },
69
+ { label: t('qualityHigh'), value: 'hd' },
70
  ];
71
 
72
  const handleSizeSelection = (event: React.ChangeEvent<HTMLSelectElement>) => {
 
89
  <div className="bg-white rounded-lg shadow-2xl p-6 w-full max-w-md transform transition-all overflow-y-auto max-h-[90vh]">
90
  <div className="flex justify-between items-center mb-6">
91
  <h2 id="settings-panel-title" className="text-xl font-semibold text-slate-800">
92
+ {t('settingsTitle')}
93
  </h2>
94
  <button
95
  onClick={onClose}
96
  className="text-slate-400 hover:text-slate-600 transition-colors"
97
+ aria-label={t('closeSettingsAria')}
98
  >
99
  <CloseIcon className="w-6 h-6" />
100
  </button>
 
102
 
103
  {/* Interface Settings */}
104
  <div className="mb-6">
105
+ <h3 className="text-md font-medium text-slate-700 mb-2">{t('interface')}</h3>
106
  <div className="space-y-3">
107
  {/* Zoom Slider Toggle */}
108
  <div className="flex items-center justify-between bg-slate-50 p-3 rounded-md border border-slate-200">
109
  <label htmlFor="showZoomSliderToggle" className="text-sm text-slate-600">
110
+ {t('showZoomSlider')}
111
  </label>
112
  <button
113
  id="showZoomSliderToggle"
 
118
  showZoomSlider ? 'bg-blue-600' : 'bg-slate-300'
119
  }`}
120
  >
121
+ <span className="sr-only">{t('toggleZoomSliderAria')}</span>
122
  <span
123
  className={`inline-block w-4 h-4 transform bg-white rounded-full transition-transform duration-200 ease-in-out ${
124
  showZoomSlider ? 'translate-x-6' : 'translate-x-1'
 
132
  <button
133
  onClick={onToggleFullscreen}
134
  className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-blue-700 bg-blue-100 hover:bg-blue-200 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
135
+ aria-label={isFullscreenActive ? t('exitFullscreenAria') : t('fullscreenAria')}
136
  >
137
  {isFullscreenActive ? (
138
  <FullscreenExitIcon className="w-5 h-5 mr-2" />
139
  ) : (
140
  <FullscreenEnterIcon className="w-5 h-5 mr-2" />
141
  )}
142
+ {isFullscreenActive ? t('exitFullscreen') : t('enterFullscreen')}
143
  </button>
144
  </div>
145
  </div>
 
147
 
148
  {/* Canvas Dimensions Settings */}
149
  <div className="mb-6">
150
+ <h3 className="text-md font-medium text-slate-700 mb-2">{t('canvasDimensions')}</h3>
151
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200">
152
  <label htmlFor="canvasSizeSelect" className="block text-sm text-slate-600 mb-1">
153
+ {t('canvasSize')}
154
  </label>
155
  <select
156
  id="canvasSizeSelect"
 
166
  ))}
167
  </select>
168
  <p id="canvas-size-description" className="text-xs text-slate-500 mt-1">
169
+ {t('canvasSizeDescription')}
170
  </p>
171
  </div>
172
  </div>
 
174
 
175
  {/* AI Image Generation Settings */}
176
  <div className="mb-6">
177
+ <h3 className="text-md font-medium text-slate-700 mb-2">{t('aiImageGeneration')}</h3>
178
 
179
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4">
180
  <label htmlFor="aiApiEndpointSelect" className="block text-sm text-slate-600 mb-1">
181
+ {t('apiEndpoint')}
182
  </label>
183
  <select
184
  id="aiApiEndpointSelect"
 
189
  >
190
  {AI_API_OPTIONS.map((option) => (
191
  <option key={option.value} value={option.value}>
192
+ {t(option.labelKey)}
193
  </option>
194
  ))}
195
  </select>
196
  <p id="ai-api-description" className="text-xs text-slate-500 mt-1">
197
+ {t('apiEndpointDescription')}
198
  </p>
199
  </div>
200
 
201
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200 mb-4">
202
  <label htmlFor="aiDimensionsModeSelect" className="block text-sm text-slate-600 mb-1">
203
+ {t('aiImageDimensions')}
204
  </label>
205
  <select
206
  id="aiDimensionsModeSelect"
 
209
  className="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500 focus:border-blue-500 transition-shadow text-sm"
210
  aria-describedby="ai-dimensions-description"
211
  >
212
+ <option value="api_default">{t('apiDefault')}</option>
213
+ <option value="match_canvas">{t('matchCanvasSize')}</option>
214
+ <option value="fixed_1024">{t('fixed1024')}</option>
215
  </select>
216
  <p id="ai-dimensions-description" className="text-xs text-slate-500 mt-1">
217
+ {t('aiImageDimensionsDescription')}
218
  </p>
219
  </div>
220
 
221
  <div className="bg-slate-50 p-3 rounded-md border border-slate-200">
222
  <label htmlFor="aiImageQualitySelect" className="block text-sm text-slate-600 mb-1">
223
+ {t('imageQuality')}
224
  </label>
225
  <select
226
  id="aiImageQualitySelect"
 
236
  ))}
237
  </select>
238
  <p id="ai-quality-description" className="text-xs text-slate-500 mt-1">
239
+ {t('imageQualityDescription')}
240
  </p>
241
  </div>
242
  </div>
 
246
  onClick={onClose}
247
  className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors text-sm font-medium"
248
  >
249
+ {t('done')}
250
  </button>
251
  </div>
252
  </div>
components/Toolbar.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import React from 'react';
2
  import { UndoIcon, RedoIcon, UploadIcon, DownloadIcon, ClearIcon, BrushIcon, EraserIcon, MagicSparkleIcon, SettingsIcon } from './icons';
 
3
 
4
  interface ToolbarProps {
5
  penColor: string;
@@ -18,7 +19,8 @@ interface ToolbarProps {
18
  onMagicUpload: () => void;
19
  isMagicUploading: boolean;
20
  canShare: boolean;
21
- onToggleSettings: () => void; // New prop for settings
 
22
  }
23
 
24
  const Toolbar: React.FC<ToolbarProps> = ({
@@ -38,7 +40,8 @@ const Toolbar: React.FC<ToolbarProps> = ({
38
  onMagicUpload,
39
  isMagicUploading,
40
  canShare,
41
- onToggleSettings, // New prop
 
42
  }) => {
43
  const fileInputRef = React.useRef<HTMLInputElement>(null);
44
 
@@ -53,9 +56,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
53
  <button
54
  onClick={onUndo}
55
  disabled={!canUndo}
56
- title="Undo"
57
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
58
- aria-label="Undo last action"
59
  >
60
  <UndoIcon className="w-5 h-5 text-gray-700" />
61
  </button>
@@ -63,9 +66,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
63
  <button
64
  onClick={onRedo}
65
  disabled={!canRedo}
66
- title="Redo"
67
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
68
- aria-label="Redo last undone action"
69
  >
70
  <RedoIcon className="w-5 h-5 text-gray-700" />
71
  </button>
@@ -76,41 +79,41 @@ const Toolbar: React.FC<ToolbarProps> = ({
76
  onChange={onLoadImage}
77
  accept="image/png, image/jpeg"
78
  className="hidden"
79
- aria-label="Load image from file"
80
  />
81
  <button
82
  onClick={handleUploadClick}
83
- title="Load Image"
84
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
85
- aria-label="Load image"
86
  >
87
  <UploadIcon className="w-5 h-5 text-gray-700" />
88
  </button>
89
 
90
  <button
91
  onClick={onExportImage}
92
- title="Export Image"
93
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
94
- aria-label="Export canvas as image"
95
  >
96
  <DownloadIcon className="w-5 h-5 text-gray-700" />
97
  </button>
98
 
99
  <button
100
  onClick={onMagicUpload}
101
- title="Share Canvas & Edit with AI"
102
  disabled={isMagicUploading || !canShare}
103
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
104
- aria-label="Share canvas by uploading and open AI edit options"
105
  >
106
  <MagicSparkleIcon className="w-5 h-5 text-purple-600" />
107
  </button>
108
 
109
  <button
110
  onClick={onClearCanvas}
111
- title="Clear Canvas"
112
  className="p-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors tool-button"
113
- aria-label="Clear canvas"
114
  >
115
  <ClearIcon className="w-5 h-5" />
116
  </button>
@@ -120,40 +123,40 @@ const Toolbar: React.FC<ToolbarProps> = ({
120
  <div className="flex flex-wrap items-center justify-start gap-x-3 gap-y-2 mt-2">
121
  <button
122
  onClick={onToggleEraser}
123
- title={isEraserMode ? "Switch to Brush" : "Switch to Eraser"}
124
  className={`p-2 rounded-md transition-colors ${isEraserMode ? 'bg-blue-500 text-white hover:bg-blue-600' : ' hover:bg-gray-100 text-gray-700 border-gray-300'}`}
125
- aria-label={isEraserMode ? "Switch to Brush mode" : "Switch to Eraser mode"}
126
  aria-pressed={isEraserMode}
127
  >
128
  {isEraserMode ? <BrushIcon className="w-5 h-5" /> : <EraserIcon className="w-5 h-5" />}
129
  </button>
130
 
131
  <div className="flex items-center gap-2 p-1 rounded-md hover:bg-slate-300 transition-colors">
132
- <label htmlFor="penColor" className="text-sm font-medium text-gray-700 sr-only">Color:</label>
133
  <input
134
  type="color"
135
  id="penColor"
136
- title="Pen Color"
137
  value={penColor}
138
  onChange={(e) => onColorChange(e.target.value)}
139
  className={`w-8 h-8 rounded-full cursor-pointer border-2 ${isEraserMode ? 'border-gray-400 opacity-50' : 'border-white shadow-sm'}`}
140
  disabled={isEraserMode}
141
- aria-label="Select pen color"
142
  />
143
  </div>
144
 
145
  <div className="flex items-center gap-2 p-1 rounded-md hover:bg-slate-300 transition-colors">
146
- <label htmlFor="penSize" className="text-sm font-medium text-gray-700 sr-only">Size:</label>
147
  <input
148
  type="range"
149
  id="penSize"
150
- title={`Pen Size: ${penSize}`}
151
  min="1"
152
  max="50"
153
  value={penSize}
154
  onChange={(e) => onSizeChange(parseInt(e.target.value, 10))}
155
  className="w-24 md:w-32 cursor-pointer accent-blue-600"
156
- aria-label={`Pen size ${penSize} pixels`}
157
  aria-valuemin={1}
158
  aria-valuemax={50}
159
  aria-valuenow={penSize}
@@ -163,9 +166,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
163
 
164
  <button
165
  onClick={onToggleSettings}
166
- title="Open Settings"
167
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
168
- aria-label="Open application settings"
169
  >
170
  <SettingsIcon className="w-5 h-5 text-gray-700" />
171
  </button>
@@ -174,4 +177,4 @@ const Toolbar: React.FC<ToolbarProps> = ({
174
  );
175
  };
176
 
177
- export default Toolbar;
 
1
  import React from 'react';
2
  import { UndoIcon, RedoIcon, UploadIcon, DownloadIcon, ClearIcon, BrushIcon, EraserIcon, MagicSparkleIcon, SettingsIcon } from './icons';
3
+ import { TFunction } from '../types';
4
 
5
  interface ToolbarProps {
6
  penColor: string;
 
19
  onMagicUpload: () => void;
20
  isMagicUploading: boolean;
21
  canShare: boolean;
22
+ onToggleSettings: () => void;
23
+ t: TFunction;
24
  }
25
 
26
  const Toolbar: React.FC<ToolbarProps> = ({
 
40
  onMagicUpload,
41
  isMagicUploading,
42
  canShare,
43
+ onToggleSettings,
44
+ t,
45
  }) => {
46
  const fileInputRef = React.useRef<HTMLInputElement>(null);
47
 
 
56
  <button
57
  onClick={onUndo}
58
  disabled={!canUndo}
59
+ title={t('undo')}
60
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
61
+ aria-label={t('undoAria')}
62
  >
63
  <UndoIcon className="w-5 h-5 text-gray-700" />
64
  </button>
 
66
  <button
67
  onClick={onRedo}
68
  disabled={!canRedo}
69
+ title={t('redo')}
70
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
71
+ aria-label={t('redoAria')}
72
  >
73
  <RedoIcon className="w-5 h-5 text-gray-700" />
74
  </button>
 
79
  onChange={onLoadImage}
80
  accept="image/png, image/jpeg"
81
  className="hidden"
82
+ aria-label={t('loadImageAria')}
83
  />
84
  <button
85
  onClick={handleUploadClick}
86
+ title={t('loadImage')}
87
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
88
+ aria-label={t('loadImageAria')}
89
  >
90
  <UploadIcon className="w-5 h-5 text-gray-700" />
91
  </button>
92
 
93
  <button
94
  onClick={onExportImage}
95
+ title={t('exportImage')}
96
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
97
+ aria-label={t('exportImageAria')}
98
  >
99
  <DownloadIcon className="w-5 h-5 text-gray-700" />
100
  </button>
101
 
102
  <button
103
  onClick={onMagicUpload}
104
+ title={t('shareAndEditWithAI')}
105
  disabled={isMagicUploading || !canShare}
106
  className="p-2 rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors border-gray-300"
107
+ aria-label={t('shareAndEditWithAIAria')}
108
  >
109
  <MagicSparkleIcon className="w-5 h-5 text-purple-600" />
110
  </button>
111
 
112
  <button
113
  onClick={onClearCanvas}
114
+ title={t('clearCanvas')}
115
  className="p-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors tool-button"
116
+ aria-label={t('clearCanvasAria')}
117
  >
118
  <ClearIcon className="w-5 h-5" />
119
  </button>
 
123
  <div className="flex flex-wrap items-center justify-start gap-x-3 gap-y-2 mt-2">
124
  <button
125
  onClick={onToggleEraser}
126
+ title={isEraserMode ? t('switchToBrush') : t('switchToEraser')}
127
  className={`p-2 rounded-md transition-colors ${isEraserMode ? 'bg-blue-500 text-white hover:bg-blue-600' : ' hover:bg-gray-100 text-gray-700 border-gray-300'}`}
128
+ aria-label={isEraserMode ? t('switchToBrushAria') : t('switchToEraserAria')}
129
  aria-pressed={isEraserMode}
130
  >
131
  {isEraserMode ? <BrushIcon className="w-5 h-5" /> : <EraserIcon className="w-5 h-5" />}
132
  </button>
133
 
134
  <div className="flex items-center gap-2 p-1 rounded-md hover:bg-slate-300 transition-colors">
135
+ <label htmlFor="penColor" className="text-sm font-medium text-gray-700 sr-only">{t('penColor')}:</label>
136
  <input
137
  type="color"
138
  id="penColor"
139
+ title={t('penColor')}
140
  value={penColor}
141
  onChange={(e) => onColorChange(e.target.value)}
142
  className={`w-8 h-8 rounded-full cursor-pointer border-2 ${isEraserMode ? 'border-gray-400 opacity-50' : 'border-white shadow-sm'}`}
143
  disabled={isEraserMode}
144
+ aria-label={t('selectPenColorAria')}
145
  />
146
  </div>
147
 
148
  <div className="flex items-center gap-2 p-1 rounded-md hover:bg-slate-300 transition-colors">
149
+ <label htmlFor="penSize" className="text-sm font-medium text-gray-700 sr-only">{t('penSize', { size: penSize })}:</label>
150
  <input
151
  type="range"
152
  id="penSize"
153
+ title={t('penSize', { size: penSize })}
154
  min="1"
155
  max="50"
156
  value={penSize}
157
  onChange={(e) => onSizeChange(parseInt(e.target.value, 10))}
158
  className="w-24 md:w-32 cursor-pointer accent-blue-600"
159
+ aria-label={t('penSizeAria', { size: penSize })}
160
  aria-valuemin={1}
161
  aria-valuemax={50}
162
  aria-valuenow={penSize}
 
166
 
167
  <button
168
  onClick={onToggleSettings}
169
+ title={t('openSettings')}
170
  className="p-2 rounded-md hover:bg-gray-100 transition-colors border-gray-300"
171
+ aria-label={t('openSettings')}
172
  >
173
  <SettingsIcon className="w-5 h-5 text-gray-700" />
174
  </button>
 
177
  );
178
  };
179
 
180
+ export default Toolbar;
components/ZoomSlider.tsx CHANGED
@@ -1,12 +1,15 @@
1
 
 
2
  import React from 'react';
 
3
 
4
  interface ZoomSliderProps {
5
  zoomLevel: number;
6
  onZoomChange: (newZoomLevel: number) => void;
 
7
  }
8
 
9
- const ZoomSlider: React.FC<ZoomSliderProps> = ({ zoomLevel, onZoomChange }) => {
10
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
11
  onZoomChange(parseFloat(event.target.value));
12
  };
@@ -20,7 +23,7 @@ const ZoomSlider: React.FC<ZoomSliderProps> = ({ zoomLevel, onZoomChange }) => {
20
  aria-label="Zoom controls"
21
  >
22
  <label htmlFor="zoom-slider" className="text-xs font-medium whitespace-nowrap">
23
- Zoom: <span className="font-bold">{zoomPercentage}%</span>
24
  </label>
25
  <input
26
  type="range"
@@ -34,7 +37,7 @@ const ZoomSlider: React.FC<ZoomSliderProps> = ({ zoomLevel, onZoomChange }) => {
34
  aria-valuemin={20}
35
  aria-valuemax={300}
36
  aria-valuenow={zoomPercentage}
37
- aria-label={`Current zoom level ${zoomPercentage} percent`}
38
  />
39
  </div>
40
  );
 
1
 
2
+
3
  import React from 'react';
4
+ import { TFunction } from '../types';
5
 
6
  interface ZoomSliderProps {
7
  zoomLevel: number;
8
  onZoomChange: (newZoomLevel: number) => void;
9
+ t: TFunction;
10
  }
11
 
12
+ const ZoomSlider: React.FC<ZoomSliderProps> = ({ zoomLevel, onZoomChange, t }) => {
13
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
14
  onZoomChange(parseFloat(event.target.value));
15
  };
 
23
  aria-label="Zoom controls"
24
  >
25
  <label htmlFor="zoom-slider" className="text-xs font-medium whitespace-nowrap">
26
+ {t('zoom', { percentage: zoomPercentage })}
27
  </label>
28
  <input
29
  type="range"
 
37
  aria-valuemin={20}
38
  aria-valuemax={300}
39
  aria-valuenow={zoomPercentage}
40
+ aria-label={t('zoomAria', { percentage: zoomPercentage })}
41
  />
42
  </div>
43
  );
hooks/useAiFeatures.ts CHANGED
@@ -2,6 +2,7 @@ import { useState, useCallback } from 'react';
2
  import { ToastMessage } from './useToasts';
3
  import { dataURLtoBlob, calculateSHA256, copyToClipboard } from '../utils/canvasUtils';
4
  import { CanvasHistoryHook } from './useCanvasHistory';
 
5
 
6
  const SHARE_API_URL = 'https://sharefile.suisuy.eu.org';
7
  const ASK_API_URL = 'https://getai.deno.dev/';
@@ -37,6 +38,7 @@ interface UseAiFeaturesProps {
37
  aiDimensionsMode: AiDimensionsMode;
38
  currentCanvasWidth: number;
39
  currentCanvasHeight: number;
 
40
  }
41
 
42
  export const useAiFeatures = ({
@@ -48,7 +50,8 @@ export const useAiFeatures = ({
48
  aiApiEndpoint,
49
  aiDimensionsMode,
50
  currentCanvasWidth,
51
- currentCanvasHeight,
 
52
  }: UseAiFeaturesProps): AiFeaturesHook => {
53
  const [isMagicUploading, setIsMagicUploading] = useState<boolean>(false);
54
  const [showAiEditModal, setShowAiEditModal] = useState<boolean>(false);
@@ -107,37 +110,37 @@ export const useAiFeatures = ({
107
  updateCanvasState(newCanvasState, currentCanvasWidth, currentCanvasHeight);
108
 
109
  setShowAiEditModal(false);
110
- showToast('AI image applied to canvas!', 'success');
111
  setAiPrompt('');
112
  setSharedImageUrlForAi(null);
113
  } else {
114
- setAiEditError('Failed to create drawing context for AI image.');
115
  }
116
  setIsGeneratingAiImage(false);
117
  };
118
  img.onerror = () => {
119
- setAiEditError('Failed to load the generated AI image. It might be an invalid image format.');
120
  setIsGeneratingAiImage(false);
121
  };
122
  img.crossOrigin = "anonymous";
123
  img.src = aiImageDataUrl;
124
- }, [showToast, updateCanvasState, currentCanvasWidth, currentCanvasHeight]);
125
 
126
  const handleMagicUpload = useCallback(async () => {
127
  if (isMagicUploading || !currentDataURL) {
128
- showToast('No canvas content to share.', 'info');
129
  return;
130
  }
131
 
132
  setIsMagicUploading(true);
133
  setAiEditError(null);
134
- showToast('Uploading image...', 'info');
135
 
136
  try {
137
  const blob = await dataURLtoBlob(currentDataURL);
138
 
139
  if (blob.size > MAX_UPLOAD_SIZE_BYTES) {
140
- showToast(`Image too large (${(blob.size / 1024 / 1024).toFixed(2)}MB). Max 50MB.`, 'error');
141
  setIsMagicUploading(false);
142
  return;
143
  }
@@ -153,8 +156,8 @@ export const useAiFeatures = ({
153
  });
154
 
155
  if (!response.ok) {
156
- let errorMsg = `Upload failed: ${response.statusText}`;
157
- try { const errorBody = await response.text(); errorMsg = `Upload failed: ${errorBody || response.statusText}`; } catch (e) { /* ignore */ }
158
  throw new Error(errorMsg);
159
  }
160
 
@@ -169,21 +172,21 @@ export const useAiFeatures = ({
169
 
170
  } catch (error: any) {
171
  console.error('Magic upload error:', error);
172
- showToast(error.message || 'Magic upload failed. Check console.', 'error');
173
  } finally {
174
  setIsMagicUploading(false);
175
  }
176
- }, [isMagicUploading, currentDataURL, showToast, clearAskUrl]);
177
 
178
  const handleGenerateAiImage = useCallback(async () => {
179
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
180
- setAiEditError('Please enter a prompt and ensure an image was uploaded.');
181
  return;
182
  }
183
  setIsGeneratingAiImage(true);
184
  setAiEditError(null);
185
  clearAskUrl();
186
- showToast('Generating AI image...', 'info');
187
 
188
  const encodedPrompt = encodeURIComponent(aiPrompt);
189
  const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi);
@@ -209,7 +212,7 @@ export const useAiFeatures = ({
209
  try {
210
  const response = await fetch(finalApiUrl);
211
  if (!response.ok) {
212
- let errorMsg = `AI image generation failed: ${response.status} ${response.statusText}`;
213
  try {
214
  const errorBody = await response.text();
215
  if (errorBody && !errorBody.toLowerCase().includes('<html')) {
@@ -221,7 +224,7 @@ export const useAiFeatures = ({
221
 
222
  const imageBlob = await response.blob();
223
  if (!imageBlob.type.startsWith('image/')) {
224
- throw new Error('AI service did not return a valid image. Please try a different prompt or check the API endpoint.');
225
  }
226
 
227
  const reader = new FileReader();
@@ -229,26 +232,26 @@ export const useAiFeatures = ({
229
  if (typeof reader.result === 'string') {
230
  loadAiImageOntoCanvas(reader.result);
231
  } else {
232
- setAiEditError('Failed to read AI image data as string.');
233
  setIsGeneratingAiImage(false);
234
  }
235
  };
236
  reader.onerror = () => {
237
- setAiEditError('Failed to read AI image data.');
238
  setIsGeneratingAiImage(false);
239
  }
240
  reader.readAsDataURL(imageBlob);
241
 
242
  } catch (error: any) {
243
  console.error('AI image generation error:', error);
244
- setAiEditError(error.message || 'An unknown error occurred during AI image generation.');
245
  setIsGeneratingAiImage(false);
246
  }
247
- }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, clearAskUrl]);
248
 
249
  const handleAskAi = useCallback(async () => {
250
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
251
- setAiEditError('Please enter a question about the image.');
252
  return;
253
  }
254
  setIsAskingAi(true);
@@ -263,19 +266,19 @@ export const useAiFeatures = ({
263
 
264
  } catch (error: any) {
265
  console.error('AI ask error:', error);
266
- setAiEditError(error.message || 'An unknown error occurred while asking AI.');
267
  } finally {
268
  setIsAskingAi(false);
269
  }
270
- }, [aiPrompt, sharedImageUrlForAi]);
271
 
272
 
273
  const handleCancelAiEdit = () => {
274
  setShowAiEditModal(false);
275
  if (sharedImageUrlForAi) {
276
- copyToClipboard(sharedImageUrlForAi, (msg, type) => showToast(msg,type as 'info' | 'error')).then(copied => {
277
  if(copied) {
278
- showToast(`Image uploaded! URL: ${sharedImageUrlForAi} (Copied!)`, 'success');
279
  }
280
  });
281
  }
 
2
  import { ToastMessage } from './useToasts';
3
  import { dataURLtoBlob, calculateSHA256, copyToClipboard } from '../utils/canvasUtils';
4
  import { CanvasHistoryHook } from './useCanvasHistory';
5
+ import { TFunction } from '../types';
6
 
7
  const SHARE_API_URL = 'https://sharefile.suisuy.eu.org';
8
  const ASK_API_URL = 'https://getai.deno.dev/';
 
38
  aiDimensionsMode: AiDimensionsMode;
39
  currentCanvasWidth: number;
40
  currentCanvasHeight: number;
41
+ t: TFunction;
42
  }
43
 
44
  export const useAiFeatures = ({
 
50
  aiApiEndpoint,
51
  aiDimensionsMode,
52
  currentCanvasWidth,
53
+ currentCanvasHeight,
54
+ t,
55
  }: UseAiFeaturesProps): AiFeaturesHook => {
56
  const [isMagicUploading, setIsMagicUploading] = useState<boolean>(false);
57
  const [showAiEditModal, setShowAiEditModal] = useState<boolean>(false);
 
110
  updateCanvasState(newCanvasState, currentCanvasWidth, currentCanvasHeight);
111
 
112
  setShowAiEditModal(false);
113
+ showToast(t('aiImageApplied'), 'success');
114
  setAiPrompt('');
115
  setSharedImageUrlForAi(null);
116
  } else {
117
+ setAiEditError(t('failedToCreateContext'));
118
  }
119
  setIsGeneratingAiImage(false);
120
  };
121
  img.onerror = () => {
122
+ setAiEditError(t('failedToLoadAiImage'));
123
  setIsGeneratingAiImage(false);
124
  };
125
  img.crossOrigin = "anonymous";
126
  img.src = aiImageDataUrl;
127
+ }, [showToast, updateCanvasState, currentCanvasWidth, currentCanvasHeight, t]);
128
 
129
  const handleMagicUpload = useCallback(async () => {
130
  if (isMagicUploading || !currentDataURL) {
131
+ showToast(t('noCanvasContentToShare'), 'info');
132
  return;
133
  }
134
 
135
  setIsMagicUploading(true);
136
  setAiEditError(null);
137
+ showToast(t('uploadingImage'), 'info');
138
 
139
  try {
140
  const blob = await dataURLtoBlob(currentDataURL);
141
 
142
  if (blob.size > MAX_UPLOAD_SIZE_BYTES) {
143
+ showToast(t('imageTooLarge', { size: (blob.size / 1024 / 1024).toFixed(2) }), 'error');
144
  setIsMagicUploading(false);
145
  return;
146
  }
 
156
  });
157
 
158
  if (!response.ok) {
159
+ let errorMsg = t('uploadFailed', { statusText: response.statusText });
160
+ try { const errorBody = await response.text(); errorMsg = t('uploadFailed', { statusText: errorBody || response.statusText }); } catch (e) { /* ignore */ }
161
  throw new Error(errorMsg);
162
  }
163
 
 
172
 
173
  } catch (error: any) {
174
  console.error('Magic upload error:', error);
175
+ showToast(error.message || t('magicUploadError'), 'error');
176
  } finally {
177
  setIsMagicUploading(false);
178
  }
179
+ }, [isMagicUploading, currentDataURL, showToast, clearAskUrl, t]);
180
 
181
  const handleGenerateAiImage = useCallback(async () => {
182
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
183
+ setAiEditError(t('enterPrompt'));
184
  return;
185
  }
186
  setIsGeneratingAiImage(true);
187
  setAiEditError(null);
188
  clearAskUrl();
189
+ showToast(t('generatingAiImage'), 'info');
190
 
191
  const encodedPrompt = encodeURIComponent(aiPrompt);
192
  const encodedImageUrl = encodeURIComponent(sharedImageUrlForAi);
 
212
  try {
213
  const response = await fetch(finalApiUrl);
214
  if (!response.ok) {
215
+ let errorMsg = t('aiGenFailed', { status: response.status, statusText: response.statusText });
216
  try {
217
  const errorBody = await response.text();
218
  if (errorBody && !errorBody.toLowerCase().includes('<html')) {
 
224
 
225
  const imageBlob = await response.blob();
226
  if (!imageBlob.type.startsWith('image/')) {
227
+ throw new Error(t('aiGenInvalidImage'));
228
  }
229
 
230
  const reader = new FileReader();
 
232
  if (typeof reader.result === 'string') {
233
  loadAiImageOntoCanvas(reader.result);
234
  } else {
235
+ setAiEditError(t('failedToReadAiData'));
236
  setIsGeneratingAiImage(false);
237
  }
238
  };
239
  reader.onerror = () => {
240
+ setAiEditError(t('failedToReadAiData'));
241
  setIsGeneratingAiImage(false);
242
  }
243
  reader.readAsDataURL(imageBlob);
244
 
245
  } catch (error: any) {
246
  console.error('AI image generation error:', error);
247
+ setAiEditError(error.message || t('unknownAiError'));
248
  setIsGeneratingAiImage(false);
249
  }
250
+ }, [aiPrompt, sharedImageUrlForAi, showToast, loadAiImageOntoCanvas, aiImageQuality, aiApiEndpoint, aiDimensionsMode, currentCanvasWidth, currentCanvasHeight, clearAskUrl, t]);
251
 
252
  const handleAskAi = useCallback(async () => {
253
  if (!aiPrompt.trim() || !sharedImageUrlForAi) {
254
+ setAiEditError(t('enterQuestion'));
255
  return;
256
  }
257
  setIsAskingAi(true);
 
266
 
267
  } catch (error: any) {
268
  console.error('AI ask error:', error);
269
+ setAiEditError(error.message || t('unknownAskError'));
270
  } finally {
271
  setIsAskingAi(false);
272
  }
273
+ }, [aiPrompt, sharedImageUrlForAi, t]);
274
 
275
 
276
  const handleCancelAiEdit = () => {
277
  setShowAiEditModal(false);
278
  if (sharedImageUrlForAi) {
279
+ copyToClipboard(sharedImageUrlForAi, (msg, type) => showToast(msg,type as 'info' | 'error'), t).then(copied => {
280
  if(copied) {
281
+ showToast(t('imageUploadedAndCopied', { url: sharedImageUrlForAi }), 'success');
282
  }
283
  });
284
  }
hooks/useCanvasFileUtils.ts CHANGED
@@ -3,6 +3,7 @@ import { getBlankCanvasDataURL } from '../utils/canvasUtils';
3
  import { CanvasHistoryHook } from './useCanvasHistory';
4
  import { ConfirmModalHook } from './useConfirmModal';
5
  import { ToastMessage } from './useToasts';
 
6
 
7
 
8
  interface UseCanvasFileUtilsProps {
@@ -21,6 +22,7 @@ interface UseCanvasFileUtilsProps {
21
  confirmModalActions: {
22
  requestConfirmation: ConfirmModalHook['requestConfirmation'];
23
  };
 
24
  }
25
 
26
  export interface CanvasFileUtilsHook {
@@ -35,6 +37,7 @@ export const useCanvasFileUtils = ({
35
  historyActions,
36
  uiActions,
37
  confirmModalActions,
 
38
  }: UseCanvasFileUtilsProps): CanvasFileUtilsHook => {
39
  const { currentDataURL, canvasWidth, canvasHeight } = canvasState;
40
  const { updateCanvasState } = historyActions;
@@ -81,18 +84,18 @@ export const useCanvasFileUtils = ({
81
  const compositeDataURL = tempCanvas.toDataURL('image/png');
82
 
83
  updateCanvasState(compositeDataURL, finalCanvasWidth, finalCanvasHeight);
84
- showToast('Image loaded successfully.', 'success');
85
  }
86
  };
87
- img.onerror = () => showToast("Error loading image file for processing.", 'error');
88
  img.src = imageDataUrl;
89
  }
90
  };
91
- reader.onerror = () => showToast("Error reading image file.", 'error');
92
  reader.readAsDataURL(file);
93
  if(event.target) event.target.value = '';
94
  }
95
- }, [canvasWidth, canvasHeight, updateCanvasState, showToast]);
96
 
97
  const handleExportImage = useCallback(() => {
98
  if (currentDataURL) {
@@ -102,34 +105,34 @@ export const useCanvasFileUtils = ({
102
  document.body.appendChild(link);
103
  link.click();
104
  document.body.removeChild(link);
105
- showToast('Image exported!', 'success');
106
  } else {
107
- showToast('No image to export.', 'info');
108
  }
109
- }, [currentDataURL, showToast]);
110
 
111
  const handleClearCanvas = useCallback(() => {
112
  const blankCanvas = getBlankCanvasDataURL(canvasWidth, canvasHeight);
113
  updateCanvasState(blankCanvas, canvasWidth, canvasHeight);
114
- showToast("Canvas cleared.", "info");
115
- }, [canvasWidth, canvasHeight, updateCanvasState, showToast]);
116
 
117
  const handleCanvasSizeChange = useCallback((newWidth: number, newHeight: number) => {
118
  if (newWidth === canvasWidth && newHeight === canvasHeight) {
119
  return;
120
  }
121
  requestConfirmation(
122
- "Confirm Canvas Size Change",
123
- "Changing canvas size will clear the current drawing and history. Are you sure you want to continue?",
124
  () => {
125
  const blankCanvas = getBlankCanvasDataURL(newWidth, newHeight);
126
  updateCanvasState(blankCanvas, newWidth, newHeight);
127
  setZoomLevel(0.5);
128
- showToast(`Canvas resized to ${newWidth}x${newHeight}px. Drawing cleared.`, 'info');
129
  },
130
  { isDestructive: true }
131
  );
132
- }, [canvasWidth, canvasHeight, requestConfirmation, updateCanvasState, setZoomLevel, showToast]);
133
 
134
  return { handleLoadImageFile, handleExportImage, handleClearCanvas, handleCanvasSizeChange };
135
  };
 
3
  import { CanvasHistoryHook } from './useCanvasHistory';
4
  import { ConfirmModalHook } from './useConfirmModal';
5
  import { ToastMessage } from './useToasts';
6
+ import { TFunction } from '../types';
7
 
8
 
9
  interface UseCanvasFileUtilsProps {
 
22
  confirmModalActions: {
23
  requestConfirmation: ConfirmModalHook['requestConfirmation'];
24
  };
25
+ t: TFunction;
26
  }
27
 
28
  export interface CanvasFileUtilsHook {
 
37
  historyActions,
38
  uiActions,
39
  confirmModalActions,
40
+ t,
41
  }: UseCanvasFileUtilsProps): CanvasFileUtilsHook => {
42
  const { currentDataURL, canvasWidth, canvasHeight } = canvasState;
43
  const { updateCanvasState } = historyActions;
 
84
  const compositeDataURL = tempCanvas.toDataURL('image/png');
85
 
86
  updateCanvasState(compositeDataURL, finalCanvasWidth, finalCanvasHeight);
87
+ showToast(t('imageLoadedSuccess'), 'success');
88
  }
89
  };
90
+ img.onerror = () => showToast(t('imageLoadError'), 'error');
91
  img.src = imageDataUrl;
92
  }
93
  };
94
+ reader.onerror = () => showToast(t('imageReadError'), 'error');
95
  reader.readAsDataURL(file);
96
  if(event.target) event.target.value = '';
97
  }
98
+ }, [canvasWidth, canvasHeight, updateCanvasState, showToast, t]);
99
 
100
  const handleExportImage = useCallback(() => {
101
  if (currentDataURL) {
 
105
  document.body.appendChild(link);
106
  link.click();
107
  document.body.removeChild(link);
108
+ showToast(t('imageExported'), 'success');
109
  } else {
110
+ showToast(t('noImageToExport'), 'info');
111
  }
112
+ }, [currentDataURL, showToast, t]);
113
 
114
  const handleClearCanvas = useCallback(() => {
115
  const blankCanvas = getBlankCanvasDataURL(canvasWidth, canvasHeight);
116
  updateCanvasState(blankCanvas, canvasWidth, canvasHeight);
117
+ showToast(t("canvasCleared"), "info");
118
+ }, [canvasWidth, canvasHeight, updateCanvasState, showToast, t]);
119
 
120
  const handleCanvasSizeChange = useCallback((newWidth: number, newHeight: number) => {
121
  if (newWidth === canvasWidth && newHeight === canvasHeight) {
122
  return;
123
  }
124
  requestConfirmation(
125
+ t('confirmCanvasSizeChangeTitle'),
126
+ t('confirmCanvasSizeChangeMessage'),
127
  () => {
128
  const blankCanvas = getBlankCanvasDataURL(newWidth, newHeight);
129
  updateCanvasState(blankCanvas, newWidth, newHeight);
130
  setZoomLevel(0.5);
131
+ showToast(t('canvasResized', { width: newWidth, height: newHeight }), 'info');
132
  },
133
  { isDestructive: true }
134
  );
135
+ }, [canvasWidth, canvasHeight, requestConfirmation, updateCanvasState, setZoomLevel, showToast, t]);
136
 
137
  return { handleLoadImageFile, handleExportImage, handleClearCanvas, handleCanvasSizeChange };
138
  };
hooks/useConfirmModal.ts CHANGED
@@ -1,4 +1,5 @@
1
  import React, { useState, useCallback } from 'react';
 
2
 
3
  export interface ConfirmModalInfo {
4
  isOpen: boolean;
@@ -35,7 +36,7 @@ export interface ConfirmModalHook {
35
  closeConfirmModal: () => void;
36
  }
37
 
38
- export const useConfirmModal = (): ConfirmModalHook => {
39
  const [confirmModalInfo, setConfirmModalInfo] = useState<ConfirmModalInfo>(initialConfirmModalState);
40
 
41
  const requestConfirmation = useCallback(
@@ -58,11 +59,11 @@ export const useConfirmModal = (): ConfirmModalHook => {
58
  setConfirmModalInfo(prev => ({ ...prev, isOpen: false })); // Close modal after action
59
  },
60
  isDestructive: options.isDestructive ?? false,
61
- confirmText: options.confirmText ?? 'Confirm',
62
- cancelText: options.cancelText ?? 'Cancel',
63
  });
64
  },
65
- []
66
  );
67
 
68
  const closeConfirmModal = useCallback(() => {
 
1
  import React, { useState, useCallback } from 'react';
2
+ import { TFunction } from '../types';
3
 
4
  export interface ConfirmModalInfo {
5
  isOpen: boolean;
 
36
  closeConfirmModal: () => void;
37
  }
38
 
39
+ export const useConfirmModal = (t: TFunction): ConfirmModalHook => {
40
  const [confirmModalInfo, setConfirmModalInfo] = useState<ConfirmModalInfo>(initialConfirmModalState);
41
 
42
  const requestConfirmation = useCallback(
 
59
  setConfirmModalInfo(prev => ({ ...prev, isOpen: false })); // Close modal after action
60
  },
61
  isDestructive: options.isDestructive ?? false,
62
+ confirmText: options.confirmText ?? t('confirm'),
63
+ cancelText: options.cancelText ?? t('cancel'),
64
  });
65
  },
66
+ [t]
67
  );
68
 
69
  const closeConfirmModal = useCallback(() => {
hooks/useFullscreen.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { useState, useEffect, useCallback } from 'react';
 
2
 
3
  export interface FullscreenHook {
4
  isFullscreenActive: boolean;
@@ -7,7 +8,8 @@ export interface FullscreenHook {
7
  }
8
 
9
  export const useFullscreen = (
10
- showToast?: (message: string, type: 'info' | 'error') => void
 
11
  ): FullscreenHook => {
12
  const [isFullscreenActive, setIsFullscreenActive] = useState<boolean>(false);
13
  const [requestFullscreenSupportError, setRequestFullscreenSupportError] = useState<string | undefined>();
@@ -50,7 +52,7 @@ export const useFullscreen = (
50
  } else if (element.msRequestFullscreen) { // IE/Edge
51
  await element.msRequestFullscreen();
52
  } else {
53
- const errorMsg = "Fullscreen API is not supported by this browser.";
54
  setRequestFullscreenSupportError(errorMsg);
55
  if (showToast) showToast(errorMsg, "error");
56
  return;
@@ -65,7 +67,7 @@ export const useFullscreen = (
65
  } else if ((document as any).msExitFullscreen) {
66
  await (document as any).msExitFullscreen();
67
  } else {
68
- const errorMsg = "Could not exit fullscreen. API not found.";
69
  setRequestFullscreenSupportError(errorMsg);
70
  if (showToast) showToast(errorMsg, "error");
71
  return;
@@ -74,7 +76,7 @@ export const useFullscreen = (
74
  setRequestFullscreenSupportError(undefined);
75
  } catch (err: any) {
76
  console.error("Fullscreen API error:", err);
77
- const errorMsg = `Fullscreen mode change failed: ${err.message || 'Unknown error'}`;
78
  setRequestFullscreenSupportError(errorMsg);
79
  if (showToast) showToast(errorMsg, "error");
80
  handleFullscreenChange(); // Ensure state reflects reality
 
1
  import { useState, useEffect, useCallback } from 'react';
2
+ import { TFunction } from '../types';
3
 
4
  export interface FullscreenHook {
5
  isFullscreenActive: boolean;
 
8
  }
9
 
10
  export const useFullscreen = (
11
+ showToast?: (message: string, type: 'info' | 'error') => void,
12
+ t?: TFunction,
13
  ): FullscreenHook => {
14
  const [isFullscreenActive, setIsFullscreenActive] = useState<boolean>(false);
15
  const [requestFullscreenSupportError, setRequestFullscreenSupportError] = useState<string | undefined>();
 
52
  } else if (element.msRequestFullscreen) { // IE/Edge
53
  await element.msRequestFullscreen();
54
  } else {
55
+ const errorMsg = t ? t('fullscreenNotSupported') : "Fullscreen API is not supported by this browser.";
56
  setRequestFullscreenSupportError(errorMsg);
57
  if (showToast) showToast(errorMsg, "error");
58
  return;
 
67
  } else if ((document as any).msExitFullscreen) {
68
  await (document as any).msExitFullscreen();
69
  } else {
70
+ const errorMsg = t ? t('fullscreenExitError') : "Could not exit fullscreen. API not found.";
71
  setRequestFullscreenSupportError(errorMsg);
72
  if (showToast) showToast(errorMsg, "error");
73
  return;
 
76
  setRequestFullscreenSupportError(undefined);
77
  } catch (err: any) {
78
  console.error("Fullscreen API error:", err);
79
+ const errorMsg = t ? t('fullscreenChangeError', { message: err.message || 'Unknown error' }) : `Fullscreen mode change failed: ${err.message || 'Unknown error'}`;
80
  setRequestFullscreenSupportError(errorMsg);
81
  if (showToast) showToast(errorMsg, "error");
82
  handleFullscreenChange(); // Ensure state reflects reality
metadata.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "xpaintai",
3
  "description": "A simple web-based paint application with features like pen customization, undo/redo, image loading/exporting, and autosave to IndexedDB.",
4
  "requestFramePermissions": [],
5
  "prompt": ""
 
1
  {
2
+ "name": "xpaintai_zh",
3
  "description": "A simple web-based paint application with features like pen customization, undo/redo, image loading/exporting, and autosave to IndexedDB.",
4
  "requestFramePermissions": [],
5
  "prompt": ""
package.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "name": "xpaintai",
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",
 
1
  {
2
+ "name": "xpaintai_zh",
3
  "private": true,
4
  "version": "0.0.0",
5
  "type": "module",
translations.ts ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ const translations = {
3
+ en: {
4
+ // Toolbar
5
+ undo: 'Undo',
6
+ redo: 'Redo',
7
+ loadImage: 'Load Image',
8
+ exportImage: 'Export Image',
9
+ shareAndEditWithAI: 'Share Canvas & Edit with AI',
10
+ clearCanvas: 'Clear Canvas',
11
+ switchToBrush: 'Switch to Brush',
12
+ switchToEraser: 'Switch to Eraser',
13
+ penColor: 'Pen Color',
14
+ penSize: 'Pen Size: {size}',
15
+ penSizeAria: 'Pen size {size} pixels',
16
+ openSettings: 'Open Settings',
17
+ undoAria: 'Undo last action',
18
+ redoAria: 'Redo last undone action',
19
+ loadImageAria: 'Load image from file',
20
+ exportImageAria: 'Export canvas as image',
21
+ shareAndEditWithAIAria: 'Share canvas by uploading and open AI edit options',
22
+ clearCanvasAria: 'Clear canvas',
23
+ switchToBrushAria: 'Switch to Brush mode',
24
+ switchToEraserAria: 'Switch to Eraser mode',
25
+ selectPenColorAria: 'Select pen color',
26
+
27
+ // AI Modal
28
+ aiModalTitle: 'Edit or Ask about Image with AI',
29
+ closeAiModalAria: 'Close AI edit modal',
30
+ aiPromptLabel: 'Describe what you want to change, or ask a question:',
31
+ aiPromptPlaceholder: "e.g., 'make the cat wear a party hat' or 'what is in this image?'",
32
+ errorPrefix: 'Error:',
33
+ aiAsk: 'Ask',
34
+ aiAsking: 'Asking...',
35
+ aiGenerateImage: 'Generate Image',
36
+ aiGenerating: 'Generating...',
37
+
38
+ // Settings Panel
39
+ settingsTitle: 'Application Settings',
40
+ closeSettingsAria: 'Close settings panel',
41
+ interface: 'Interface',
42
+ showZoomSlider: 'Show Zoom Slider',
43
+ toggleZoomSliderAria: 'Toggle Zoom Slider Visibility',
44
+ enterFullscreen: 'Enter Fullscreen',
45
+ exitFullscreen: 'Exit Fullscreen',
46
+ fullscreenAria: 'Enter Fullscreen',
47
+ exitFullscreenAria: 'Exit Fullscreen',
48
+ canvasDimensions: 'Canvas Dimensions',
49
+ canvasSize: 'Canvas Size',
50
+ canvasSizeDescription: 'Changing size will clear the current canvas and history.',
51
+ aiImageGeneration: 'AI Image Generation',
52
+ apiEndpoint: 'API Endpoint',
53
+ apiEndpointDescription: 'Select an API for image generation. Placeholders are filled automatically.',
54
+ pollinationsApi: 'Pollinations AI',
55
+ geminiSimpleGetApi: 'Gemini Simple Get',
56
+ aiImageDimensions: 'AI Image Dimensions',
57
+ aiImageDimensionsDescription: "Controls `width` & `height` params for APIs that support them (e.g., Pollinations AI).",
58
+ apiDefault: 'API Default (no size)',
59
+ matchCanvasSize: 'Match Canvas Size',
60
+ fixed1024: 'Fixed 1024x1024',
61
+ imageQuality: 'Image Quality',
62
+ imageQualityDescription: "Used for APIs that support a 'quality' parameter (e.g., Pollinations AI).",
63
+ qualityLow: 'Low',
64
+ qualityMedium: 'Medium',
65
+ qualityHigh: 'High (HD)',
66
+ done: 'Done',
67
+
68
+ // Confirm Modal
69
+ confirm: 'Confirm',
70
+ cancel: 'Cancel',
71
+ confirmCanvasSizeChangeTitle: 'Confirm Canvas Size Change',
72
+ confirmCanvasSizeChangeMessage: 'Changing canvas size will clear the current drawing and history. Are you sure you want to continue?',
73
+
74
+ // Zoom Slider
75
+ zoom: 'Zoom: {percentage}%',
76
+ zoomAria: 'Current zoom level {percentage} percent',
77
+
78
+ // Canvas
79
+ drawingCanvas: 'Drawing canvas',
80
+ initializingCanvas: 'Initializing Canvas...',
81
+
82
+ // App Loading
83
+ loadingCanvasMessage: 'Loading Your Canvas...',
84
+ loadingCanvasSubMessage: 'Getting things ready!',
85
+
86
+ // Toasts
87
+ imageLoadedSuccess: 'Image loaded successfully.',
88
+ imageLoadError: 'Error loading image file for processing.',
89
+ imageReadError: 'Error reading image file.',
90
+ imageExported: 'Image exported!',
91
+ noImageToExport: 'No image to export.',
92
+ canvasCleared: 'Canvas cleared.',
93
+ canvasResized: 'Canvas resized to {width}x{height}px. Drawing cleared.',
94
+ noCanvasContentToShare: 'No canvas content to share.',
95
+ uploadingImage: 'Uploading image...',
96
+ imageTooLarge: 'Image too large ({size}MB). Max 50MB.',
97
+ uploadFailed: 'Upload failed: {statusText}',
98
+ magicUploadError: 'Magic upload failed. Check console.',
99
+ imageUploadedAndCopied: 'Image uploaded! URL: {url} (Copied!)',
100
+ aiImageApplied: 'AI image applied to canvas!',
101
+ failedToCreateContext: 'Failed to create drawing context for AI image.',
102
+ failedToLoadAiImage: 'Failed to load the generated AI image. It might be an invalid image format.',
103
+ generatingAiImage: 'Generating AI image...',
104
+ aiGenFailed: 'AI image generation failed: {status} {statusText}',
105
+ aiGenInvalidImage: 'AI service did not return a valid image. Please try a different prompt or check the API endpoint.',
106
+ failedToReadAiData: 'Failed to read AI image data.',
107
+ unknownAiError: 'An unknown error occurred during AI image generation.',
108
+ enterPrompt: 'Please enter a prompt and ensure an image was uploaded.',
109
+ enterQuestion: 'Please enter a question about the image.',
110
+ unknownAskError: 'An unknown error occurred while asking AI.',
111
+ clipboardApiNotAvailable: 'Clipboard API not available.',
112
+ failedToCopyUrl: 'Failed to copy URL to clipboard.',
113
+ fullscreenNotSupported: 'Fullscreen API is not supported by this browser.',
114
+ fullscreenExitError: 'Could not exit fullscreen. API not found.',
115
+ fullscreenChangeError: 'Fullscreen mode change failed: {message}',
116
+ },
117
+ zh: {
118
+ // Toolbar
119
+ undo: '撤销',
120
+ redo: '重做',
121
+ loadImage: '加载图片',
122
+ exportImage: '导出图片',
123
+ shareAndEditWithAI: '分享并用AI编辑',
124
+ clearCanvas: '清空画布',
125
+ switchToBrush: '切换到画笔',
126
+ switchToEraser: '切换到橡皮擦',
127
+ penColor: '画笔颜色',
128
+ penSize: '画笔大小: {size}',
129
+ penSizeAria: '画笔大小 {size} 像素',
130
+ openSettings: '打开设置',
131
+ undoAria: '撤销上一步操作',
132
+ redoAria: '重做上一步撤销的操作',
133
+ loadImageAria: '从文件加载图片',
134
+ exportImageAria: '将画布导出为图片',
135
+ shareAndEditWithAIAria: '通过上传分享画布并打开AI编辑选项',
136
+ clearCanvasAria: '清空画布',
137
+ switchToBrushAria: '切换到画笔模式',
138
+ switchToEraserAria: '切换到橡皮擦模式',
139
+ selectPenColorAria: '选择画笔颜色',
140
+
141
+ // AI Modal
142
+ aiModalTitle: '使用 AI 编辑或询问图片',
143
+ closeAiModalAria: '关闭AI编辑窗口',
144
+ aiPromptLabel: '描述您想更改的内容,或提出一个问题:',
145
+ aiPromptPlaceholder: "例如,“给猫带上派对帽”或“这张图里有什么?”",
146
+ errorPrefix: '错误:',
147
+ aiAsk: '提问',
148
+ aiAsking: '提问中...',
149
+ aiGenerateImage: '生成图片',
150
+ aiGenerating: '生成中...',
151
+
152
+ // Settings Panel
153
+ settingsTitle: '应用设置',
154
+ closeSettingsAria: '关闭设置面板',
155
+ interface: '界面',
156
+ showZoomSlider: '显示缩放滑块',
157
+ toggleZoomSliderAria: '切换缩放滑块可见性',
158
+ enterFullscreen: '进入全屏',
159
+ exitFullscreen: '退出全屏',
160
+ fullscreenAria: '进入全屏',
161
+ exitFullscreenAria: '退出全屏',
162
+ canvasDimensions: '画布尺寸',
163
+ canvasSize: '画布大小',
164
+ canvasSizeDescription: '更改尺寸将清除当前画布和历史记录。',
165
+ aiImageGeneration: 'AI 图像生成',
166
+ apiEndpoint: 'API 端点',
167
+ apiEndpointDescription: '选择用于图像生成的 API。占位符会自动填充。',
168
+ pollinationsApi: 'Pollinations AI',
169
+ geminiSimpleGetApi: 'Gemini Simple Get',
170
+ aiImageDimensions: 'AI 图像尺寸',
171
+ aiImageDimensionsDescription: "控制支持它们的 API 的 `width` 和 `height` 参数(例如 Pollinations AI)。",
172
+ apiDefault: 'API 默认(无尺寸)',
173
+ matchCanvasSize: '匹配画布尺寸',
174
+ fixed1024: '固定 1024x1024',
175
+ imageQuality: '图像质量',
176
+ imageQualityDescription: "用于支持 'quality' 参数的 API(例如 Pollinations AI)。",
177
+ qualityLow: '低',
178
+ qualityMedium: '中',
179
+ qualityHigh: '高 (HD)',
180
+ done: '完成',
181
+
182
+ // Confirm Modal
183
+ confirm: '确认',
184
+ cancel: '取消',
185
+ confirmCanvasSizeChangeTitle: '确认更改画布尺寸',
186
+ confirmCanvasSizeChangeMessage: '更改画布尺寸将清除当前绘图和历史记录。您确定要继续吗?',
187
+
188
+ // Zoom Slider
189
+ zoom: '缩放: {percentage}%',
190
+ zoomAria: '当前缩放级别 {percentage} percent',
191
+
192
+ // Canvas
193
+ drawingCanvas: '绘画画布',
194
+ initializingCanvas: '正在初始化画布...',
195
+
196
+ // App Loading
197
+ loadingCanvasMessage: '正在加载您的画布...',
198
+ loadingCanvasSubMessage: '正在准备中!',
199
+
200
+ // Toasts
201
+ imageLoadedSuccess: '图片加载成功。',
202
+ imageLoadError: '处理图片文件时出错。',
203
+ imageReadError: '读取图片文件时出错。',
204
+ imageExported: '图片已导出!',
205
+ noImageToExport: '没有可导出的图片。',
206
+ canvasCleared: '画布已清空。',
207
+ canvasResized: '画布尺寸已调整为 {width}x{height}px。绘图已清除。',
208
+ noCanvasContentToShare: '没有可分享的画布内容。',
209
+ uploadingImage: '正在上传图片...',
210
+ imageTooLarge: '图片太大 ({size}MB)。最大 50MB。',
211
+ uploadFailed: '上传失败: {statusText}',
212
+ magicUploadError: '魔法上传失败。请检查控制台。',
213
+ imageUploadedAndCopied: '图片已上传!URL: {url} (已复制!)',
214
+ aiImageApplied: 'AI 图片已应用到画布!',
215
+ failedToCreateContext: '为 AI 图片创建绘图上下文失败。',
216
+ failedToLoadAiImage: '加载生成的 AI 图片失败。可能为无效的图片格式。',
217
+ generatingAiImage: '正在生成 AI 图片...',
218
+ aiGenFailed: 'AI 图片生成失败: {status} {statusText}',
219
+ aiGenInvalidImage: 'AI 服务未返回有效图片。请尝试其他提示或检查 API 端点。',
220
+ failedToReadAiData: '读取 AI 图片数据失败。',
221
+ unknownAiError: 'AI 图片生成期间发生未知错误。',
222
+ enterPrompt: '请输入提示并确保已上传图片。',
223
+ enterQuestion: '请输入关于图片的问题。',
224
+ unknownAskError: '询问 AI 时发生未知错误。',
225
+ clipboardApiNotAvailable: '剪贴板 API 不可用。',
226
+ failedToCopyUrl: '复制 URL 到剪贴板失败。',
227
+ fullscreenNotSupported: '此浏览器不支持全屏 API。',
228
+ fullscreenExitError: '无法退出全屏。未找到 API。',
229
+ fullscreenChangeError: '全屏模式更改失败: {message}',
230
+ },
231
+ };
232
+
233
+ type Language = keyof typeof translations;
234
+ export type TranslationKey = keyof typeof translations['en'];
235
+
236
+ const getLanguage = (): Language => {
237
+ if (typeof navigator !== 'undefined' && navigator.language) {
238
+ if (navigator.language.startsWith('zh')) {
239
+ return 'zh';
240
+ }
241
+ }
242
+ return 'en';
243
+ }
244
+
245
+ const currentLang = getLanguage();
246
+
247
+ export const getTranslator = () => {
248
+ const langDict = translations[currentLang];
249
+ const fallbackDict = translations.en;
250
+
251
+ return (key: TranslationKey, replacements?: Record<string, string | number>): string => {
252
+ let text: string = (langDict as any)[key] || (fallbackDict as any)[key];
253
+ if (replacements) {
254
+ Object.entries(replacements).forEach(([placeholder, value]) => {
255
+ text = text.replace(`{${placeholder}}`, String(value));
256
+ });
257
+ }
258
+ return text;
259
+ };
260
+ };
types.ts CHANGED
@@ -1,3 +1,6 @@
 
 
1
  // No global shared complex types needed for now.
2
  // Specific types/interfaces are defined within components or services where they are used.
3
  // Example: export interface Point { x: number; y: number; } if it were broadly used.
 
 
1
+ import { TranslationKey } from './translations';
2
+
3
  // No global shared complex types needed for now.
4
  // Specific types/interfaces are defined within components or services where they are used.
5
  // Example: export interface Point { x: number; y: number; } if it were broadly used.
6
+ export type TFunction = (key: TranslationKey, replacements?: Record<string, string | number>) => string;
utils/canvasUtils.ts CHANGED
@@ -1,4 +1,6 @@
1
 
 
 
2
  export const getBlankCanvasDataURL = (width: number, height: number): string => {
3
  const tempCanvas = document.createElement('canvas');
4
  tempCanvas.width = width;
@@ -25,10 +27,11 @@ export const calculateSHA256 = async (blob: Blob): Promise<string> => {
25
 
26
  export const copyToClipboard = async (
27
  text: string,
28
- showInfoToast: (message: string, type: 'info' | 'error') => void // Callback for toast
 
29
  ): Promise<boolean> => {
30
  if (!navigator.clipboard) {
31
- showInfoToast('Clipboard API not available.', 'info');
32
  return false;
33
  }
34
  try {
@@ -36,7 +39,7 @@ export const copyToClipboard = async (
36
  return true;
37
  } catch (err) {
38
  console.error('Failed to copy text: ', err);
39
- showInfoToast('Failed to copy URL to clipboard.', 'error');
40
  return false;
41
  }
42
  };
 
1
 
2
+ import { TFunction } from '../types';
3
+
4
  export const getBlankCanvasDataURL = (width: number, height: number): string => {
5
  const tempCanvas = document.createElement('canvas');
6
  tempCanvas.width = width;
 
27
 
28
  export const copyToClipboard = async (
29
  text: string,
30
+ showInfoToast: (message: string, type: 'info' | 'error') => void, // Callback for toast
31
+ t: TFunction
32
  ): Promise<boolean> => {
33
  if (!navigator.clipboard) {
34
+ showInfoToast(t('clipboardApiNotAvailable'), 'info');
35
  return false;
36
  }
37
  try {
 
39
  return true;
40
  } catch (err) {
41
  console.error('Failed to copy text: ', err);
42
+ showInfoToast(t('failedToCopyUrl'), 'error');
43
  return false;
44
  }
45
  };