diff --git a/app/package.json b/app/package.json index b4059f46..e0ad1de3 100644 --- a/app/package.json +++ b/app/package.json @@ -44,11 +44,14 @@ "clsx": "^2.1.1", "date-fns": "^3.6.0", "framer-motion": "^12.29.0", + "i18next": "^26.0.6", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.454.0", "motion": "^12.29.0", "react": "^18.3.0", "react-dom": "^18.3.0", "react-hook-form": "^7.53.0", + "react-i18next": "^17.0.4", "react-sound-visualizer": "^1.4.0", "tailwind-merge": "^2.5.4", "wavesurfer.js": "^7.0.0", diff --git a/app/src/components/Generation/FloatingGenerateBox.tsx b/app/src/components/Generation/FloatingGenerateBox.tsx index 79e6a92a..ac64406f 100644 --- a/app/src/components/Generation/FloatingGenerateBox.tsx +++ b/app/src/components/Generation/FloatingGenerateBox.tsx @@ -3,6 +3,7 @@ import { useMatchRoute } from '@tanstack/react-router'; import { AnimatePresence, motion } from 'framer-motion'; import { Loader2, SlidersHorizontal, Sparkles } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form'; import { @@ -34,6 +35,7 @@ export function FloatingGenerateBox({ isPlayerOpen = false, showVoiceSelector = false, }: FloatingGenerateBoxProps) { + const { t } = useTranslation(); const selectedProfileId = useUIStore((state) => state.selectedProfileId); const setSelectedProfileId = useUIStore((state) => state.setSelectedProfileId); const setSelectedEngine = useUIStore((state) => state.setSelectedEngine); @@ -276,10 +278,12 @@ export function FloatingGenerateBox({ onChange={field.onChange} placeholder={ isStoriesRoute && currentStory - ? `Generate speech for "${currentStory.name}"... (type / for effects)` + ? t('generation.placeholder.storyWithEffects', { + name: currentStory.name, + }) : selectedProfile - ? `Type / for effects like [laugh], [sigh]...` - : 'Select a voice profile above...' + ? t('generation.placeholder.effectsHint') + : t('generation.placeholder.selectVoice') } className="px-3 py-2 resize-none bg-transparent border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0 outline-none ring-0 rounded-2xl text-sm w-full" style={{ @@ -302,10 +306,12 @@ export function FloatingGenerateBox({ }} placeholder={ isStoriesRoute && currentStory - ? `Generate speech for "${currentStory.name}"...` + ? t('generation.placeholder.story', { name: currentStory.name }) : selectedProfile - ? `Generate speech using ${selectedProfile.name}...` - : 'Select a voice profile above...' + ? t('generation.placeholder.profile', { + name: selectedProfile.name, + }) + : t('generation.placeholder.selectVoice') } className="resize-none bg-transparent border-none focus-visible:ring-0 focus-visible:ring-offset-0 focus:outline-none focus:ring-0 outline-none ring-0 rounded-2xl text-sm placeholder:text-muted-foreground/60 w-full" style={{ @@ -334,10 +340,10 @@ export function FloatingGenerateBox({ size="icon" aria-label={ isPending - ? 'Generating...' + ? t('generation.button.generating') : !selectedProfileId - ? 'Select a voice profile first' - : 'Generate speech' + ? t('generation.button.selectFirst') + : t('generation.button.generate') } > {isPending ? ( @@ -348,10 +354,10 @@ export function FloatingGenerateBox({ {isPending - ? 'Generating...' + ? t('generation.button.generating') : !selectedProfileId - ? 'Select a voice profile first' - : 'Generate speech'} + ? t('generation.button.selectFirst') + : t('generation.button.generate')} @@ -379,15 +385,15 @@ export function FloatingGenerateBox({ )} aria-label={ isInstructExpanded - ? 'Hide delivery instructions' - : 'Show delivery instructions' + ? t('generation.instruct.hide') + : t('generation.instruct.show') } aria-pressed={isInstructExpanded} > - Delivery instructions (tone, emotion, pace) + {t('generation.instruct.tooltip')} @@ -414,7 +420,7 @@ export function FloatingGenerateBox({