diff --git a/package-lock.json b/package-lock.json index 69cc5aa..b232bfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,12 @@ "dependencies": { "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.2", + "i18next": "^26.3.6", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.475.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-i18next": "^17.0.10" }, "devDependencies": { "@tauri-apps/cli": "^2.5.0", @@ -257,6 +260,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -1685,6 +1697,52 @@ "node": ">=6.9.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1843,6 +1901,33 @@ "react": "^19.2.7" } }, + "node_modules/react-i18next": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz", + "integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1945,7 +2030,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -1986,6 +2071,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -2061,6 +2155,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index d23e205..0288907 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,12 @@ "dependencies": { "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.2", + "i18next": "^26.3.6", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.475.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-i18next": "^17.0.10" }, "devDependencies": { "@tauri-apps/cli": "^2.5.0", diff --git a/src/components/LanguageSelector.tsx b/src/components/LanguageSelector.tsx new file mode 100644 index 0000000..830862c --- /dev/null +++ b/src/components/LanguageSelector.tsx @@ -0,0 +1,21 @@ +import { useTranslation } from "react-i18next"; +import { SUPPORTED_LANGUAGES } from "../i18n"; + +export function LanguageSelector() { + const { t, i18n } = useTranslation(); + + return ( + + ); +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..530eaa3 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,31 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import en from "./locales/en/translation.json"; +import ptBR from "./locales/pt-BR/translation.json"; + +i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + resources: { + en: { translation: en }, + "pt-BR": { translation: ptBR }, + }, + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, + detection: { + order: ["localStorage", "navigator"], + caches: ["localStorage"], + lookupLocalStorage: "autoshorts_language", + }, + }); + +export default i18n; + +export const SUPPORTED_LANGUAGES = [ + { code: "en", label: "English" }, + { code: "pt-BR", label: "Português (Brasil)" }, +] as const; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json new file mode 100644 index 0000000..4236d37 --- /dev/null +++ b/src/locales/en/translation.json @@ -0,0 +1,240 @@ +{ + "brand": { + "name": "AutoShorts", + "tagline": "Long recording in. Short clips out." + }, + "sidebar": { + "goHomeTitle": "Go to Home Dashboard", + "importRecording": "Import recording", + "allProjects": "All Projects" + }, + "topbar": { + "apiSettings": "API Settings", + "refreshTitle": "Refresh", + "languageSelector": "Language" + }, + "settings": { + "transcriptionEngine": "Transcription Engine", + "transcriptionOptions": { + "local": "Local Whisper (Offline)", + "deepgram": "Deepgram (Cloud)" + }, + "llmEngine": "LLM Engine", + "llmOptions": { + "local": "Ollama (Offline Local)", + "claude": "Claude (Cloud)", + "deepseek": "DeepSeek (Cloud)", + "gemini": "Google Gemini (Cloud)", + "openai": "OpenAI (Cloud)", + "openrouter": "OpenRouter (Cloud)" + }, + "deepgramKey": "Deepgram API Key", + "claudeKey": "Claude API Key", + "deepseekKey": "DeepSeek API Key", + "geminiKey": "Gemini API Key", + "openaiKey": "OpenAI API Key", + "openrouterKey": "OpenRouter API Key", + "loadedFromEnv": "Loaded from env", + "optionalKey": "Optional ({{label}})", + "ollamaModelName": "Ollama Model Name", + "ollamaModelPlaceholder": "e.g. llama3.2, qwen2.5:7b", + "pull": "Pull", + "resetConfig": "Reset App Configuration & Onboarding", + "resetConfirm": "Are you sure you want to reset your configuration and restart onboarding from scratch?" + }, + "pipeline": { + "transcript": "Transcript", + "moments": "Moments", + "cut": "Cut", + "captions": "Captions", + "export": "Export" + }, + "transcript": { + "title": "Transcript", + "segments_one": "{{count}} segment", + "segments_other": "{{count}} segments", + "empty": "No transcript", + "transcribe": "Transcribe", + "pending": "Transcript pending", + "warningLocal": "Local Whisper (Python package 'openai-whisper') is not installed. Run 'pip3 install openai-whisper' in your terminal.", + "warningCloud": "Deepgram API Key is missing. Transcribing will not work. Please add your key in API Settings." + }, + "candidates": { + "title": "Clip Candidates", + "selected_one": "{{count}} selected", + "selected_other": "{{count}} selected", + "empty": "No candidates", + "cut": "Cut", + "findMoments": "Find Viral Moments", + "warningLocal": "Ollama local server is not running at http://localhost:11434. Moment detection will not work.", + "warningCloud": "{{engine}} API Key is missing. Viral moment identification will not work. Please add your key in API Settings.", + "match": "Match", + "cutReady": "Cut ready", + "cutFailed": "Cut failed", + "pending": "Pending", + "cutting": "Cutting...", + "reCut": "Re-cut", + "subtitles": "Subtitles: {{path}}" + }, + "home": { + "title": "All Projects", + "subtitle": "Select a project below or import a new media file to get started.", + "importRecording": "Import recording", + "noProjects": "No projects found", + "emptyHint": "Import your first recording to begin creating shorts.", + "duration": "Duration:", + "created": "Created:", + "open": "Open", + "rename": "Rename", + "delete": "Delete" + }, + "status": { + "systemReady": "System Ready", + "ffmpeg": "ffmpeg", + "ffprobe": "ffprobe", + "whisperModel": "Whisper Model", + "ollama": "Ollama", + "deepgram": "Deepgram", + "claude": "Claude", + "deepseek": "DeepSeek", + "ffmpegStatus": "FFmpeg status", + "ffprobeStatus": "FFprobe status", + "whisperModelStatus": "Whisper Model status", + "ollamaStatus": "Ollama status", + "deepgramKeyStatus": "Deepgram Key status", + "claudeKeyStatus": "Claude Key status", + "deepseekKeyStatus": "DeepSeek Key status" + }, + "styleModal": { + "title": "Choose Caption Style", + "subtitle": "Select how your automated captions should look on the portrait short-form video clips.", + "styles": { + "modernBox": { + "name": "Modern Box", + "description": "Sleek white text inside a semi-transparent black background padding box. Highly readable." + }, + "classicOutline": { + "name": "Classic Outline", + "description": "Vibrant bold yellow text with a clean black outline. High-energy CapCut formatting." + }, + "minimalShadow": { + "name": "Minimal Shadow", + "description": "Pure white text with a soft, elegant drop shadow. Unobtrusive and modern." + }, + "vibrantCyan": { + "name": "Vibrant Cyan", + "description": "Vibrant tech cyan text with a black drop shadow for a clean look." + }, + "vibrantYellowBox": { + "name": "Vibrant Yellow Box", + "description": "Bold black text inside a solid yellow padding box. Punchy and high visibility." + }, + "vibrantGreen": { + "name": "Vibrant Green", + "description": "High-energy neon green text with black borders and a drop shadow (Hormozi style)." + }, + "vibrantRed": { + "name": "Vibrant Red", + "description": "Dramatic neon crimson text with outline and drop shadow (gaming/action style)." + } + }, + "cancel": "Cancel", + "confirm": "Confirm & Import" + }, + "modelDownload": { + "title": "Downloading Ollama Model", + "description": "Downloading model weights for \"{{modelName}}\". Please do not close the app.", + "connecting": "Connecting to Ollama...", + "complete": "Download complete!" + }, + "onboarding": { + "welcome": "Welcome to AutoShorts", + "tagline": "Long recording in. Short clips out. Select how you would like to run the studio.", + "offline": { + "title": "Fully Offline & Private", + "description": "Process everything locally on your computer. Private, secure, and completely free.", + "badge": "Offline (Ollama)" + }, + "cloud": { + "title": "Cloud APIs", + "description": "Use high-speed cloud services for transcription and analysis. No local GPU needed.", + "badge": "API Keys Required" + }, + "localSetup": { + "title": "Configure Offline Mode", + "subtitle": "Follow these steps to set up your local studio.", + "step1": { + "title": "Install Python Whisper", + "description": "Open your terminal and run the following command to install the transcription engine:", + "command": "pip3 install -U openai-whisper", + "copy": "Copy", + "copied": "Copied!", + "installed": "Whisper installed in Python!", + "notDetected": "Python package 'whisper' not detected yet. Run the command above." + }, + "step2": { + "title": "Set up local LLM (Ollama)", + "description": "Ollama must be installed and running on your machine. If you don't have it installed, you can download it from ollama.com.", + "selectModel": "Select a model to download:", + "models": { + "llama32": { + "name": "LLaMA 3.2 3B", + "size": "1.9 GB", + "description": "Requires 8GB+ RAM. Recommended for standard setups. Fast and efficient." + }, + "qwen3b": { + "name": "Qwen 2.5 3B", + "size": "2.0 GB", + "description": "Requires 8GB+ RAM. Excellent coding and logical reasoning abilities." + }, + "qwen7b": { + "name": "Qwen 2.5 7B", + "size": "4.7 GB", + "description": "Requires 16GB+ RAM. High-quality moment detection and hook precision." + } + } + }, + "back": "Back", + "downloadStart": "Download & Start Setup", + "checkingOllama": "Checking Ollama..." + }, + "cloudSetup": { + "title": "Configure Cloud APIs", + "subtitle": "Add your keys below. AutoShorts will route transcription and analysis to the cloud.", + "deepgramKey": "Deepgram API Key *", + "deepgramPlaceholder": "Insert your Deepgram API Key (for transcription)", + "claudeKey": "Claude API Key", + "claudePlaceholder": "Insert your Anthropic API Key (moment detection)", + "deepseekKey": "DeepSeek API Key", + "deepseekPlaceholder": "Insert your DeepSeek API Key (alternative moment detection)", + "help": "* Deepgram Key + at least one LLM Key (Claude or DeepSeek) is required.", + "back": "Back", + "saveStart": "Save & Start" + }, + "downloading": { + "title": "Downloading Local Model", + "subtitle": "Please wait while your local environment is downloaded. Do not close the application." + }, + "errors": { + "deepgramRequired": "Deepgram API Key is required for cloud mode.", + "llmRequired": "Please provide at least one LLM Key (Claude or DeepSeek).", + "installFailed": "Automatic installation failed: {{error}}. Please install it manually from ollama.com.", + "ollamaNotFound": "Ollama not found. Starting automatic installer...", + "ollamaConnected": "Ollama connected. Initiating model download..." + } + }, + "common": { + "fileFilterMedia": "Media", + "probing": "Probing...", + "initializingDownload": "Initializing download...", + "alertModelDownloadFailed": "Failed to download model: {{error}}", + "renamePrompt": "Rename Project:", + "deleteConfirm": "Are you sure you want to delete the project \"{{name}}\"?", + "downloadModelPrompt": "Ollama model \"{{modelName}}\" is not downloaded. Would you like to download it now?", + "importSuccessWhisperMissing": "Import successful. Local Whisper GGML model (ggml-base.bin) is missing in your models directory. Please add it to start transcription.", + "importSuccessDeepgramMissing": "Import successful. Deepgram key is missing. Please add it to start transcription.", + "importSuccessOllamaMissing": "Import successful. Local Ollama server is not running at http://localhost:11434. Please start it to find viral moments.", + "transcriptionCompleteKeyMissing": "Transcription complete. {{engine}} API Key is missing. Please add it in settings to analyze viral moments.", + "selectProjectTitle": "Open project {{name}}" + } +} diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json new file mode 100644 index 0000000..53c0b8a --- /dev/null +++ b/src/locales/pt-BR/translation.json @@ -0,0 +1,240 @@ +{ + "brand": { + "name": "AutoShorts", + "tagline": "Gravação longa entra. Clipes curtos saem." + }, + "sidebar": { + "goHomeTitle": "Ir para o Painel Inicial", + "importRecording": "Importar gravação", + "allProjects": "Todos os Projetos" + }, + "topbar": { + "apiSettings": "Configurações de API", + "refreshTitle": "Atualizar", + "languageSelector": "Idioma" + }, + "settings": { + "transcriptionEngine": "Motor de Transcrição", + "transcriptionOptions": { + "local": "Whisper Local (Offline)", + "deepgram": "Deepgram (Nuvem)" + }, + "llmEngine": "Motor de LLM", + "llmOptions": { + "local": "Ollama (Local Offline)", + "claude": "Claude (Nuvem)", + "deepseek": "DeepSeek (Nuvem)", + "gemini": "Google Gemini (Nuvem)", + "openai": "OpenAI (Nuvem)", + "openrouter": "OpenRouter (Nuvem)" + }, + "deepgramKey": "Chave de API do Deepgram", + "claudeKey": "Chave de API do Claude", + "deepseekKey": "Chave de API do DeepSeek", + "geminiKey": "Chave de API do Gemini", + "openaiKey": "Chave de API do OpenAI", + "openrouterKey": "Chave de API do OpenRouter", + "loadedFromEnv": "Carregada do ambiente", + "optionalKey": "Opcional ({{label}})", + "ollamaModelName": "Nome do Modelo Ollama", + "ollamaModelPlaceholder": "ex: llama3.2, qwen2.5:7b", + "pull": "Baixar", + "resetConfig": "Redefinir Configurações do Aplicativo e Onboarding", + "resetConfirm": "Tem certeza de que deseja redefinir sua configuração e reiniciar o onboarding do zero?" + }, + "pipeline": { + "transcript": "Transcrição", + "moments": "Momentos", + "cut": "Corte", + "captions": "Legendas", + "export": "Exportar" + }, + "transcript": { + "title": "Transcrição", + "segments_one": "{{count}} segmento", + "segments_other": "{{count}} segmentos", + "empty": "Sem transcrição", + "transcribe": "Transcrever", + "pending": "Transcrição pendente", + "warningLocal": "O Whisper local (pacote Python 'openai-whisper') não está instalado. Execute 'pip3 install openai-whisper' no seu terminal.", + "warningCloud": "A chave de API do Deepgram está ausente. A transcrição não funcionará. Adicione sua chave nas Configurações de API." + }, + "candidates": { + "title": "Candidatos a Clipes", + "selected_one": "{{count}} selecionado", + "selected_other": "{{count}} selecionados", + "empty": "Sem candidatos", + "cut": "Cortar", + "findMoments": "Encontrar Momentos Virais", + "warningLocal": "O servidor local do Ollama não está em execução em http://localhost:11434. A detecção de momentos não funcionará.", + "warningCloud": "A chave de API do {{engine}} está ausente. A identificação de momentos virais não funcionará. Adicione sua chave nas Configurações de API.", + "match": "Compatibilidade", + "cutReady": "Corte pronto", + "cutFailed": "Falha no corte", + "pending": "Pendente", + "cutting": "Cortando...", + "reCut": "Recortar", + "subtitles": "Legendas: {{path}}" + }, + "home": { + "title": "Todos os Projetos", + "subtitle": "Selecione um projeto abaixo ou importe um novo arquivo de mídia para começar.", + "importRecording": "Importar gravação", + "noProjects": "Nenhum projeto encontrado", + "emptyHint": "Importe sua primeira gravação para começar a criar shorts.", + "duration": "Duração:", + "created": "Criado:", + "open": "Abrir", + "rename": "Renomear", + "delete": "Excluir" + }, + "status": { + "systemReady": "Sistema Pronto", + "ffmpeg": "ffmpeg", + "ffprobe": "ffprobe", + "whisperModel": "Modelo Whisper", + "ollama": "Ollama", + "deepgram": "Deepgram", + "claude": "Claude", + "deepseek": "DeepSeek", + "ffmpegStatus": "Status do FFmpeg", + "ffprobeStatus": "Status do FFprobe", + "whisperModelStatus": "Status do Modelo Whisper", + "ollamaStatus": "Status do Ollama", + "deepgramKeyStatus": "Status da chave Deepgram", + "claudeKeyStatus": "Status da chave Claude", + "deepseekKeyStatus": "Status da chave DeepSeek" + }, + "styleModal": { + "title": "Escolher Estilo de Legenda", + "subtitle": "Selecione como suas legendas automatizadas devem aparecer nos clipes verticais de short-form.", + "styles": { + "modernBox": { + "name": "Caixa Moderna", + "description": "Texto branco elegante dentro de uma caixa de preenchimento com fundo preto semitransparente. Altamente legível." + }, + "classicOutline": { + "name": "Contorno Clássico", + "description": "Texto amarelo vibrante e negrito com um contorno preto limpo. Formatação de alta energia no estilo CapCut." + }, + "minimalShadow": { + "name": "Sombra Minimalista", + "description": "Texto branco puro com uma sombra suave e elegante. Discreto e moderno." + }, + "vibrantCyan": { + "name": "Ciano Vibrante", + "description": "Texto ciano tecnológico vibrante com sombra preta para um visual limpo." + }, + "vibrantYellowBox": { + "name": "Caixa Amarela Vibrante", + "description": "Texto preto negrito dentro de uma caixa de preenchimento amarela sólida. Chamativo e de alta visibilidade." + }, + "vibrantGreen": { + "name": "Verde Vibrante", + "description": "Texto verde neon de alta energia com bordas pretas e sombra (estilo Hormozi)." + }, + "vibrantRed": { + "name": "Vermelho Vibrante", + "description": "Texto carmim neon dramático com contorno e sombra (estilo gaming/ação)." + } + }, + "cancel": "Cancelar", + "confirm": "Confirmar e Importar" + }, + "modelDownload": { + "title": "Baixando Modelo Ollama", + "description": "Baixando pesos do modelo \"{{modelName}}\". Por favor, não feche o aplicativo.", + "connecting": "Conectando ao Ollama...", + "complete": "Download concluído!" + }, + "onboarding": { + "welcome": "Bem-vindo ao AutoShorts", + "tagline": "Gravação longa entra. Clipes curtos saem. Selecione como deseja executar o estúdio.", + "offline": { + "title": "Totalmente Offline e Privado", + "description": "Processe tudo localmente no seu computador. Privado, seguro e completamente gratuito.", + "badge": "Offline (Ollama)" + }, + "cloud": { + "title": "APIs na Nuvem", + "description": "Use serviços em nuvem de alta velocidade para transcrição e análise. Não é necessária GPU local.", + "badge": "Chaves de API Necessárias" + }, + "localSetup": { + "title": "Configurar Modo Offline", + "subtitle": "Siga estas etapas para configurar seu estúdio local.", + "step1": { + "title": "Instalar o Python Whisper", + "description": "Abra seu terminal e execute o seguinte comando para instalar o motor de transcrição:", + "command": "pip3 install -U openai-whisper", + "copy": "Copiar", + "copied": "Copiado!", + "installed": "Whisper instalado no Python!", + "notDetected": "Pacote Python 'whisper' ainda não detectado. Execute o comando acima." + }, + "step2": { + "title": "Configurar LLM local (Ollama)", + "description": "O Ollama deve estar instalado e em execução na sua máquina. Se ainda não o tiver instalado, baixe-o em ollama.com.", + "selectModel": "Selecione um modelo para baixar:", + "models": { + "llama32": { + "name": "LLaMA 3.2 3B", + "size": "1,9 GB", + "description": "Requer 8GB+ de RAM. Recomendado para configurações padrão. Rápido e eficiente." + }, + "qwen3b": { + "name": "Qwen 2.5 3B", + "size": "2,0 GB", + "description": "Requer 8GB+ de RAM. Excelentes habilidades de codificação e raciocínio lógico." + }, + "qwen7b": { + "name": "Qwen 2.5 7B", + "size": "4,7 GB", + "description": "Requer 16GB+ de RAM. Detecção de momentos de alta qualidade e precisão no gancho." + } + } + }, + "back": "Voltar", + "downloadStart": "Baixar e Iniciar Configuração", + "checkingOllama": "Verificando Ollama..." + }, + "cloudSetup": { + "title": "Configurar APIs na Nuvem", + "subtitle": "Adicione suas chaves abaixo. O AutoShorts direcionará transcrição e análise para a nuvem.", + "deepgramKey": "Chave de API do Deepgram *", + "deepgramPlaceholder": "Insira sua chave de API do Deepgram (para transcrição)", + "claudeKey": "Chave de API do Claude", + "claudePlaceholder": "Insira sua chave de API da Anthropic (detecção de momentos)", + "deepseekKey": "Chave de API do DeepSeek", + "deepseekPlaceholder": "Insira sua chave de API do DeepSeek (detecção de momentos alternativa)", + "help": "* Chave do Deepgram + pelo menos uma chave de LLM (Claude ou DeepSeek) são necessárias.", + "back": "Voltar", + "saveStart": "Salvar e Iniciar" + }, + "downloading": { + "title": "Baixando Modelo Local", + "subtitle": "Aguarde enquanto seu ambiente local é baixado. Não feche o aplicativo." + }, + "errors": { + "deepgramRequired": "A chave de API do Deepgram é obrigatória para o modo nuvem.", + "llmRequired": "Forneça pelo menos uma chave de LLM (Claude ou DeepSeek).", + "installFailed": "Falha na instalação automática: {{error}}. Instale manualmente em ollama.com.", + "ollamaNotFound": "Ollama não encontrado. Iniciando instalador automático...", + "ollamaConnected": "Ollama conectado. Iniciando download do modelo..." + } + }, + "common": { + "fileFilterMedia": "Mídia", + "probing": "Sondando...", + "initializingDownload": "Inicializando download...", + "alertModelDownloadFailed": "Falha ao baixar o modelo: {{error}}", + "renamePrompt": "Renomear Projeto:", + "deleteConfirm": "Tem certeza de que deseja excluir o projeto \"{{name}}\"?", + "downloadModelPrompt": "O modelo \"{{modelName}}\" do Ollama não está baixado. Deseja baixá-lo agora?", + "importSuccessWhisperMissing": "Importação bem-sucedida. O modelo GGML Whisper local (ggml-base.bin) está ausente no diretório de modelos. Adicione-o para iniciar a transcrição.", + "importSuccessDeepgramMissing": "Importação bem-sucedida. A chave do Deepgram está ausente. Adicione-a para iniciar a transcrição.", + "importSuccessOllamaMissing": "Importação bem-sucedida. O servidor local do Ollama não está em execução em http://localhost:11434. Inicie-o para encontrar momentos virais.", + "transcriptionCompleteKeyMissing": "Transcrição concluída. A chave de API do {{engine}} está ausente. Adicione-a nas configurações para analisar momentos virais.", + "selectProjectTitle": "Abrir projeto {{name}}" + } +} diff --git a/src/main.tsx b/src/main.tsx index 13f561f..62de63e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; import { listen } from "@tauri-apps/api/event"; +import { useTranslation } from "react-i18next"; +import "./i18n"; +import { LanguageSelector } from "./components/LanguageSelector"; import { AudioLines, BadgeCheck, @@ -112,6 +115,7 @@ type BusyState = | "cut"; function App() { + const { t } = useTranslation(); const [environment, setEnvironment] = useState(null); const [projects, setProjects] = useState([]); const [detail, setDetail] = useState(null); @@ -251,7 +255,7 @@ function App() { const pullModelDirectly = async (modelName: string) => { setDownloadingModelName(modelName); setModelDownloadProgress(0); - setModelDownloadStatus("Connecting to Ollama..."); + setModelDownloadStatus(t("modelDownload.connecting")); try { const unlisten = await listen<{ status: string; @@ -268,11 +272,11 @@ function App() { await invoke("pull_ollama_model", { modelName }); unlisten(); - setModelDownloadStatus("Download complete!"); + setModelDownloadStatus(t("modelDownload.complete")); setModelDownloadProgress(100); setTimeout(() => setDownloadingModelName(null), 500); } catch (err) { - alert("Failed to download model: " + String(err)); + alert(t("common.alertModelDownloadFailed", { error: String(err) })); setDownloadingModelName(null); } }; @@ -311,7 +315,7 @@ function App() { multiple: false, filters: [ { - name: "Media", + name: t("common.fileFilterMedia"), extensions: ["mp4", "mov", "mp3", "wav", "m4a"], }, ], @@ -349,20 +353,20 @@ function App() { if (transcriptionEngine === "local") { if (!env.hasLocalWhisperModel) { - setError("Import successful. Local Whisper GGML model (ggml-base.bin) is missing in your models directory. Please add it to start transcription."); + setError(t("common.importSuccessWhisperMissing")); return; } } else { const hasDG = env.hasDeepgramKey || deepgramKey.trim().length > 0; if (!hasDG) { - setError("Import successful. Deepgram key is missing. Please add it to start transcription."); + setError(t("common.importSuccessDeepgramMissing")); return; } } if (llmEngine === "local") { if (!env.hasOllama) { - setError("Import successful. Local Ollama server is not running at http://localhost:11434. Please start it to find viral moments."); + setError(t("common.importSuccessOllamaMissing")); return; } } else { @@ -379,13 +383,8 @@ function App() { llmEngine === "openai" ? (env.hasOpenaiKey || activeKey.trim().length > 0) : llmEngine === "openrouter" ? (env.hasOpenrouterKey || activeKey.trim().length > 0) : false; if (!hasActiveKey) { - const engineName = - llmEngine === "claude" ? "Claude" : - llmEngine === "deepseek" ? "DeepSeek" : - llmEngine === "gemini" ? "Gemini" : - llmEngine === "openai" ? "OpenAI" : - llmEngine === "openrouter" ? "OpenRouter" : "LLM"; - setError(`Transcription complete. ${engineName} API Key is missing. Please add it in settings to analyze viral moments.`); + const engineName = t(`status.${llmEngine === "claude" ? "claude" : llmEngine === "deepseek" ? "deepseek" : llmEngine === "gemini" ? "gemini" : llmEngine === "openai" ? "openai" : llmEngine === "openrouter" ? "openrouter" : "llm"}`); + setError(t("common.transcriptionCompleteKeyMissing", { engine: engineName })); return; } } @@ -420,7 +419,7 @@ function App() { } catch (err) { const errMsg = String(err); if (llmEngine === "local" && (errMsg.includes("not found") || errMsg.includes("404"))) { - if (window.confirm(`Ollama model "${localLlmModel}" is not downloaded. Would you like to download it now?`)) { + if (window.confirm(t("common.downloadModelPrompt", { modelName: localLlmModel }))) { setTimeout(() => { void pullModelDirectly(localLlmModel).then(() => { void refresh(projectId); @@ -439,7 +438,7 @@ function App() { const project = projects.find((p) => p.id === projectId); if (!project) return; const currentName = project.name || fileName(project.sourcePath); - const newName = window.prompt("Rename Project:", currentName); + const newName = window.prompt(t("common.renamePrompt"), currentName); if (newName === null) return; const trimmed = newName.trim(); if (!trimmed) return; @@ -456,7 +455,7 @@ function App() { const project = projects.find((p) => p.id === projectId); if (!project) return; const name = project.name || fileName(project.sourcePath); - if (!window.confirm(`Are you sure you want to delete the project "${name}"?`)) return; + if (!window.confirm(t("common.deleteConfirm", { name }))) return; try { await invoke("delete_project", { projectId }); @@ -502,7 +501,7 @@ function App() { } catch (err) { const errMsg = String(err); if (llmEngine === "local" && (errMsg.includes("not found") || errMsg.includes("404"))) { - if (window.confirm(`Ollama model "${localLlmModel}" is not downloaded. Would you like to download it now?`)) { + if (window.confirm(t("common.downloadModelPrompt", { modelName: localLlmModel }))) { setTimeout(() => { void pullModelDirectly(localLlmModel).then(() => { void refresh(detail.project.id); @@ -596,29 +595,29 @@ function App() { className="brand-row" onClick={() => setDetail(null)} style={{ cursor: "pointer" }} - title="Go to Home Dashboard" + title={t("sidebar.goHomeTitle")} >
-

AutoShorts

-

Long recording in. Short clips out.

+

{t("brand.name")}

+

{t("brand.tagline")}

-
+
@@ -645,15 +644,16 @@ function App() {

{detail.project.name || fileName(detail.project.sourcePath)}

+ -
@@ -663,89 +663,89 @@ function App() {
{transcriptionEngine === "deepgram" && ( )} {llmEngine === "claude" && ( )} {llmEngine === "deepseek" && ( )} {llmEngine === "gemini" && ( )} {llmEngine === "openai" && ( )} {llmEngine === "openrouter" && ( @@ -753,12 +753,12 @@ function App() { {llmEngine === "local" && ( @@ -779,13 +779,13 @@ function App() { className="icon-button" style={{ background: 'rgba(239, 68, 68, 0.08)', borderColor: 'rgba(239, 68, 68, 0.2)', color: '#f87171' }} onClick={() => { - if (window.confirm("Are you sure you want to reset your configuration and restart onboarding from scratch?")) { + if (window.confirm(t("settings.resetConfirm"))) { localStorage.clear(); window.location.reload(); } }} > - Reset App Configuration & Onboarding + {t("settings.resetConfig")}
@@ -794,24 +794,24 @@ function App() { {error &&
{error}
}
- } label="Transcript" done={Boolean(detail.transcript)} /> - } label="Moments" done={detail.candidates.length > 0} /> - } label="Cut" done={selectedCount > 0 && selectedCutCount === selectedCount} /> - } label="Captions" done={selectedCount > 0 && selectedCaptionsCount === selectedCount} /> - } label="Export" done={selectedCount > 0 && selectedCutCount === selectedCount} /> + } label={t("pipeline.transcript")} done={Boolean(detail.transcript)} /> + } label={t("pipeline.moments")} done={detail.candidates.length > 0} /> + } label={t("pipeline.cut")} done={selectedCount > 0 && selectedCutCount === selectedCount} /> + } label={t("pipeline.captions")} done={selectedCount > 0 && selectedCaptionsCount === selectedCount} /> + } label={t("pipeline.export")} done={selectedCount > 0 && selectedCutCount === selectedCount} />
-

Transcript

-

{transcript ? `${transcript.segments.length} segments` : "No transcript"}

+

{t("transcript.title")}

+

{transcript ? t("transcript.segments", { count: transcript.segments.length }) : t("transcript.empty")}

@@ -819,8 +819,8 @@ function App() { {!canTranscribe && (
{transcriptionEngine === "local" - ? `⚠️ Local Whisper (Python package 'openai-whisper') is not installed. Run 'pip3 install openai-whisper' in your terminal.` - : "⚠️ Deepgram API Key is missing. Transcribing will not work. Please add your key in API Settings."} + ? t("transcript.warningLocal") + : t("transcript.warningCloud")}
)} @@ -830,24 +830,24 @@ function App() { {formatTime(segment.start)}

{segment.text}

- )) ?? } label="Transcript pending" />} + )) ?? } label={t("transcript.pending")} />}
-

Clip Candidates

-

{detail.candidates.length ? `${selectedCount} selected` : "No candidates"}

+

{t("candidates.title")}

+

{detail.candidates.length ? t("candidates.selected", { count: selectedCount }) : t("candidates.empty")}

@@ -855,13 +855,8 @@ function App() { {!canUseActiveLlm && (
{llmEngine === "local" - ? "⚠️ Ollama local server is not running at http://localhost:11434. Moment detection will not work." - : `⚠️ ${llmEngine === "claude" ? "Claude" : - llmEngine === "deepseek" ? "DeepSeek" : - llmEngine === "gemini" ? "Gemini" : - llmEngine === "openai" ? "OpenAI" : - llmEngine === "openrouter" ? "OpenRouter" : "LLM" - } API Key is missing. Viral moment identification will not work. Please add your key in API Settings.`} + ? t("candidates.warningLocal") + : t("candidates.warningCloud", { engine: t(`status.${llmEngine === "claude" ? "claude" : llmEngine === "deepseek" ? "deepseek" : llmEngine === "gemini" ? "gemini" : llmEngine === "openai" ? "openai" : llmEngine === "openrouter" ? "openrouter" : "llm"}`) })}
)} @@ -907,14 +902,14 @@ function App() {
{formatTime(candidate.startSec)} - {formatTime(candidate.endSec)} - {Math.round(candidate.score * 100)}% Match + {Math.round(candidate.score * 100)}% {t("candidates.match")}

{candidate.hook}

{candidate.rationale}

- {isCut ? "Cut ready" : clip?.status === "error" ? "Cut failed" : clip?.status ?? "Pending"} + {isCut ? t("candidates.cutReady") : clip?.status === "error" ? t("candidates.cutFailed") : clip?.status ?? t("candidates.pending")}
{clip?.outputPath &&
{clip.outputPath}
} {clip?.captionAssPath && (
- Subtitles: {clip.captionAssPath} + {t("candidates.subtitles", { path: clip.captionAssPath })}
)} {clip?.renderLog &&
{clip.renderLog}
} @@ -940,7 +935,7 @@ function App() { ); })} - {detail.candidates.length === 0 && } label="Moments pending" />} + {detail.candidates.length === 0 && } label={t("candidates.empty")} />}
@@ -949,13 +944,16 @@ function App() {
-

All Projects

-

Select a project below or import a new media file to get started.

+

{t("home.title")}

+

{t("home.subtitle")}

+
+
+ +
-
{projects.length > 0 ? ( @@ -970,18 +968,18 @@ function App() {

{name}

- Duration: {project.sourceDuration ? formatTime(project.sourceDuration) : "Probing..."} - Created: {new Date(project.createdAt).toLocaleDateString()} + {t("home.duration")} {project.sourceDuration ? formatTime(project.sourceDuration) : t("common.probing")} + {t("home.created")} {new Date(project.createdAt).toLocaleDateString()}
@@ -991,8 +989,8 @@ function App() { ) : (
-

No projects found

-

Import your first recording to begin creating shorts.

+

{t("home.noProjects")}

+

{t("home.emptyHint")}

)} @@ -1002,17 +1000,17 @@ function App() {
- System Ready + {t("status.systemReady")}
- ffmpeg - ffprobe - Whisper Model - Ollama - Deepgram - Claude - DeepSeek + {t("status.ffmpeg")} + {t("status.ffprobe")} + {t("status.whisperModel")} + {t("status.ollama")} + {t("status.deepgram")} + {t("status.claude")} + {t("status.deepseek")}
@@ -1021,8 +1019,8 @@ function App() {
-

Choose Caption Style

-

Select how your automated captions should look on the portrait short-form video clips.

+

{t("styleModal.title")}

+

{t("styleModal.subtitle")}

@@ -1033,8 +1031,8 @@ function App() {
BRAINFOOD BECAUSE
-
Modern Box
-
Sleek white text inside a semi-transparent black background padding box. Highly readable.
+
{t("styleModal.styles.modernBox.name")}
+
{t("styleModal.styles.modernBox.description")}
BRAINFOOD BECAUSE
-
Classic Outline
-
Vibrant bold yellow text with a clean black outline. High-energy CapCut formatting.
+
{t("styleModal.styles.classicOutline.name")}
+
{t("styleModal.styles.classicOutline.description")}
BRAINFOOD BECAUSE
-
Minimal Shadow
-
Pure white text with a soft, elegant drop shadow. Unobtrusive and modern.
+
{t("styleModal.styles.minimalShadow.name")}
+
{t("styleModal.styles.minimalShadow.description")}
BRAINFOOD BECAUSE
-
Vibrant Cyan
-
Vibrant tech cyan text with a black drop shadow for a clean look.
+
{t("styleModal.styles.vibrantCyan.name")}
+
{t("styleModal.styles.vibrantCyan.description")}
BRAINFOOD BECAUSE
-
Vibrant Yellow Box
-
Bold black text inside a solid yellow padding box. Punchy and high visibility.
+
{t("styleModal.styles.vibrantYellowBox.name")}
+
{t("styleModal.styles.vibrantYellowBox.description")}
BRAINFOOD BECAUSE
-
Vibrant Green
-
High-energy neon green text with black borders and a drop shadow (Hormozi style).
+
{t("styleModal.styles.vibrantGreen.name")}
+
{t("styleModal.styles.vibrantGreen.description")}
BRAINFOOD BECAUSE
-
Vibrant Red
-
Dramatic neon crimson text with outline and drop shadow (gaming/action style).
+
{t("styleModal.styles.vibrantRed.name")}
+
{t("styleModal.styles.vibrantRed.description")}
@@ -1119,8 +1117,8 @@ function App() {
-

Downloading Ollama Model

-

Downloading model weights for "{downloadingModelName}". Please do not close the app.

+

{t("modelDownload.title")}

+

{t("modelDownload.description", { modelName: downloadingModelName })}

@@ -1210,6 +1208,7 @@ function Onboarding({ deepseekKey: initialDeepseekKey, refreshEnv, }: OnboardingProps) { + const { t } = useTranslation(); const [setupMode, setSetupMode] = useState<"choose" | "local" | "cloud" | "downloading">("choose"); const [selectedModel, setSelectedModel] = useState("llama3.2"); @@ -1217,7 +1216,7 @@ function Onboarding({ const [antKey, setAntKey] = useState(initialAnthropicKey); const [dsKey, setDsKey] = useState(initialDeepseekKey); - const [downloadStatus, setDownloadStatus] = useState("Initializing download..."); + const [downloadStatus, setDownloadStatus] = useState(t("common.initializingDownload")); const [downloadProgress, setDownloadProgress] = useState(0); const [error, setError] = useState(null); const [checkingOllama, setCheckingOllama] = useState(false); @@ -1232,11 +1231,11 @@ function Onboarding({ const handleCloudSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!dgKey.trim()) { - setError("Deepgram API Key is required for cloud mode."); + setError(t("onboarding.errors.deepgramRequired")); return; } if (!antKey.trim() && !dsKey.trim()) { - setError("Please provide at least one LLM Key (Claude or DeepSeek)."); + setError(t("onboarding.errors.llmRequired")); return; } @@ -1280,7 +1279,7 @@ function Onboarding({ if (!isOllamaRunning) { setSetupMode("downloading"); - setDownloadStatus("Ollama not found. Starting automatic installer..."); + setDownloadStatus(t("onboarding.errors.ollamaNotFound")); try { const unlistenInstall = await listen("ollama-install-status", (event) => { @@ -1290,14 +1289,14 @@ function Onboarding({ await invoke("install_ollama"); unlistenInstall(); } catch (err) { - setError("Automatic installation failed: " + String(err) + ". Please install it manually from ollama.com."); + setError(t("onboarding.errors.installFailed", { error: String(err) })); setSetupMode("local"); return; } } setSetupMode("downloading"); - setDownloadStatus("Ollama connected. Initiating model download..."); + setDownloadStatus(t("onboarding.errors.ollamaConnected")); try { const unlisten = await listen<{ @@ -1342,8 +1341,8 @@ function Onboarding({
-

Welcome to AutoShorts

-

Long recording in. Short clips out. Select how you would like to run the studio.

+

{t("onboarding.welcome")}

+

{t("onboarding.tagline")}

@@ -1351,18 +1350,18 @@ function Onboarding({
-

Fully Offline & Private

-

Process everything locally on your computer. Private, secure, and completely free.

-
Offline (Ollama)
+

{t("onboarding.offline.title")}

+

{t("onboarding.offline.description")}

+
{t("onboarding.offline.badge")}
setSetupMode("cloud")}>
-

Cloud APIs

-

Use high-speed cloud services for transcription and analysis. No local GPU needed.

-
API Keys Required
+

{t("onboarding.cloud.title")}

+

{t("onboarding.cloud.description")}

+
{t("onboarding.cloud.badge")}
@@ -1371,8 +1370,8 @@ function Onboarding({ {setupMode === "local" && (
-

Configure Offline Mode

-

Follow these steps to set up your local studio.

+

{t("onboarding.localSetup.title")}

+

{t("onboarding.localSetup.subtitle")}

{error &&
{error}
} @@ -1381,19 +1380,19 @@ function Onboarding({
1
-

Install Python Whisper

-

Open your terminal and run the following command to install the transcription engine:

+

{t("onboarding.localSetup.step1.title")}

+

{t("onboarding.localSetup.step1.description")}

- pip3 install -U openai-whisper + {t("onboarding.localSetup.step1.command")}
{environment?.hasLocalWhisperModel ? ( - Whisper installed in Python! + {t("onboarding.localSetup.step1.installed")} ) : ( - ⚠️ Python package 'whisper' not detected yet. Run the command above. + ⚠️ {t("onboarding.localSetup.step1.notDetected")} )}
@@ -1401,12 +1400,9 @@ function Onboarding({
2
-

Set up local LLM (Ollama)

-

- Ollama must be installed and running on your machine. - If you don't have it installed, you can download it from ollama.com. -

-

Select a model to download:

+

{t("onboarding.localSetup.step2.title")}

+

+

{t("onboarding.localSetup.step2.selectModel")}

setSelectedModel("llama3.2")} >
-
LLaMA 3.2 3B
- 1.9 GB +
{t("onboarding.localSetup.step2.models.llama32.name")}
+ {t("onboarding.localSetup.step2.models.llama32.size")}
-

Requires 8GB+ RAM. Recommended for standard setups. Fast and efficient.

+

{t("onboarding.localSetup.step2.models.llama32.description")}

setSelectedModel("qwen2.5:3b")} >
-
Qwen 2.5 3B
- 2.0 GB +
{t("onboarding.localSetup.step2.models.qwen3b.name")}
+ {t("onboarding.localSetup.step2.models.qwen3b.size")}
-

Requires 8GB+ RAM. Excellent coding and logical reasoning abilities.

+

{t("onboarding.localSetup.step2.models.qwen3b.description")}

setSelectedModel("qwen2.5:7b")} >
-
Qwen 2.5 7B
- 4.7 GB +
{t("onboarding.localSetup.step2.models.qwen7b.name")}
+ {t("onboarding.localSetup.step2.models.qwen7b.size")}
-

Requires 16GB+ RAM. High-quality moment detection and hook precision.

+

{t("onboarding.localSetup.step2.models.qwen7b.description")}

@@ -1447,7 +1443,7 @@ function Onboarding({
- +
@@ -1464,48 +1460,48 @@ function Onboarding({ {setupMode === "cloud" && (
-

Configure Cloud APIs

-

Add your keys below. AutoShorts will route transcription and analysis to the cloud.

+

{t("onboarding.cloudSetup.title")}

+

{t("onboarding.cloudSetup.subtitle")}

{error &&
{error}
}
- + setDgKey(e.target.value)} - placeholder="Insert your Deepgram API Key (for transcription)" + placeholder={t("onboarding.cloudSetup.deepgramPlaceholder")} />
- + setAntKey(e.target.value)} - placeholder="Insert your Anthropic API Key (moment detection)" + placeholder={t("onboarding.cloudSetup.claudePlaceholder")} />
- + setDsKey(e.target.value)} - placeholder="Insert your DeepSeek API Key (alternative moment detection)" + placeholder={t("onboarding.cloudSetup.deepseekPlaceholder")} />
-

* Deepgram Key + at least one LLM Key (Claude or DeepSeek) is required.

+

{t("onboarding.cloudSetup.help")}

- - + +
)} @@ -1513,8 +1509,8 @@ function Onboarding({ {setupMode === "downloading" && (
-

Downloading Local Model

-

Please wait while your local environment is downloaded. Do not close the application.

+

{t("onboarding.downloading.title")}

+

{t("onboarding.downloading.subtitle")}

diff --git a/src/styles.css b/src/styles.css index a856eef..b66e779 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1714,3 +1714,23 @@ button:disabled { font-weight: 700; color: var(--accent-primary); } + +.language-selector { + background: var(--bg-input); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 13px; + padding: 6px 10px; + cursor: pointer; + outline: none; +} + +.language-selector:focus { + border-color: var(--border-color-focus); +} + +.language-selector option { + background: var(--bg-panel); + color: var(--text-primary); +}