This commit is contained in:
wangshouhao 2024-08-17 23:30:53 +08:00 committed by GitHub
commit 7f68e5e687
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1349 additions and 144 deletions

View File

@ -36,10 +36,14 @@
"codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"html2canvas": "^1.4.1",
"i18next": "^23.13.0",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.1",
"react-icons": "^4.12.0",
"react-router-dom": "^6.20.1",
"tailwind-merge": "^2.0.0",

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "Screenshot zu Code",
"errors": {
"emptyInstruction": "Bitte geben Sie Anweisungen für die KI zur Aktualisierung an.",
"noCurrentVersion": "Keine aktuelle Version festgelegt. Kontaktieren Sie den Support oder öffnen Sie ein GitHub-Issue.",
"invalidHistory": "Ungültige Versionsverlauf. Dies sollte nicht passieren. Bitte kontaktieren Sie den Support oder öffnen Sie ein GitHub-Issue."
}
},
"history": {
"versions": "Versionen",
"toggle": "Umschalten",
"fullPrompt": "Vollständige Eingabeaufforderung",
"create": "Erstellen",
"edit": "Bearbeiten",
"importedFromCode": "Aus Code importiert"
},
"messages": {
"deprecationMessage": "Wir unterstützen dieses Modell nicht mehr. Stattdessen wird die Codegenerierung GPT-4o oder Claude Sonnet 3.5 verwenden, die zwei modernsten Modelle.",
"onboardingNote": {
"intro": "Um Screenshot zu Code zu nutzen,",
"buyCredits": "kaufen Sie Credits (100 Generierungen für $36)",
"orUseKey": "oder verwenden Sie Ihren eigenen OpenAI API-Schlüssel mit GPT4-Vision-Zugang.",
"followInstructions": "Folgen Sie diesen Anweisungen, um einen Schlüssel zu erhalten.",
"pasteKey": "und fügen Sie ihn in den Einstellungen-Dialog ein (Zahnrad-Symbol oben). Ihr Schlüssel wird nur in Ihrem Browser gespeichert. Niemals auf unseren Servern."
},
"picoBadge": {
"featureRequests": "Feature-Wünsche?",
"openSourceProject": "ein Open-Source-Projekt von Pico"
},
"tipLink": {
"text": "Tipps für bessere Ergebnisse"
}
},
"preview": {
"codeTab": {
"copyCode": "Code kopieren",
"openIn": "Öffnen in",
"codeCopied": "In die Zwischenablage kopiert"
},
"pane": {
"reset": "Zurücksetzen",
"download": "Herunterladen",
"desktop": "Desktop",
"mobile": "Mobil",
"code": "Code"
}
},
"recording": {
"start": "Bildschirm aufnehmen",
"inProgress": "Aufnahme läuft...",
"finish": "Aufnahme beenden",
"captured": "Bildschirmaufnahme erfasst.",
"reRecord": "Erneut aufnehmen",
"generate": "Generieren",
"errorStarting": "Bildschirmaufnahme konnte nicht gestartet werden",
"noDataError": "Bildschirmaufnahme existiert nicht. Bitte versuchen Sie es erneut."
},
"sidebar": {
"videoGenerationWarning": "Die Codegenerierung aus Videos kann 3-4 Minuten dauern. Wir machen mehrere Durchläufe, um das beste Ergebnis zu erzielen. Bitte haben Sie Geduld.",
"cancel": "Abbrechen",
"updateInstructionPlaceholder": "Sagen Sie der KI, was geändert werden soll...",
"includeScreenshot": "Screenshot der aktuellen Version einschließen?",
"update": "Aktualisieren",
"regenerate": "Neu generieren",
"referenceImageAlt": "Referenz",
"originalVideo": "Originalvideo",
"originalScreenshot": "Original-Screenshot",
"console": "Konsole"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "Sagen Sie der KI, was an diesem Element geändert werden soll...",
"update": "Aktualisieren"
},
"selectAndEdit": {
"exitMode": "Auswahlmodus beenden",
"enterMode": "Auswählen und aktualisieren"
}
},
"setting": {
"title": "Einstellungen",
"dallePlaceholder": "DALL-E Platzhalter-Bildgenerierung",
"dallePlaceholderDescription": "Mehr Spaß damit, aber wenn Sie Geld sparen möchten, schalten Sie es aus.",
"openAIApiKey": "OpenAI API-Schlüssel",
"apiKeyDescription": "Nur in Ihrem Browser gespeichert. Niemals auf Servern gespeichert. Überschreibt Ihre .env-Konfiguration.",
"openAIApiKeyPlaceholder": "OpenAI API-Schlüssel",
"openAIBaseURL": "OpenAI Basis-URL (optional)",
"openAIBaseURLDescription": "Ersetzen Sie durch eine Proxy-URL, wenn Sie die Standardeinstellung nicht verwenden möchten.",
"openAIBaseURLPlaceholder": "OpenAI Basis-URL",
"anthropicApiKey": "Anthropic API-Schlüssel",
"anthropicApiKeyPlaceholder": "Anthropic API-Schlüssel",
"language": "Sprache",
"screenshotConfig": "Screenshot-Konfiguration per URL",
"screenshotDescription": "Wenn Sie URLs direkt verwenden möchten, anstatt selbst Screenshots zu machen, fügen Sie einen ScreenshotOne API-Schlüssel hinzu.",
"getFreeScreenshots": "Erhalten Sie 100 Screenshots/Monat kostenlos.",
"screenshotApiKeyPlaceholder": "ScreenshotOne API-Schlüssel",
"themeSettings": "Theme-Einstellungen",
"appTheme": "App-Theme",
"toggleDarkMode": "Dunkelmodus umschalten",
"codeEditorTheme": "Code-Editor-Theme - erfordert Seitenaktualisierung zur Aktualisierung",
"save": "Speichern",
"aiModel": "KI-Modell:",
"beta": "Beta",
"generating": "Generierung:"
},
"imageUpload": {
"dragDropPrompt": "Screenshot hier per Drag & Drop ablegen, \n oder klicken Sie zum Hochladen",
"new": "Neu!",
"uploadPrompt": "Laden Sie eine Bildschirmaufnahme (.mp4, .mov) hoch oder nehmen Sie Ihren Bildschirm auf, um eine ganze App zu klonen (experimentell).",
"learnMore": "Erfahren Sie mehr.",
"errorReading": "Fehler beim Lesen der Dateien: "
},
"importCode": {
"buttonText": "Aus Code importieren",
"dialogTitle": "Fügen Sie Ihren HTML-Code ein",
"dialogDescription": "Stellen Sie sicher, dass der zu importierende Code gültiges HTML ist.",
"textareaPlaceholder": "Fügen Sie Ihren HTML-Code hier ein...",
"stackLabel": "Stack:",
"importButton": "Importieren",
"errorEmptyCode": "Bitte fügen Sie Code ein",
"errorNoStack": "Bitte wählen Sie Ihren Stack aus"
},
"termsOfService": {
"title": "Geben Sie Ihre E-Mail-Adresse ein, um zu beginnen",
"emailPlaceholder": "E-Mail",
"consentText": "Durch die Angabe Ihrer E-Mail-Adresse stimmen Sie dem Erhalt gelegentlicher Produkt-Updates zu und akzeptieren die",
"termsLink": "Nutzungsbedingungen",
"localRunText": "Möchten Sie es lieber lokal ausführen? Dieses Projekt ist Open Source.",
"githubLink": "Laden Sie den Code herunter und beginnen Sie auf Github.",
"emailError": "Bitte geben Sie Ihre E-Mail-Adresse ein",
"agreeButton": "Zustimmen & Fortfahren",
"usageText": "Designer und Ingenieure dieser Organisationen verwenden Screenshot zu Code, um Schnittstellen schneller zu erstellen."
},
"urlInput": {
"errorNoApiKey": "Bitte fügen Sie einen ScreenshotOne API-Schlüssel in den Einstellungen-Dialog hinzu. Dies ist optional - Sie können auch Bilder direkt per Drag & Drop hochladen.",
"errorNoUrl": "Bitte geben Sie eine URL ein",
"errorCapture": "Screenshot konnte nicht erfasst werden",
"errorCaptureDetails": "Screenshot konnte nicht erfasst werden. Schauen Sie in die Konsole und Ihre Backend-Logs für weitere Details.",
"orScreenshot": "Oder Screenshot einer URL...",
"enterUrl": "URL eingeben",
"capturing": "Erfassung läuft...",
"capture": "Erfassen"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "Screenshot to Code",
"errors": {
"emptyInstruction": "Please include some instructions for AI on what to update.",
"noCurrentVersion": "No current version set. Contact support or open a Github issue.",
"invalidHistory": "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
}
},
"history": {
"versions": "Versions",
"toggle": "Toggle",
"fullPrompt": "Full prompt",
"create": "Create",
"edit": "Edit",
"importedFromCode": "Imported from code"
},
"messages": {
"deprecationMessage": "We no longer support this model. Instead, code generation will use GPT-4o or Claude Sonnet 3.5, the 2 state-of-the-art models.",
"onboardingNote": {
"intro": "To use Screenshot to Code,",
"buyCredits": "buy some credits (100 generations for $36)",
"orUseKey": "or use your own OpenAI API key with GPT4 vision access.",
"followInstructions": "Follow these instructions to get yourself a key.",
"pasteKey": "and paste it in the Settings dialog (gear icon above). Your key is only stored in your browser. Never stored on our servers."
},
"picoBadge": {
"featureRequests": "feature requests?",
"openSourceProject": "an open source project by Pico"
},
"tipLink": {
"text": "Tips for better results"
}
},
"preview": {
"codeTab": {
"copyCode": "Copy Code",
"openIn": "Open in",
"codeCopied": "Copied to clipboard"
},
"pane": {
"reset": "Reset",
"download": "Download",
"desktop": "Desktop",
"mobile": "Mobile",
"code": "Code"
}
},
"recording": {
"start": "Record Screen",
"inProgress": "Recording...",
"finish": "Finish Recording",
"captured": "Screen Recording Captured.",
"reRecord": "Re-record",
"generate": "Generate",
"errorStarting": "Could not start screen recording",
"noDataError": "Screen recording does not exist. Please try again."
},
"sidebar": {
"videoGenerationWarning": "Code generation from videos can take 3-4 minutes. We do multiple passes to get the best result. Please be patient.",
"cancel": "Cancel",
"updateInstructionPlaceholder": "Tell the AI what to change...",
"includeScreenshot": "Include screenshot of current version?",
"update": "Update",
"regenerate": "Regenerate",
"referenceImageAlt": "Reference",
"originalVideo": "Original Video",
"originalScreenshot": "Original Screenshot",
"console": "Console"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "Tell the AI what to change about this element...",
"update": "Update"
},
"selectAndEdit": {
"exitMode": "Exit selection mode",
"enterMode": "Select and update"
}
},
"setting": {
"title": "Settings",
"dallePlaceholder": "DALL-E Placeholder Image Generation",
"dallePlaceholderDescription": "More fun with it but if you want to save money, turn it off.",
"openAIApiKey": "OpenAI API key",
"apiKeyDescription": "Only stored in your browser. Never stored on servers. Overrides your .env config.",
"openAIApiKeyPlaceholder": "OpenAI API key",
"openAIBaseURL": "OpenAI Base URL (optional)",
"openAIBaseURLDescription": "Replace with a proxy URL if you don't want to use the default.",
"openAIBaseURLPlaceholder": "OpenAI Base URL",
"anthropicApiKey": "Anthropic API key",
"anthropicApiKeyPlaceholder": "Anthropic API key",
"language": "Language",
"screenshotConfig": "Screenshot by URL Config",
"screenshotDescription": "If you want to use URLs directly instead of taking the screenshot yourself, add a ScreenshotOne API key.",
"getFreeScreenshots": "Get 100 screenshots/mo for free.",
"screenshotApiKeyPlaceholder": "ScreenshotOne API key",
"themeSettings": "Theme Settings",
"appTheme": "App Theme",
"toggleDarkMode": "Toggle dark mode",
"codeEditorTheme": "Code Editor Theme - requires page refresh to update",
"save": "Save",
"aiModel": "AI Model:",
"beta": "Beta",
"generating": "Generating:"
},
"imageUpload": {
"dragDropPrompt": "Drag & drop a screenshot here, \n or click to upload",
"new": "New!",
"uploadPrompt": "Upload a screen recording (.mp4, .mov) or record your screen to clone a whole app (experimental).",
"learnMore": "Learn more.",
"errorReading": "Error reading files: "
},
"importCode": {
"buttonText": "Import from Code",
"dialogTitle": "Paste in your HTML code",
"dialogDescription": "Make sure that the code you're importing is valid HTML.",
"textareaPlaceholder": "Paste your HTML code here...",
"stackLabel": "Stack:",
"importButton": "Import",
"errorEmptyCode": "Please paste in some code",
"errorNoStack": "Please select your stack"
},
"termsOfService": {
"title": "Enter your email to get started",
"emailPlaceholder": "Email",
"consentText": "By providing your email, you consent to receiving occasional product updates, and you accept the",
"termsLink": "terms of service",
"localRunText": "Prefer to run it yourself locally? This project is open source.",
"githubLink": "Download the code and get started on Github.",
"emailError": "Please enter your email",
"agreeButton": "Agree & Continue",
"usageText": "Designers and engineers from these organizations use Screenshot to Code to build interfaces faster."
},
"urlInput": {
"errorNoApiKey": "Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
"errorNoUrl": "Please enter a URL",
"errorCapture": "Failed to capture screenshot",
"errorCaptureDetails": "Failed to capture screenshot. Look at the console and your backend logs for more details.",
"orScreenshot": "Or screenshot a URL...",
"enterUrl": "Enter URL",
"capturing": "Capturing...",
"capture": "Capture"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "Screenshot vers Code",
"errors": {
"emptyInstruction": "Veuillez inclure des instructions pour l'IA sur ce qu'il faut mettre à jour.",
"noCurrentVersion": "Aucune version actuelle définie. Contactez le support ou ouvrez un problème sur Github.",
"invalidHistory": "L'historique des versions est invalide. Cela ne devrait pas se produire. Veuillez contacter le support ou ouvrir un problème sur Github."
}
},
"history": {
"versions": "Versions",
"toggle": "Basculer",
"fullPrompt": "Prompt complet",
"create": "Créer",
"edit": "Modifier",
"importedFromCode": "Importé du code"
},
"messages": {
"deprecationMessage": "Nous ne prenons plus en charge ce modèle. À la place, la génération de code utilisera GPT-4o ou Claude Sonnet 3.5, les 2 modèles de pointe.",
"onboardingNote": {
"intro": "Pour utiliser Screenshot vers Code,",
"buyCredits": "achetez des crédits (100 générations pour 36$)",
"orUseKey": "ou utilisez votre propre clé API OpenAI avec accès à GPT4 vision.",
"followInstructions": "Suivez ces instructions pour obtenir une clé.",
"pasteKey": "et collez-la dans la boîte de dialogue des paramètres (icône d'engrenage ci-dessus). Votre clé est uniquement stockée dans votre navigateur. Jamais stockée sur nos serveurs."
},
"picoBadge": {
"featureRequests": "demandes de fonctionnalités ?",
"openSourceProject": "un projet open source par Pico"
},
"tipLink": {
"text": "Astuces pour de meilleurs résultats"
}
},
"preview": {
"codeTab": {
"copyCode": "Copier le code",
"openIn": "Ouvrir dans",
"codeCopied": "Copié dans le presse-papiers"
},
"pane": {
"reset": "Réinitialiser",
"download": "Télécharger",
"desktop": "Bureau",
"mobile": "Mobile",
"code": "Code"
}
},
"recording": {
"start": "Enregistrer l'écran",
"inProgress": "Enregistrement...",
"finish": "Terminer l'enregistrement",
"captured": "Enregistrement d'écran capturé.",
"reRecord": "Réenregistrer",
"generate": "Générer",
"errorStarting": "Impossible de démarrer l'enregistrement d'écran",
"noDataError": "L'enregistrement d'écran n'existe pas. Veuillez réessayer."
},
"sidebar": {
"videoGenerationWarning": "La génération de code à partir de vidéos peut prendre 3 à 4 minutes. Nous effectuons plusieurs passages pour obtenir le meilleur résultat. Veuillez patienter.",
"cancel": "Annuler",
"updateInstructionPlaceholder": "Dites à l'IA ce qu'il faut changer...",
"includeScreenshot": "Inclure une capture d'écran de la version actuelle ?",
"update": "Mettre à jour",
"regenerate": "Régénérer",
"referenceImageAlt": "Référence",
"originalVideo": "Vidéo originale",
"originalScreenshot": "Capture d'écran originale",
"console": "Console"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "Dites à l'IA ce qu'il faut changer à propos de cet élément...",
"update": "Mettre à jour"
},
"selectAndEdit": {
"exitMode": "Quitter le mode de sélection",
"enterMode": "Sélectionner et mettre à jour"
}
},
"setting": {
"title": "Paramètres",
"dallePlaceholder": "Génération d'images de remplacement DALL-E",
"dallePlaceholderDescription": "Plus amusant avec, mais si vous voulez économiser de l'argent, désactivez-le.",
"openAIApiKey": "Clé API OpenAI",
"apiKeyDescription": "Stockée uniquement dans votre navigateur. Jamais stockée sur les serveurs. Remplace votre configuration .env.",
"openAIApiKeyPlaceholder": "Clé API OpenAI",
"openAIBaseURL": "URL de base OpenAI (optionnel)",
"openAIBaseURLDescription": "Remplacez par une URL proxy si vous ne voulez pas utiliser la valeur par défaut.",
"openAIBaseURLPlaceholder": "URL de base OpenAI",
"anthropicApiKey": "Clé API Anthropic",
"anthropicApiKeyPlaceholder": "Clé API Anthropic",
"language": "Langue",
"screenshotConfig": "Configuration de capture d'écran par URL",
"screenshotDescription": "Si vous voulez utiliser directement des URLs au lieu de prendre vous-même la capture d'écran, ajoutez une clé API ScreenshotOne.",
"getFreeScreenshots": "Obtenez 100 captures d'écran/mois gratuitement.",
"screenshotApiKeyPlaceholder": "Clé API ScreenshotOne",
"themeSettings": "Paramètres du thème",
"appTheme": "Thème de l'application",
"toggleDarkMode": "Basculer en mode sombre",
"codeEditorTheme": "Thème de l'éditeur de code - nécessite un rafraîchissement de la page pour mettre à jour",
"save": "Enregistrer",
"aiModel": "Modèle d'IA :",
"beta": "Bêta",
"generating": "Génération :"
},
"imageUpload": {
"dragDropPrompt": "Glissez et déposez une capture d'écran ici, \n ou cliquez pour télécharger",
"new": "Nouveau !",
"uploadPrompt": "Téléchargez un enregistrement d'écran (.mp4, .mov) ou enregistrez votre écran pour cloner une application entière (expérimental).",
"learnMore": "En savoir plus.",
"errorReading": "Erreur de lecture des fichiers : "
},
"importCode": {
"buttonText": "Importer depuis le code",
"dialogTitle": "Collez votre code HTML",
"dialogDescription": "Assurez-vous que le code que vous importez est du HTML valide.",
"textareaPlaceholder": "Collez votre code HTML ici...",
"stackLabel": "Stack :",
"importButton": "Importer",
"errorEmptyCode": "Veuillez coller du code",
"errorNoStack": "Veuillez sélectionner votre stack"
},
"termsOfService": {
"title": "Entrez votre e-mail pour commencer",
"emailPlaceholder": "E-mail",
"consentText": "En fournissant votre e-mail, vous consentez à recevoir des mises à jour occasionnelles du produit et vous acceptez les",
"termsLink": "conditions d'utilisation",
"localRunText": "Préférez-vous l'exécuter localement ? Ce projet est open source.",
"githubLink": "Téléchargez le code et commencez sur Github.",
"emailError": "Veuillez entrer votre e-mail",
"agreeButton": "Accepter et continuer",
"usageText": "Les designers et ingénieurs de ces organisations utilisent Screenshot vers Code pour construire des interfaces plus rapidement."
},
"urlInput": {
"errorNoApiKey": "Veuillez ajouter une clé API ScreenshotOne dans la boîte de dialogue des paramètres. C'est optionnel - vous pouvez également glisser-déposer et télécharger des images directement.",
"errorNoUrl": "Veuillez entrer une URL",
"errorCapture": "Échec de la capture d'écran",
"errorCaptureDetails": "Échec de la capture d'écran. Consultez la console et vos logs backend pour plus de détails.",
"orScreenshot": "Ou capture d'écran d'une URL...",
"enterUrl": "Entrer l'URL",
"capturing": "Capture en cours...",
"capture": "Capturer"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "スクリーンショットからコードへ",
"errors": {
"emptyInstruction": "AIに更新内容を指示してください。",
"noCurrentVersion": "現在のバージョンが設定されていません。サポートに連絡するかGithubでイシューを開いてください。",
"invalidHistory": "バージョン履歴が無効です。これは起こるべきではありません。サポートに連絡するかGithubでイシューを開いてください。"
}
},
"history": {
"versions": "バージョン",
"toggle": "切り替え",
"fullPrompt": "完全なプロンプト",
"create": "作成",
"edit": "編集",
"importedFromCode": "コードからインポート"
},
"messages": {
"deprecationMessage": "このモデルはもうサポートしていません。代わりに、コード生成にはGPT-4oまたはClaude Sonnet 3.5という最先端の2つのモデルを使用します。",
"onboardingNote": {
"intro": "Screenshot to Codeを使用するには、",
"buyCredits": "クレジットを購入するか100回の生成で36ドル",
"orUseKey": "またはGPT4ビジョンアクセス付きの自分のOpenAI APIキーを使用してください。",
"followInstructions": "キーを取得するにはこれらの指示に従ってください。",
"pasteKey": "そして設定ダイアログ(上部の歯車アイコン)にペーストしてください。キーはブラウザにのみ保存され、サーバーには決して保存されません。"
},
"picoBadge": {
"featureRequests": "機能リクエスト?",
"openSourceProject": "Picoによるオープンソースプロジェクト"
},
"tipLink": {
"text": "より良い結果を得るためのヒント"
}
},
"preview": {
"codeTab": {
"copyCode": "コードをコピー",
"openIn": "で開く",
"codeCopied": "クリップボードにコピーしました"
},
"pane": {
"reset": "リセット",
"download": "ダウンロード",
"desktop": "デスクトップ",
"mobile": "モバイル",
"code": "コード"
}
},
"recording": {
"start": "画面録画",
"inProgress": "録画中...",
"finish": "録画終了",
"captured": "画面録画が完了しました。",
"reRecord": "再録画",
"generate": "生成",
"errorStarting": "画面録画を開始できませんでした",
"noDataError": "画面録画が存在しません。もう一度お試しください。"
},
"sidebar": {
"videoGenerationWarning": "ビデオからのコード生成には3〜4分かかる場合があります。最良の結果を得るために複数回のパスを行います。お待ちください。",
"cancel": "キャンセル",
"updateInstructionPlaceholder": "AIに変更内容を指示してください...",
"includeScreenshot": "現在のバージョンのスクリーンショットを含めますか?",
"update": "更新",
"regenerate": "再生成",
"referenceImageAlt": "参照",
"originalVideo": "元のビデオ",
"originalScreenshot": "元のスクリーンショット",
"console": "コンソール"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "この要素について変更したい内容をAIに指示してください...",
"update": "更新"
},
"selectAndEdit": {
"exitMode": "選択モードを終了",
"enterMode": "選択して更新"
}
},
"setting": {
"title": "設定",
"dallePlaceholder": "DALL-Eプレースホルダー画像生成",
"dallePlaceholderDescription": "オンにすると楽しいですが、お金を節約したい場合はオフにしてください。",
"openAIApiKey": "OpenAI APIキー",
"apiKeyDescription": "ブラウザにのみ保存されます。サーバーには決して保存されません。.env設定よりも優先されます。",
"openAIApiKeyPlaceholder": "OpenAI APIキー",
"openAIBaseURL": "OpenAIベースURLオプション",
"openAIBaseURLDescription": "デフォルトを使用したくない場合は、プロキシURLに置き換えてください。",
"openAIBaseURLPlaceholder": "OpenAIベースURL",
"anthropicApiKey": "Anthropic APIキー",
"anthropicApiKeyPlaceholder": "Anthropic APIキー",
"language": "言語",
"screenshotConfig": "URLによるスクリーンショット設定",
"screenshotDescription": "スクリーンショットを自分で撮る代わりにURLを直接使用したい場合は、ScreenshotOne APIキーを追加してください。",
"getFreeScreenshots": "月100枚のスクリーンショットを無料で取得。",
"screenshotApiKeyPlaceholder": "ScreenshotOne APIキー",
"themeSettings": "テーマ設定",
"appTheme": "アプリテーマ",
"toggleDarkMode": "ダークモード切り替え",
"codeEditorTheme": "コードエディターテーマ - 更新にはページの再読み込みが必要です",
"save": "保存",
"aiModel": "AIモデル",
"beta": "ベータ",
"generating": "生成中:"
},
"imageUpload": {
"dragDropPrompt": "スクリーンショットをここにドラッグ&ドロップするか、\nクリックしてアップロードしてください",
"new": "新機能!",
"uploadPrompt": "画面録画(.mp4、.movをアップロードするか、画面を録画してアプリ全体をクローンします実験的。",
"learnMore": "詳細はこちら。",
"errorReading": "ファイルの読み込みエラー: "
},
"importCode": {
"buttonText": "コードからインポート",
"dialogTitle": "HTMLコードを貼り付けてください",
"dialogDescription": "インポートするコードが有効なHTMLであることを確認してください。",
"textareaPlaceholder": "ここにHTMLコードを貼り付けてください...",
"stackLabel": "スタック:",
"importButton": "インポート",
"errorEmptyCode": "コードを貼り付けてください",
"errorNoStack": "スタックを選択してください"
},
"termsOfService": {
"title": "開始するにはメールアドレスを入力してください",
"emailPlaceholder": "メールアドレス",
"consentText": "メールアドレスを提供することで、製品更新の不定期な受信に同意し、",
"termsLink": "利用規約",
"localRunText": "ローカルで実行することを希望しますか?このプロジェクトはオープンソースです。",
"githubLink": "Githubでコードをダウンロードして始めましょう。",
"emailError": "メールアドレスを入力してください",
"agreeButton": "同意して続行",
"usageText": "これらの組織のデザイナーとエンジニアは、インターフェースをより速く構築するためにScreenshot to Codeを使用しています。"
},
"urlInput": {
"errorNoApiKey": "設定ダイアログでScreenshotOne APIキーを追加してください。これはオプションです - 画像を直接ドラッグ&ドロップしてアップロードすることもできます。",
"errorNoUrl": "URLを入力してください",
"errorCapture": "スクリーンショットの取得に失敗しました",
"errorCaptureDetails": "スクリーンショットの取得に失敗しました。詳細については、コンソールとバックエンドログを確認してください。",
"orScreenshot": "またはURLのスクリーンショットを撮影...",
"enterUrl": "URLを入力",
"capturing": "取得中...",
"capture": "取得"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "스크린샷에서 코드로",
"errors": {
"emptyInstruction": "AI에게 업데이트할 내용에 대한 지시사항을 포함해 주세요.",
"noCurrentVersion": "현재 버전이 설정되지 않았습니다. 지원팀에 문의하거나 Github 이슈를 열어주세요.",
"invalidHistory": "버전 기록이 유효하지 않습니다. 이런 일은 일어나지 않아야 합니다. 지원팀에 문의하거나 Github 이슈를 열어주세요."
}
},
"history": {
"versions": "버전",
"toggle": "토글",
"fullPrompt": "전체 프롬프트",
"create": "생성",
"edit": "편집",
"importedFromCode": "코드에서 가져옴"
},
"messages": {
"deprecationMessage": "우리는 더 이상 이 모델을 지원하지 않습니다. 대신, 코드 생성은 GPT-4o 또는 Claude Sonnet 3.5, 최첨단 모델 2개를 사용할 것입니다.",
"onboardingNote": {
"intro": "Screenshot to Code를 사용하려면,",
"buyCredits": "크레딧을 구매하거나 (100회 생성에 $36)",
"orUseKey": "또는 GPT4 비전 액세스가 있는 자신의 OpenAI API 키를 사용하세요.",
"followInstructions": "키를 얻으려면 이 지침을 따르세요.",
"pasteKey": "그리고 설정 대화상자(위의 기어 아이콘)에 붙여넣으세요. 키는 브라우저에만 저장됩니다. 우리 서버에는 절대 저장되지 않습니다."
},
"picoBadge": {
"featureRequests": "기능 요청?",
"openSourceProject": "Pico의 오픈 소스 프로젝트"
},
"tipLink": {
"text": "더 나은 결과를 위한 팁"
}
},
"preview": {
"codeTab": {
"copyCode": "코드 복사",
"openIn": "열기",
"codeCopied": "클립보드에 복사됨"
},
"pane": {
"reset": "초기화",
"download": "다운로드",
"desktop": "데스크톱",
"mobile": "모바일",
"code": "코드"
}
},
"recording": {
"start": "화면 녹화",
"inProgress": "녹화 중...",
"finish": "녹화 종료",
"captured": "화면 녹화가 캡처되었습니다.",
"reRecord": "다시 녹화",
"generate": "생성",
"errorStarting": "화면 녹화를 시작할 수 없습니다",
"noDataError": "화면 녹화가 존재하지 않습니다. 다시 시도해 주세요."
},
"sidebar": {
"videoGenerationWarning": "비디오에서 코드 생성은 3-4분이 걸릴 수 있습니다. 최상의 결과를 얻기 위해 여러 번의 패스를 수행합니다. 기다려 주세요.",
"cancel": "취소",
"updateInstructionPlaceholder": "AI에게 변경할 내용을 알려주세요...",
"includeScreenshot": "현재 버전의 스크린샷을 포함하시겠습니까?",
"update": "업데이트",
"regenerate": "재생성",
"referenceImageAlt": "참조",
"originalVideo": "원본 비디오",
"originalScreenshot": "원본 스크린샷",
"console": "콘솔"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "AI에게 이 요소에 대해 변경할 내용을 알려주세요...",
"update": "업데이트"
},
"selectAndEdit": {
"exitMode": "선택 모드 종료",
"enterMode": "선택 및 업데이트"
}
},
"setting": {
"title": "설정",
"dallePlaceholder": "DALL-E 플레이스홀더 이미지 생성",
"dallePlaceholderDescription": "켜면 더 재미있지만 돈을 절약하고 싶다면 끄세요.",
"openAIApiKey": "OpenAI API 키",
"apiKeyDescription": "브라우저에만 저장됩니다. 서버에는 절대 저장되지 않습니다. .env 설정을 덮어씁니다.",
"openAIApiKeyPlaceholder": "OpenAI API 키",
"openAIBaseURL": "OpenAI 기본 URL (선택사항)",
"openAIBaseURLDescription": "기본값을 사용하지 않으려면 프록시 URL로 대체하세요.",
"openAIBaseURLPlaceholder": "OpenAI 기본 URL",
"anthropicApiKey": "Anthropic API 키",
"anthropicApiKeyPlaceholder": "Anthropic API 키",
"language": "언어",
"screenshotConfig": "URL로 스크린샷 설정",
"screenshotDescription": "직접 스크린샷을 찍는 대신 URL을 직접 사용하려면 ScreenshotOne API 키를 추가하세요.",
"getFreeScreenshots": "월 100장의 무료 스크린샷을 받으세요.",
"screenshotApiKeyPlaceholder": "ScreenshotOne API 키",
"themeSettings": "테마 설정",
"appTheme": "앱 테마",
"toggleDarkMode": "다크 모드 전환",
"codeEditorTheme": "코드 에디터 테마 - 업데이트하려면 페이지 새로고침 필요",
"save": "저장",
"aiModel": "AI 모델:",
"beta": "베타",
"generating": "생성 중:"
},
"imageUpload": {
"dragDropPrompt": "스크린샷을 여기에 드래그 앤 드롭하거나,\n클릭하여 업로드하세요",
"new": "새로운!",
"uploadPrompt": "화면 녹화 (.mp4, .mov)를 업로드하거나 화면을 녹화하여 전체 앱을 복제하세요 (실험적).",
"learnMore": "자세히 알아보기.",
"errorReading": "파일 읽기 오류: "
},
"importCode": {
"buttonText": "코드에서 가져오기",
"dialogTitle": "HTML 코드를 붙여넣으세요",
"dialogDescription": "가져오려는 코드가 유효한 HTML인지 확인하세요.",
"textareaPlaceholder": "여기에 HTML 코드를 붙여넣으세요...",
"stackLabel": "스택:",
"importButton": "가져오기",
"errorEmptyCode": "코드를 붙여넣어 주세요",
"errorNoStack": "스택을 선택해 주세요"
},
"termsOfService": {
"title": "시작하려면 이메일을 입력하세요",
"emailPlaceholder": "이메일",
"consentText": "이메일을 제공함으로써, 가끔의 제품 업데이트 수신에 동의하며,",
"termsLink": "서비스 약관",
"localRunText": "로컬에서 직접 실행하고 싶으신가요? 이 프로젝트는 오픈 소스입니다.",
"githubLink": "Github에서 코드를 다운로드하고 시작하세요.",
"emailError": "이메일을 입력해 주세요",
"agreeButton": "동의 및 계속",
"usageText": "이 조직들의 디자이너와 엔지니어들이 Screenshot to Code를 사용하여 더 빠르게 인터페이스를 구축합니다."
},
"urlInput": {
"errorNoApiKey": "설정 대화상자에 ScreenshotOne API 키를 추가해 주세요. 이는 선택사항입니다 - 이미지를 직접 드래그/드롭하여 업로드할 수도 있습니다.",
"errorNoUrl": "URL을 입력해 주세요",
"errorCapture": "스크린샷 캡처 실패",
"errorCaptureDetails": "스크린샷 캡처에 실패했습니다. 자세한 내용은 콘솔과 백엔드 로그를 확인하세요.",
"orScreenshot": "또는 URL의 스크린샷 찍기...",
"enterUrl": "URL 입력",
"capturing": "캡처 중...",
"capture": "캡처"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "Скриншот в код",
"errors": {
"emptyInstruction": "Пожалуйста, включите инструкции для ИИ о том, что обновить.",
"noCurrentVersion": "Текущая версия не установлена. Свяжитесь с поддержкой или откройте проблему на Github.",
"invalidHistory": "История версий недействительна. Этого не должно происходить. Пожалуйста, свяжитесь с поддержкой или откройте проблему на Github."
}
},
"history": {
"versions": "Версии",
"toggle": "Переключить",
"fullPrompt": "Полный запрос",
"create": "Создать",
"edit": "Редактировать",
"importedFromCode": "Импортировано из кода"
},
"messages": {
"deprecationMessage": "Мы больше не поддерживаем эту модель. Вместо этого генерация кода будет использовать GPT-4o или Claude Sonnet 3.5, две самые современные модели.",
"onboardingNote": {
"intro": "Чтобы использовать Screenshot to Code,",
"buyCredits": "купите кредиты (100 генераций за $36)",
"orUseKey": "или используйте свой собственный ключ API OpenAI с доступом к GPT4 vision.",
"followInstructions": "Следуйте этим инструкциям, чтобы получить ключ.",
"pasteKey": "и вставьте его в диалог настроек (значок шестеренки вверху). Ваш ключ хранится только в вашем браузере. Никогда не хранится на наших серверах."
},
"picoBadge": {
"featureRequests": "запросы функций?",
"openSourceProject": "проект с открытым исходным кодом от Pico"
},
"tipLink": {
"text": "Советы для лучших результатов"
}
},
"preview": {
"codeTab": {
"copyCode": "Копировать код",
"openIn": "Открыть в",
"codeCopied": "Скопировано в буфер обмена"
},
"pane": {
"reset": "Сбросить",
"download": "Скачать",
"desktop": "Десктоп",
"mobile": "Мобильный",
"code": "Код"
}
},
"recording": {
"start": "Записать экран",
"inProgress": "Запись...",
"finish": "Завершить запись",
"captured": "Запись экрана захвачена.",
"reRecord": "Перезаписать",
"generate": "Сгенерировать",
"errorStarting": "Не удалось начать запись экрана",
"noDataError": "Запись экрана не существует. Пожалуйста, попробуйте еще раз."
},
"sidebar": {
"videoGenerationWarning": "Генерация кода из видео может занять 3-4 минуты. Мы делаем несколько проходов для получения наилучшего результата. Пожалуйста, будьте терпеливы.",
"cancel": "Отмена",
"updateInstructionPlaceholder": "Скажите ИИ, что изменить...",
"includeScreenshot": "Включить скриншот текущей версии?",
"update": "Обновить",
"regenerate": "Перегенерировать",
"referenceImageAlt": "Ссылка",
"originalVideo": "Оригинальное видео",
"originalScreenshot": "Оригинальный скриншот",
"console": "Консоль"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "Скажите ИИ, что изменить в этом элементе...",
"update": "Обновить"
},
"selectAndEdit": {
"exitMode": "Выйти из режима выбора",
"enterMode": "Выбрать и обновить"
}
},
"setting": {
"title": "Настройки",
"dallePlaceholder": "Генерация заполнителей изображений DALL-E",
"dallePlaceholderDescription": "С ней веселее, но если хотите сэкономить деньги, выключите.",
"openAIApiKey": "Ключ API OpenAI",
"apiKeyDescription": "Хранится только в вашем браузере. Никогда не хранится на серверах. Переопределяет вашу конфигурацию .env.",
"openAIApiKeyPlaceholder": "Ключ API OpenAI",
"openAIBaseURL": "Базовый URL OpenAI (необязательно)",
"openAIBaseURLDescription": "Замените прокси-URL, если не хотите использовать по умолчанию.",
"openAIBaseURLPlaceholder": "Базовый URL OpenAI",
"anthropicApiKey": "Ключ API Anthropic",
"anthropicApiKeyPlaceholder": "Ключ API Anthropic",
"language": "Язык",
"screenshotConfig": "Конфигурация скриншота по URL",
"screenshotDescription": "Если вы хотите использовать URL напрямую вместо самостоятельного создания скриншотов, добавьте ключ API ScreenshotOne.",
"getFreeScreenshots": "Получите 100 скриншотов/мес бесплатно.",
"screenshotApiKeyPlaceholder": "Ключ API ScreenshotOne",
"themeSettings": "Настройки темы",
"appTheme": "Тема приложения",
"toggleDarkMode": "Переключить темный режим",
"codeEditorTheme": "Тема редактора кода - требуется обновление страницы для обновления",
"save": "Сохранить",
"aiModel": "Модель ИИ:",
"beta": "Бета",
"generating": "Генерация:"
},
"imageUpload": {
"dragDropPrompt": "Перетащите скриншот сюда, \n или нажмите для загрузки",
"new": "Новое!",
"uploadPrompt": "Загрузите запись экрана (.mp4, .mov) или запишите свой экран, чтобы клонировать целое приложение (экспериментально).",
"learnMore": "Узнать больше.",
"errorReading": "Ошибка чтения файлов: "
},
"importCode": {
"buttonText": "Импорт из кода",
"dialogTitle": "Вставьте ваш HTML-код",
"dialogDescription": "Убедитесь, что импортируемый код является валидным HTML.",
"textareaPlaceholder": "Вставьте ваш HTML-код здесь...",
"stackLabel": "Стек:",
"importButton": "Импорт",
"errorEmptyCode": "Пожалуйста, вставьте код",
"errorNoStack": "Пожалуйста, выберите ваш стек"
},
"termsOfService": {
"title": "Введите свой email, чтобы начать",
"emailPlaceholder": "Email",
"consentText": "Предоставляя свой email, вы соглашаетесь получать периодические обновления продукта и принимаете",
"termsLink": "условия использования",
"localRunText": "Предпочитаете запускать локально? Этот проект с открытым исходным кодом.",
"githubLink": "Скачайте код и начните на Github.",
"emailError": "Пожалуйста, введите ваш email",
"agreeButton": "Согласиться и продолжить",
"usageText": "Дизайнеры и инженеры из этих организаций используют Screenshot to Code для более быстрого создания интерфейсов."
},
"urlInput": {
"errorNoApiKey": "Пожалуйста, добавьте ключ API ScreenshotOne в диалог настроек. Это необязательно - вы также можете напрямую перетаскивать и загружать изображения.",
"errorNoUrl": "Пожалуйста, введите URL",
"errorCapture": "Не удалось захватить скриншот",
"errorCaptureDetails": "Не удалось захватить скриншот. Проверьте консоль и журналы бэкенда для получения дополнительной информации.",
"orScreenshot": "Или скриншот URL...",
"enterUrl": "Введите URL",
"capturing": "Захват...",
"capture": "Захватить"
}
}

View File

@ -0,0 +1,145 @@
{
"app": {
"title": "截图转代码",
"errors": {
"emptyInstruction": "请为AI提供一些关于更新内容的指示。",
"noCurrentVersion": "未设置当前版本。请联系支持或在Github上开issue。",
"invalidHistory": "版本历史无效。这不应该发生。请联系支持或在Github上开issue。"
}
},
"history": {
"versions": "版本",
"toggle": "切换",
"fullPrompt": "完整提示",
"create": "创建",
"edit": "编辑",
"importedFromCode": "从代码导入"
},
"messages": {
"deprecationMessage": "我们不再支持此模型。相反,代码生成将使用 GPT-4o 或 Claude Sonnet 3.5,这两个是最先进的模型。",
"onboardingNote": {
"intro": "要使用截图转代码功能,",
"buyCredits": "购买一些积分36美元100次生成",
"orUseKey": "或使用您自己的OpenAI API密钥需要GPT4 vision访问权限。",
"followInstructions": "按照这些说明获取密钥。",
"pasteKey": "然后将其粘贴到设置对话框中(上方的齿轮图标)。您的密钥仅存储在您的浏览器中,绝不会存储在我们的服务器上。"
},
"picoBadge": {
"featureRequests": "功能请求?",
"openSourceProject": "Pico 的开源项目"
},
"tipLink": {
"text": "获取更好结果的提示"
}
},
"preview": {
"codeTab": {
"copyCode": "复制代码",
"openIn": "在以下打开",
"codeCopied": "已复制到剪贴板"
},
"pane": {
"reset": "重置",
"download": "下载",
"desktop": "桌面",
"mobile": "移动",
"code": "代码"
}
},
"recording": {
"start": "录制屏幕",
"inProgress": "录制中...",
"finish": "结束录制",
"captured": "屏幕录制已完成。",
"reRecord": "重新录制",
"generate": "生成",
"errorStarting": "无法开始屏幕录制",
"noDataError": "屏幕录制不存在。请重试。"
},
"sidebar": {
"videoGenerationWarning": "从视频生成代码可能需要3-4分钟。我们会进行多次处理以获得最佳结果。请耐心等待。",
"cancel": "取消",
"updateInstructionPlaceholder": "告诉AI要改变什么...",
"includeScreenshot": "包含当前版本的截图?",
"update": "更新",
"regenerate": "重新生成",
"referenceImageAlt": "参考图",
"originalVideo": "原始视频",
"originalScreenshot": "原始截图",
"console": "控制台"
},
"selectAndEdit": {
"editPopup": {
"placeholder": "告诉 AI 要如何改变这个元素...",
"update": "更新"
},
"selectAndEdit": {
"exitMode": "退出选择模式",
"enterMode": "选择并更新"
}
},
"setting": {
"title": "设置",
"dallePlaceholder": "DALL-E 占位图像生成",
"dallePlaceholderDescription": "使用它会更有趣,但如果您想节省成本,可以关闭它。",
"openAIApiKey": "OpenAI API 密钥",
"apiKeyDescription": "仅存储在您的浏览器中。绝不存储在服务器上。覆盖您的 .env 配置。",
"openAIApiKeyPlaceholder": "OpenAI API 密钥",
"openAIBaseURL": "OpenAI 基础 URL可选",
"openAIBaseURLDescription": "如果您不想使用默认值,请替换为代理 URL。",
"openAIBaseURLPlaceholder": "OpenAI 基础 URL",
"anthropicApiKey": "Anthropic API 密钥",
"anthropicApiKeyPlaceholder": "Anthropic API 密钥",
"language": "语言",
"screenshotConfig": "URL 截图配置",
"screenshotDescription": "如果您想直接使用 URL 而不是自己截图,请添加 ScreenshotOne API 密钥。",
"getFreeScreenshots": "每月免费获取 100 张截图。",
"screenshotApiKeyPlaceholder": "ScreenshotOne API 密钥",
"themeSettings": "主题设置",
"appTheme": "应用主题",
"toggleDarkMode": "切换深色模式",
"codeEditorTheme": "代码编辑器主题 - 需要刷新页面才能更新",
"save": "保存",
"aiModel": "AI 模型:",
"beta": "测试版",
"generating": "生成:"
},
"imageUpload": {
"dragDropPrompt": "将截图拖放到此处,\n 或点击上传",
"new": "新功能!",
"uploadPrompt": "上传屏幕录像(.mp4.mov或录制您的屏幕以克隆整个应用实验性功能。",
"learnMore": "了解更多。",
"errorReading": "读取文件时出错:"
},
"importCode": {
"buttonText": "从代码导入",
"dialogTitle": "粘贴您的HTML代码",
"dialogDescription": "请确保您导入的代码是有效的HTML。",
"textareaPlaceholder": "在此粘贴您的HTML代码...",
"stackLabel": "技术栈:",
"importButton": "导入",
"errorEmptyCode": "请粘贴一些代码",
"errorNoStack": "请选择您的技术栈"
},
"termsOfService": {
"title": "请输入您的电子邮箱以开始使用",
"emailPlaceholder": "电子邮箱",
"consentText": "提供您的电子邮箱即表示您同意接收偶尔的产品更新,并且您接受",
"termsLink": "服务条款",
"localRunText": "想在本地运行?这个项目是开源的。",
"githubLink": "在 Github 上下载代码并开始使用。",
"emailError": "请输入您的电子邮箱",
"agreeButton": "同意并继续",
"usageText": "这些组织的设计师和工程师使用 Screenshot to Code 来更快地构建界面。"
},
"urlInput": {
"errorNoApiKey": "请在设置对话框中添加 ScreenshotOne API 密钥。这是可选的 - 您也可以直接拖放和上传图片。",
"errorNoUrl": "请输入 URL",
"errorCapture": "截图失败",
"errorCaptureDetails": "截图失败。请查看控制台和后端日志以获取更多详细信息。",
"orScreenshot": "或者截取 URL 的屏幕截图...",
"enterUrl": "输入 URL",
"capturing": "正在截图...",
"capture": "截图"
}
}

View File

@ -23,8 +23,11 @@ import DeprecationMessage from "./components/messages/DeprecationMessage";
import { GenerationSettings } from "./components/settings/GenerationSettings";
import StartPane from "./components/start-pane/StartPane";
import { takeScreenshot } from "./lib/takeScreenshot";
import { useTranslation } from 'react-i18next'; // 导入 useTranslation hook
function App() {
const { t } = useTranslation(); // 初始化 useTranslation hook
const {
// Inputs
inputMode,
@ -118,16 +121,14 @@ function App() {
const regenerate = () => {
if (currentVersion === null) {
toast.error(
"No current version set. Please open a Github issue as this shouldn't happen."
);
toast.error(t('app.errors.noCurrentVersion'));
return;
}
// Retrieve the previous command
const previousCommand = appHistory[currentVersion];
if (previousCommand.type !== "ai_create") {
toast.error("Only the first version can be regenerated.");
toast.error(t('app.errors.regenerateFirstVersion'));
return;
}
@ -189,9 +190,7 @@ function App() {
setAppHistory((prev) => {
// Validate parent version
if (parentVersion === null) {
toast.error(
"No parent version set. Contact support or open a Github issue."
);
toast.error(t('app.errors.noCurrentVersion'));
return prev;
}
@ -254,14 +253,12 @@ function App() {
selectedElement?: HTMLElement
) {
if (updateInstruction.trim() === "") {
toast.error("Please include some instructions for AI on what to update.");
toast.error(t('app.errors.emptyInstruction'));
return;
}
if (currentVersion === null) {
toast.error(
"No current version set. Contact support or open a Github issue."
);
toast.error(t('app.errors.noCurrentVersion'));
return;
}
@ -269,9 +266,7 @@ function App() {
try {
historyTree = extractHistoryTree(appHistory, currentVersion);
} catch {
toast.error(
"Version history is invalid. This shouldn't happen. Please contact support or open a Github issue."
);
toast.error(t('app.errors.invalidHistory'));
return;
}
@ -365,7 +360,7 @@ function App() {
<div className="flex grow flex-col gap-y-2 overflow-y-auto border-r border-gray-200 bg-white px-6 dark:bg-zinc-950 dark:text-white">
{/* Header with access to settings */}
<div className="flex items-center justify-between mt-10 mb-2">
<h1 className="text-2xl ">Screenshot to Code</h1>
<h1 className="text-2xl ">{t('app.title')}</h1>
<SettingsDialog settings={settings} setSettings={setSettings} />
</div>
@ -414,4 +409,4 @@ function App() {
);
}
export default App;
export default App;

View File

@ -7,6 +7,7 @@ import { URLS } from "../urls";
import { Badge } from "./ui/badge";
import ScreenRecorder from "./recording/ScreenRecorder";
import { ScreenRecorderState } from "../types";
import { useTranslation } from 'react-i18next';
const baseStyle = {
flex: 1,
@ -62,6 +63,7 @@ interface Props {
}
function ImageUpload({ setReferenceImages }: Props) {
const { t } = useTranslation();
const [files, setFiles] = useState<FileWithPreview[]>([]);
// TODO: Switch to Zustand
const [screenRecorderState, setScreenRecorderState] =
@ -109,7 +111,7 @@ function ImageUpload({ setReferenceImages }: Props) {
});
},
onDropRejected: (rejectedFiles) => {
toast.error(rejectedFiles[0].errors[0].message);
toast.error(t('imageUpload.errorReading') + rejectedFiles[0].errors[0].message);
},
});
@ -168,21 +170,19 @@ function ImageUpload({ setReferenceImages }: Props) {
<div {...getRootProps({ style: style as any })}>
<input {...getInputProps()} className="file-input" />
<p className="text-slate-700 text-lg">
Drag & drop a screenshot here, <br />
or click to upload
{t('imageUpload.dragDropPrompt')}
</p>
</div>
)}
{screenRecorderState === ScreenRecorderState.INITIAL && (
<div className="text-center text-sm text-slate-800 mt-4">
<Badge>New!</Badge> Upload a screen recording (.mp4, .mov) or record
your screen to clone a whole app (experimental).{" "}
<Badge>{t('imageUpload.new')}</Badge> {t('imageUpload.uploadPrompt')}{" "}{" "}
<a
className="underline"
href={URLS["intro-to-video"]}
target="_blank"
>
Learn more.
{t('imageUpload.learnMore')}
</a>
</div>
)}

View File

@ -13,23 +13,25 @@ import { Textarea } from "./ui/textarea";
import OutputSettingsSection from "./settings/OutputSettingsSection";
import toast from "react-hot-toast";
import { Stack } from "../lib/stacks";
import { useTranslation } from 'react-i18next';
interface Props {
importFromCode: (code: string, stack: Stack) => void;
}
function ImportCodeSection({ importFromCode }: Props) {
const { t } = useTranslation();
const [code, setCode] = useState("");
const [stack, setStack] = useState<Stack | undefined>(undefined);
const doImport = () => {
if (code === "") {
toast.error("Please paste in some code");
toast.error(t('importCode.errorEmptyCode'));
return;
}
if (stack === undefined) {
toast.error("Please select your stack");
toast.error(t('importCode.errorNoStack'));
return;
}
@ -39,14 +41,14 @@ function ImportCodeSection({ importFromCode }: Props) {
<Dialog>
<DialogTrigger asChild>
<Button className="import-from-code-btn" variant="secondary">
Import from Code
{t('importCode.buttonText')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Paste in your HTML code</DialogTitle>
<DialogTitle>{t('importCode.dialogTitle')}</DialogTitle>
<DialogDescription>
Make sure that the code you're importing is valid HTML.
{t('importCode.dialogDescription')}
</DialogDescription>
</DialogHeader>
@ -54,18 +56,19 @@ function ImportCodeSection({ importFromCode }: Props) {
value={code}
onChange={(e) => setCode(e.target.value)}
className="w-full h-64"
placeholder={t('importCode.textareaPlaceholder')}
/>
<OutputSettingsSection
stack={stack}
setStack={(config: Stack) => setStack(config)}
label="Stack:"
label={t('importCode.stackLabel')}
shouldDisableUpdates={false}
/>
<DialogFooter>
<Button className="import-btn" type="submit" onClick={doImport}>
Import
{t('importCode.importButton')}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -10,6 +10,7 @@ import {
import { Input } from "./ui/input";
import toast from "react-hot-toast";
import { PICO_BACKEND_FORM_SECRET } from "../config";
import { useTranslation } from 'react-i18next';
const LOGOS = ["microsoft", "amazon", "mit", "stanford", "bytedance", "baidu"];
@ -17,6 +18,7 @@ const TermsOfServiceDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const { t } = useTranslation();
const [email, setEmail] = React.useState("");
const onSubscribe = async () => {
@ -34,13 +36,13 @@ const TermsOfServiceDialog: React.FC<{
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="mb-2 text-xl">
Enter your email to get started
{t('termsOfService.title')}
</AlertDialogTitle>
</AlertDialogHeader>
<div className="mb-2">
<Input
placeholder="Email"
placeholder={t('termsOfService.emailPlaceholder')}
value={email}
onChange={(e) => {
setEmail(e.target.value);
@ -49,27 +51,26 @@ const TermsOfServiceDialog: React.FC<{
</div>
<div className="flex flex-col space-y-3 text-sm">
<p>
By providing your email, you consent to receiving occasional product
updates, and you accept the{" "}
{t('termsOfService.consentText')}{" "}
<a
href="https://a.picoapps.xyz/camera-write"
target="_blank"
className="underline"
>
terms of service
{t('termsOfService.termsLink')}
</a>
.{" "}
</p>
<p>
{" "}
Prefer to run it yourself locally? This project is open source.{" "}
{t('termsOfService.localRunText')}{" "}
<a
href="https://github.com/abi/screenshot-to-code"
target="_blank"
className="underline"
>
Download the code and get started on Github.
{t('termsOfService.githubLink')}
</a>
</p>
</div>
@ -79,13 +80,13 @@ const TermsOfServiceDialog: React.FC<{
onClick={(e) => {
if (!email.trim() || !email.trim().includes("@")) {
e.preventDefault();
toast.error("Please enter your email");
toast.error(t('termsOfService.emailError'));
} else {
onSubscribe();
}
}}
>
Agree & Continue
{t('termsOfService.agreeButton')}
</AlertDialogAction>
</AlertDialogFooter>
@ -107,8 +108,7 @@ const TermsOfServiceDialog: React.FC<{
))}
</div>
<div className="text-gray-500 text-xs mt-4 text-center">
Designers and engineers from these organizations use Screenshot to
Code to build interfaces faster.
{t('termsOfService.usageText')}
</div>
</div>
</AlertDialogContent>

View File

@ -3,6 +3,7 @@ import { HTTP_BACKEND_URL } from "../config";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { toast } from "react-hot-toast";
import { useTranslation } from 'react-i18next';
interface Props {
screenshotOneApiKey: string | null;
@ -10,20 +11,18 @@ interface Props {
}
export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [referenceUrl, setReferenceUrl] = useState("");
async function takeScreenshot() {
if (!screenshotOneApiKey) {
toast.error(
"Please add a ScreenshotOne API key in the Settings dialog. This is optional - you can also drag/drop and upload images directly.",
{ duration: 8000 }
);
toast.error(t('urlInput.errorNoApiKey'), { duration: 8000 });
return;
}
if (!referenceUrl) {
toast.error("Please enter a URL");
toast.error(t('urlInput.errorNoUrl'));
return;
}
@ -42,16 +41,14 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
});
if (!response.ok) {
throw new Error("Failed to capture screenshot");
throw new Error(t('urlInput.errorCapture'));
}
const res = await response.json();
doCreate([res.url], "image");
} catch (error) {
console.error(error);
toast.error(
"Failed to capture screenshot. Look at the console and your backend logs for more details."
);
toast.error(t('urlInput.errorCaptureDetails'));
} finally {
setIsLoading(false);
}
@ -60,9 +57,9 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
return (
<div className="max-w-[90%] min-w-[40%] gap-y-2 flex flex-col">
<div className="text-gray-500 text-sm">Or screenshot a URL...</div>
<div className="text-gray-500 text-sm">{t('urlInput.orScreenshot')}</div>
<Input
placeholder="Enter URL"
placeholder={t('urlInput.enterUrl')}
onChange={(e) => setReferenceUrl(e.target.value)}
value={referenceUrl}
/>
@ -71,7 +68,7 @@ export function UrlInputSection({ doCreate, screenshotOneApiKey }: Props) {
disabled={isLoading}
className="bg-slate-400 capture-btn"
>
{isLoading ? "Capturing..." : "Capture"}
{isLoading ? t('urlInput.capturing') : t('urlInput.capture')}
</Button>
</div>
);

View File

@ -11,6 +11,7 @@ import {
import { Button } from "../ui/button";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { useProjectStore } from "../../store/project-store";
import { useTranslation } from 'react-i18next';
interface Props {
shouldDisableReverts: boolean;
@ -24,6 +25,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
setGeneratedCode,
} = useProjectStore();
const renderedHistory = renderHistory(history, currentVersion);
const { t } = useTranslation();
const revertToVersion = (index: number) => {
if (index < 0 || index >= history.length || !history[index]) return;
@ -33,7 +35,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
return renderedHistory.length === 0 ? null : (
<div className="flex flex-col h-screen">
<h1 className="font-bold mb-2">Versions</h1>
<h1 className="font-bold mb-2">{t('history.versions')}</h1>
<ul className="space-y-0 flex flex-col-reverse">
{renderedHistory.map((item, index) => (
<li key={index}>
@ -59,7 +61,7 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
}
>
<div className="flex gap-x-1 truncate">
<h2 className="text-sm truncate">{item.summary}</h2>
<h2 className="text-sm truncate">{t(item.summary)}</h2>
{item.parentVersion !== null && (
<h2 className="text-sm">
(parent: {item.parentVersion})
@ -71,14 +73,14 @@ export default function HistoryDisplay({ shouldDisableReverts }: Props) {
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-6">
<CaretSortIcon className="h-4 w-4" />
<span className="sr-only">Toggle</span>
<span className="sr-only">{t('historytoggle')}</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="w-full bg-slate-300 p-2">
<div>Full prompt: {item.summary}</div>
<div>{t('history.fullPrompt')}: {t(item.summary)}</div>
<div className="flex justify-end">
<Badge>{item.type}</Badge>
<Badge>{t(item.type)}</Badge>
</div>
</CollapsibleContent>
</Collapsible>

View File

@ -39,11 +39,11 @@ export function extractHistoryTree(
function displayHistoryItemType(itemType: HistoryItemType) {
switch (itemType) {
case "ai_create":
return "Create";
return "history.create";
case "ai_edit":
return "Edit";
return "history.edit";
case "code_create":
return "Imported from code";
return "history.importedFromCode";
default: {
const exhaustiveCheck: never = itemType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);
@ -55,11 +55,11 @@ function summarizeHistoryItem(item: HistoryItem) {
const itemType = item.type;
switch (itemType) {
case "ai_create":
return "Create";
return "history.create";
case "ai_edit":
return item.inputs.prompt;
case "code_create":
return "Imported from code";
return "history.importedFromCode";
default: {
const exhaustiveCheck: never = itemType;
throw new Error(`Unhandled case: ${exhaustiveCheck}`);

View File

@ -0,0 +1,20 @@
.language-selector-container {
display: inline-block;
}
.language-selector {
color: white;
background-color: #333;
padding: 8px 32px 8px 12px;
font-size: 14px;
border: 1px solid #444;
border-radius: 4px;
cursor: pointer;
outline: none;
transition: all 0.3s ease;
}
.language-selector:hover, .language-selector:focus {
background-color: #444;
border-color: #555;
}

View File

@ -0,0 +1,46 @@
import React, { useState, useEffect } from 'react';
import i18n from '../../i18n';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
import './LanguageSelector.css';
type Languages = 'en' | 'zh' | 'ja' | 'ko' | 'de' | 'ru' | 'fr';
const LanguageSelector: React.FC = () => {
const [selectedLang, setSelectedLang] = useState<Languages>(i18n.language as Languages);
useEffect(() => {
const handleLanguageChanged = (lang: string) => {
setSelectedLang(lang as Languages);
};
i18n.on('languageChanged', handleLanguageChanged);
return () => {
i18n.off('languageChanged', handleLanguageChanged);
};
}, []);
const handleLanguageChange = (value: string) => {
const nextLang = value as Languages;
i18n.changeLanguage(nextLang);
};
return (
<Select onValueChange={handleLanguageChange} value={selectedLang}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Language" />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh"></SelectItem>
<SelectItem value="ja"></SelectItem>
<SelectItem value="ko"></SelectItem>
<SelectItem value="de">Deutsch</SelectItem>
<SelectItem value="ru">Русский</SelectItem>
<SelectItem value="fr">Français</SelectItem>
</SelectContent>
</Select>
);
};
export default LanguageSelector;

View File

@ -1,16 +1,17 @@
import React from "react";
import { useTranslation } from 'react-i18next';
interface DeprecationMessageProps {}
const DeprecationMessage: React.FC<DeprecationMessageProps> = () => {
const { t } = useTranslation();
return (
<div className="rounded-lg p-2 bg-fuchsia-200">
<p className="text-gray-800 text-sm">
We no longer support this model. Instead, code generation will use
GPT-4o or Claude Sonnet 3.5, the 2 state-of-the-art models.
{t('messages.deprecationMessage')}
</p>
</div>
);
};
export default DeprecationMessage;
export default DeprecationMessage;

View File

@ -1,25 +1,27 @@
import { useTranslation } from 'react-i18next';
export function OnboardingNote() {
const { t } = useTranslation();
return (
<div className="flex flex-col space-y-4 bg-green-700 p-2 rounded text-stone-200 text-sm">
<span>
To use Screenshot to Code,{" "}
{t('messages.onboardingNote.intro')}{" "}
<a
className="inline underline hover:opacity-70"
href="https://buy.stripe.com/8wM6sre70gBW1nqaEE"
target="_blank"
>
buy some credits (100 generations for $36)
{t('messages.onboardingNote.buyCredits')}
</a>{" "}
or use your own OpenAI API key with GPT4 vision access.{" "}
{t('messages.onboardingNote.orUseKey')}{" "}
<a
href="https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md"
className="inline underline hover:opacity-70"
target="_blank"
>
Follow these instructions to get yourself a key.
{t('messages.onboardingNote.followInstructions')}
</a>{" "}
and paste it in the Settings dialog (gear icon above). Your key is only
stored in your browser. Never stored on our servers.
{t('messagesonboardingNote.pasteKey')}
</span>
</div>
);

View File

@ -1,4 +1,7 @@
import { useTranslation } from 'react-i18next';
export function PicoBadge() {
const { t } = useTranslation();
return (
<>
<a
@ -9,7 +12,7 @@ export function PicoBadge() {
className="fixed z-50 bottom-16 right-5 rounded-md shadow bg-black
text-white px-4 text-xs py-3 cursor-pointer"
>
feature requests?
{t('messages.picoBadge.featureRequests')}
</div>
</a>
<a href="https://picoapps.xyz?ref=screenshot-to-code" target="_blank">
@ -17,7 +20,7 @@ export function PicoBadge() {
className="fixed z-50 bottom-5 right-5 rounded-md shadow text-black
bg-white px-4 text-xs py-3 cursor-pointer"
>
an open source project by Pico
{t('messages.picoBadge.openSourceProject')}
</div>
</a>
</>

View File

@ -1,6 +1,8 @@
import { useTranslation } from 'react-i18next';
import { URLS } from "../../urls";
function TipLink() {
const { t } = useTranslation();
return (
<a
className="text-xs underline text-gray-500 text-right"
@ -8,7 +10,7 @@ function TipLink() {
target="_blank"
rel="noopener"
>
Tips for better results
{t('messages.tipLink.text')}
</a>
);
}

View File

@ -5,6 +5,7 @@ import { Settings } from "../../types";
import copy from "copy-to-clipboard";
import { useCallback } from "react";
import toast from "react-hot-toast";
import { useTranslation } from 'react-i18next';
interface Props {
code: string;
@ -13,10 +14,11 @@ interface Props {
}
function CodeTab({ code, setCode, settings }: Props) {
const { t } = useTranslation();
const copyCode = useCallback(() => {
copy(code);
toast.success("Copied to clipboard");
}, [code]);
toast.success(t('preview.codeTab.codeCopied'));
}, [code, t]);
const doOpenInCodepenio = useCallback(async () => {
// TODO: Update CSS and JS external links depending on the framework being used
@ -57,17 +59,17 @@ function CodeTab({ code, setCode, settings }: Props) {
<div className="relative">
<div className="flex justify-start items-center px-4 mb-2">
<span
title="Copy Code"
title={t('preview.copyCode')}
className="bg-black text-white flex items-center justify-center hover:text-black hover:bg-gray-100 cursor-pointer rounded-lg text-sm p-2.5"
onClick={copyCode}
>
Copy Code <FaCopy className="ml-2" />
{t('preview.codeTab.copyCode')} <FaCopy className="ml-2" />
</span>
<Button
onClick={doOpenInCodepenio}
className="bg-gray-100 text-black ml-2 py-2 px-4 border border-black rounded-md hover:bg-gray-400 focus:outline-none"
>
Open in{" "}
{t('preview.codeTab.openIn')}{" "}
<img
src="https://assets.codepen.io/t-1/codepen-logo.svg"
alt="codepen.io"

View File

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
import {
FaUndo,
@ -22,6 +23,7 @@ interface Props {
}
function PreviewPane({ doUpdate, reset, settings }: Props) {
const { t } = useTranslation();
const { appState } = useAppStore();
const { inputMode, generatedCode, setGeneratedCode } = useProjectStore();
@ -42,14 +44,14 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
className="flex items-center ml-4 gap-x-2 dark:text-white dark:bg-gray-700"
>
<FaUndo />
Reset
{t('preview.pane.reset')}
</Button>
<Button
onClick={() => downloadCode(generatedCode)}
variant="secondary"
className="flex items-center gap-x-2 mr-4 dark:text-white dark:bg-gray-700 download-btn"
>
<FaDownload /> Download
<FaDownload /> {t('preview.pane.download')}
</Button>
</>
)}
@ -57,14 +59,14 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
<div className="flex items-center">
<TabsList>
<TabsTrigger value="desktop" className="flex gap-x-2">
<FaDesktop /> Desktop
<FaDesktop /> {t('preview.pane.desktop')}
</TabsTrigger>
<TabsTrigger value="mobile" className="flex gap-x-2">
<FaMobile /> Mobile
<FaMobile /> {t('preview.pane.mobile')}
</TabsTrigger>
<TabsTrigger value="code" className="flex gap-x-2">
<FaCode />
Code
{t('preview.pane.code')}
</TabsTrigger>
</TabsList>
</div>
@ -95,4 +97,4 @@ function PreviewPane({ doUpdate, reset, settings }: Props) {
);
}
export default PreviewPane;
export default PreviewPane;

View File

@ -4,6 +4,7 @@ import { ScreenRecorderState } from "../../types";
import { blobToBase64DataUrl } from "./utils";
import fixWebmDuration from "webm-duration-fix";
import toast from "react-hot-toast";
import { useTranslation } from 'react-i18next';
interface Props {
screenRecorderState: ScreenRecorderState;
@ -19,6 +20,7 @@ function ScreenRecorder({
setScreenRecorderState,
generateCode,
}: Props) {
const { t } = useTranslation();
const [mediaStream, setMediaStream] = useState<MediaStream | null>(null);
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
null
@ -66,7 +68,7 @@ function ScreenRecorder({
mediaRecorder.start();
setScreenRecorderState(ScreenRecorderState.RECORDING);
} catch (error) {
toast.error("Could not start screen recording");
toast.error(t('recording.errorStarting'));
throw error;
}
};
@ -90,7 +92,7 @@ function ScreenRecorder({
if (screenRecordingDataUrl) {
generateCode([screenRecordingDataUrl], "video");
} else {
toast.error("Screen recording does not exist. Please try again.");
toast.error(t('recording.noDataError'));
throw new Error("No screen recording data url");
}
};
@ -98,23 +100,23 @@ function ScreenRecorder({
return (
<div className="flex items-center justify-center my-3">
{screenRecorderState === ScreenRecorderState.INITIAL && (
<Button onClick={startScreenRecording}>Record Screen</Button>
<Button onClick={startScreenRecording}>{t('recording.start')}</Button>
)}
{screenRecorderState === ScreenRecorderState.RECORDING && (
<div className="flex items-center flex-col gap-y-4">
<div className="flex items-center mr-2 text-xl gap-x-1">
<span className="block h-10 w-10 bg-red-600 rounded-full mr-1 animate-pulse"></span>
<span>Recording...</span>
<span>{t('recording.inProgress')}</span>
</div>
<Button onClick={stopScreenRecording}>Finish Recording</Button>
<Button onClick={stopScreenRecording}>{t('recording.finish')}</Button>
</div>
)}
{screenRecorderState === ScreenRecorderState.FINISHED && (
<div className="flex items-center flex-col gap-y-4">
<div className="flex items-center mr-2 text-xl gap-x-1">
<span>Screen Recording Captured.</span>
<span>{t('recording.captured')}</span>
</div>
{screenRecordingDataUrl && (
<video
@ -132,9 +134,9 @@ function ScreenRecorder({
setScreenRecorderState(ScreenRecorderState.INITIAL)
}
>
Re-record
{t('recording.reRecord')}
</Button>
<Button onClick={kickoffGeneration}>Generate</Button>
<Button onClick={kickoffGeneration}>{t('recording.generate')}</Button>
</div>
</div>
)}

View File

@ -4,6 +4,7 @@ import { Button } from "../ui/button";
import { addHighlight, getAdjustedCoordinates, removeHighlight } from "./utils";
import { useAppStore } from "../../store/app-store";
import KeyboardShortcutBadge from "../core/KeyboardShortcutBadge";
import { useTranslation } from 'react-i18next';
interface EditPopupProps {
event: MouseEvent | null;
@ -16,6 +17,7 @@ const EditPopup: React.FC<EditPopupProps> = ({
iframeRef,
doUpdate,
}) => {
const { t } = useTranslation();
// App state
const { inSelectAndEditMode } = useAppStore();
@ -126,7 +128,7 @@ const EditPopup: React.FC<EditPopupProps> = ({
ref={textareaRef}
value={updateText}
onChange={(e) => setUpdateText(e.target.value)}
placeholder="Tell the AI what to change about this element..."
placeholder={t('selectAndEdit.editPopup.placeholder')}
className="dark:bg-gray-700 dark:text-white"
onKeyDown={(e) => {
if (e.key === "Enter") {
@ -140,7 +142,7 @@ const EditPopup: React.FC<EditPopupProps> = ({
className="dark:bg-gray-700 dark:text-white"
onClick={() => onUpdate(updateText)}
>
Update <KeyboardShortcutBadge letter="enter" />
{t('selectAndEdit.editPopup.update')} <KeyboardShortcutBadge letter="enter" />
</Button>
</div>
</div>

View File

@ -1,10 +1,11 @@
import { GiClick } from "react-icons/gi";
import { useAppStore } from "../../store/app-store";
import { Button } from "../ui/button";
import { useTranslation } from 'react-i18next';
function SelectAndEditModeToggleButton() {
const { inSelectAndEditMode, toggleInSelectAndEditMode } = useAppStore();
const { t } = useTranslation();
return (
<Button
onClick={toggleInSelectAndEditMode}
@ -13,7 +14,9 @@ function SelectAndEditModeToggleButton() {
>
<GiClick className="text-lg" />
<span>
{inSelectAndEditMode ? "Exit selection mode" : "Select and update"}
{inSelectAndEditMode
? t('selectAndEdit.selectAndEdit.exitMode')
: t('selectAndEdit.selectAndEdit.enterMode')}
</span>
</Button>
);

View File

@ -10,6 +10,7 @@ import {
CodeGenerationModel,
} from "../../lib/models";
import { Badge } from "../ui/badge";
import { useTranslation } from 'react-i18next';
interface Props {
codeGenerationModel: CodeGenerationModel;
@ -22,10 +23,11 @@ function ModelSettingsSection({
setCodeGenerationModel,
shouldDisableUpdates = false,
}: Props) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-y-2 justify-between text-sm">
<div className="grid grid-cols-3 items-center gap-4">
<span>AI Model:</span>
<span>{t('setting.aiModel')}</span>
<Select
value={codeGenerationModel}
onValueChange={(value: string) =>
@ -48,7 +50,7 @@ function ModelSettingsSection({
</span>
{CODE_GENERATION_MODEL_DESCRIPTIONS[model].inBeta && (
<Badge className="ml-2" variant="secondary">
Beta
{t('setting.beta')}
</Badge>
)}
</div>

View File

@ -8,6 +8,7 @@ import {
} from "../ui/select";
import { Badge } from "../ui/badge";
import { Stack, STACK_DESCRIPTIONS } from "../../lib/stacks";
import { useTranslation } from 'react-i18next';
function generateDisplayComponent(stack: Stack) {
const stackComponents = STACK_DESCRIPTIONS[stack].components;
@ -34,13 +35,14 @@ interface Props {
function OutputSettingsSection({
stack,
setStack,
label = "Generating:",
label = "setting.generating",
shouldDisableUpdates = false,
}: Props) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-y-2 justify-between text-sm">
<div className="grid grid-cols-3 items-center gap-4">
<span>{label}</span>
<span>{t(label)}</span>
<Select
value={stack}
onValueChange={(value: string) => setStack(value as Stack)}
@ -57,7 +59,7 @@ function OutputSettingsSection({
{generateDisplayComponent(stack)}
{STACK_DESCRIPTIONS[stack].inBeta && (
<Badge className="ml-2" variant="secondary">
Beta
{t('setting.beta')}
</Badge>
)}
</div>

View File

@ -22,6 +22,8 @@ import {
AccordionItem,
AccordionTrigger,
} from "../ui/accordion";
import LanguageSelector from "../languageSelector/LanguageSelector";
import { useTranslation } from 'react-i18next';
interface Props {
settings: Settings;
@ -29,6 +31,7 @@ interface Props {
}
function SettingsDialog({ settings, setSettings }: Props) {
const { t } = useTranslation();
const handleThemeChange = (theme: EditorTheme) => {
setSettings((s) => ({
...s,
@ -43,14 +46,14 @@ function SettingsDialog({ settings, setSettings }: Props) {
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="mb-4">Settings</DialogTitle>
<DialogTitle className="mb-4">{t('setting.title')}</DialogTitle>
</DialogHeader>
<div className="flex items-center space-x-2">
<Label htmlFor="image-generation">
<div>DALL-E Placeholder Image Generation</div>
<div>{t('setting.dallePlaceholder')}</div>
<div className="font-light mt-2 text-xs">
More fun with it but if you want to save money, turn it off.
{t('setting.dallePlaceholderDescription')}
</div>
</Label>
<Switch
@ -67,16 +70,15 @@ function SettingsDialog({ settings, setSettings }: Props) {
<div className="flex flex-col space-y-6">
<div>
<Label htmlFor="openai-api-key">
<div>OpenAI API key</div>
<div>{t('setting.openAIApiKey')}</div>
<div className="font-light mt-1 mb-2 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides
your .env config.
{t('setting.apiKeyDescription')}
</div>
</Label>
<Input
id="openai-api-key"
placeholder="OpenAI API key"
placeholder={t('setting.openAIApiKeyPlaceholder')}
value={settings.openAiApiKey || ""}
onChange={(e) =>
setSettings((s) => ({
@ -90,15 +92,15 @@ function SettingsDialog({ settings, setSettings }: Props) {
{!IS_RUNNING_ON_CLOUD && (
<div>
<Label htmlFor="openai-api-key">
<div>OpenAI Base URL (optional)</div>
<div>{t('setting.openAIBaseURL')}</div>
<div className="font-light mt-2 leading-relaxed">
Replace with a proxy URL if you don't want to use the default.
{t('setting.openAIBaseURLDescription')}
</div>
</Label>
<Input
id="openai-base-url"
placeholder="OpenAI Base URL"
placeholder={t('setting.openAIBaseURLPlaceholder')}
value={settings.openAiBaseURL || ""}
onChange={(e) =>
setSettings((s) => ({
@ -112,16 +114,15 @@ function SettingsDialog({ settings, setSettings }: Props) {
<div>
<Label htmlFor="anthropic-api-key">
<div>Anthropic API key</div>
<div>{t('setting.anthropicApiKey')}</div>
<div className="font-light mt-1 text-xs leading-relaxed">
Only stored in your browser. Never stored on servers. Overrides
your .env config.
{t('setting.apiKeyDescription')}
</div>
</Label>
<Input
id="anthropic-api-key"
placeholder="Anthropic API key"
placeholder={t('setting.anthropicApiKeyPlaceholder')}
value={settings.anthropicApiKey || ""}
onChange={(e) =>
setSettings((s) => ({
@ -131,21 +132,23 @@ function SettingsDialog({ settings, setSettings }: Props) {
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="language-select">{t('setting.language')}</Label>
<LanguageSelector />
</div>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Screenshot by URL Config</AccordionTrigger>
<AccordionTrigger>{t('setting.screenshotConfig')}</AccordionTrigger>
<AccordionContent>
<Label htmlFor="screenshot-one-api-key">
<div className="leading-normal font-normal text-xs">
If you want to use URLs directly instead of taking the
screenshot yourself, add a ScreenshotOne API key.{" "}
{t('setting.screenshotDescription')}{" "}
<a
href="https://screenshotone.com?via=screenshot-to-code"
className="underline"
target="_blank"
>
Get 100 screenshots/mo for free.
{t('setting.getFreeScreenshots')}
</a>
</div>
</Label>
@ -153,7 +156,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
<Input
id="screenshot-one-api-key"
className="mt-2"
placeholder="ScreenshotOne API key"
placeholder={t('setting.screenshotApiKeyPlaceholder')}
value={settings.screenshotOneApiKey || ""}
onChange={(e) =>
setSettings((s) => ({
@ -168,11 +171,11 @@ function SettingsDialog({ settings, setSettings }: Props) {
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Theme Settings</AccordionTrigger>
<AccordionTrigger>{t('setting.themeSettings')}</AccordionTrigger>
<AccordionContent className="space-y-4 flex flex-col">
<div className="flex items-center justify-between">
<Label htmlFor="app-theme">
<div>App Theme</div>
<div>{t('setting.appTheme')}</div>
</Label>
<div>
<button
@ -180,25 +183,25 @@ function SettingsDialog({ settings, setSettings }: Props) {
onClick={() => {
document
.querySelector("div.mt-2")
?.classList.toggle("dark"); // enable dark mode for sidebar
?.classList.toggle("dark");
document.body.classList.toggle("dark");
document
.querySelector('div[role="presentation"]')
?.classList.toggle("dark"); // enable dark mode for upload container
?.classList.toggle("dark");
}}
>
Toggle dark mode
{t('setting.toggleDarkMode')}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="editor-theme">
<div>
Code Editor Theme - requires page refresh to update
{t('setting.codeEditorTheme')}
</div>
</Label>
<div>
<Select // Use the custom Select component here
<Select
name="editor-theme"
value={settings.editorTheme}
onValueChange={(value) =>
@ -221,7 +224,7 @@ function SettingsDialog({ settings, setSettings }: Props) {
</div>
<DialogFooter>
<DialogClose>Save</DialogClose>
<DialogClose>{t('setting.save')}</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -12,6 +12,7 @@ import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { useEffect, useRef } from "react";
import HistoryDisplay from "../history/HistoryDisplay";
import { useTranslation } from 'react-i18next';
interface SidebarProps {
showSelectAndEditFeature: boolean;
@ -26,6 +27,7 @@ function Sidebar({
regenerate,
cancelCodeGeneration,
}: SidebarProps) {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null);
const {
@ -52,12 +54,8 @@ function Sidebar({
<div className="flex flex-col">
{/* Speed disclaimer for video mode */}
{inputMode === "video" && (
<div
className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700
p-2 text-xs mb-4 mt-1"
>
Code generation from videos can take 3-4 minutes. We do multiple
passes to get the best result. Please be patient.
<div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-2 text-xs mb-4 mt-1">
{t('sidebar.videoGenerationWarning')}
</div>
)}
@ -73,7 +71,7 @@ function Sidebar({
onClick={cancelCodeGeneration}
className="w-full dark:text-white dark:bg-gray-700"
>
Cancel
{t('sidebar.cancel')}
</Button>
</div>
</div>
@ -84,7 +82,7 @@ function Sidebar({
<div className="grid w-full gap-2">
<Textarea
ref={textareaRef}
placeholder="Tell the AI what to change..."
placeholder={t('sidebar.updateInstructionPlaceholder')}
onChange={(e) => setUpdateInstruction(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
@ -95,7 +93,7 @@ function Sidebar({
/>
<div className="flex justify-between items-center gap-x-2">
<div className="font-500 text-xs text-slate-700 dark:text-white">
Include screenshot of current version?
{t('sidebar.includeScreenshot')}
</div>
<Switch
checked={shouldIncludeResultImage}
@ -107,7 +105,7 @@ function Sidebar({
onClick={() => doUpdate(updateInstruction)}
className="dark:text-white dark:bg-gray-700 update-btn"
>
Update <KeyboardShortcutBadge letter="enter" />
{t('sidebar.update')} <KeyboardShortcutBadge letter="enter" />
</Button>
</div>
<div className="flex items-center justify-end gap-x-2 mt-2">
@ -115,7 +113,7 @@ function Sidebar({
onClick={regenerate}
className="flex items-center gap-x-2 dark:text-white dark:bg-gray-700 regenerate-btn"
>
🔄 Regenerate
🔄 {t('sidebar.regenerate')}
</Button>
{showSelectAndEditFeature && <SelectAndEditModeToggleButton />}
</div>
@ -138,7 +136,7 @@ function Sidebar({
<img
className="w-[340px] border border-gray-200 rounded-md"
src={referenceImages[0]}
alt="Reference"
alt={t('sidebar.referenceImageAlt')}
/>
)}
{inputMode === "video" && (
@ -152,12 +150,12 @@ function Sidebar({
)}
</div>
<div className="text-gray-400 uppercase text-sm text-center mt-1">
{inputMode === "video" ? "Original Video" : "Original Screenshot"}
{inputMode === "video" ? t('sidebar.originalVideo') : t('sidebar.originalScreenshot')}
</div>
</div>
)}
<div className="bg-gray-400 px-4 py-2 rounded text-sm hidden">
<h2 className="text-lg mb-4 border-b border-gray-800">Console</h2>
<h2 className="text-lg mb-4 border-b border-gray-800">{t('sidebar.console')}</h2>
{executionConsole.map((line, index) => (
<div
key={index}

18
frontend/src/i18n.ts Normal file
View File

@ -0,0 +1,18 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
interpolation: {
escapeValue: false
}
});
export default i18n;

View File

@ -5,6 +5,7 @@ import "./index.css";
import { Toaster } from "react-hot-toast";
import EvalsPage from "./components/evals/EvalsPage.tsx";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import './i18n';
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>

View File

@ -420,6 +420,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.23.2", "@babel/runtime@^7.24.8":
version "7.25.0"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz"
@ -2735,6 +2742,13 @@ crelt@^1.0.5:
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-fetch@4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983"
integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==
dependencies:
node-fetch "^2.6.12"
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz"
@ -3563,6 +3577,13 @@ html-minifier-terser@^6.1.0:
relateurl "^0.2.7"
terser "^5.10.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
@ -3597,6 +3618,27 @@ human-signals@^5.0.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
i18next-browser-languagedetector@^8.0.0:
version "8.0.0"
resolved "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f"
integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==
dependencies:
"@babel/runtime" "^7.23.2"
i18next-http-backend@^2.5.2:
version "2.5.2"
resolved "https://registry.npmmirror.com/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz#3d846cc239987fe7700d1cf0f17975807bfd25d3"
integrity sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==
dependencies:
cross-fetch "4.0.0"
i18next@^23.13.0:
version "23.13.0"
resolved "https://registry.npmmirror.com/i18next/-/i18next-23.13.0.tgz#d3cba6c5611b9826ff988f97a4929638a91875f6"
integrity sha512-B+g0/KTKmN3+NeMKPljQxdrih6Q6lyDF5O2e/Ofd0JQsTLojJD/BSTTN04iw6OVc0yBiHeypu5hoBNV6ag44Zw==
dependencies:
"@babel/runtime" "^7.23.2"
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -4459,6 +4501,13 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-fetch@^2.6.12:
version "2.7.0"
resolved "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
dependencies:
whatwg-url "^5.0.0"
node-html-parser@^5.3.3:
version "5.4.2"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
@ -4948,6 +4997,14 @@ react-hot-toast@^2.4.1:
dependencies:
goober "^2.1.10"
react-i18next@^15.0.1:
version "15.0.1"
resolved "https://registry.npmmirror.com/react-i18next/-/react-i18next-15.0.1.tgz#fc662d93829ecb39683fe2757a47ebfbc5c912a0"
integrity sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w==
dependencies:
"@babel/runtime" "^7.24.8"
html-parse-stringify "^3.0.1"
react-icons@^4.12.0:
version "4.12.0"
resolved "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz"
@ -5528,6 +5585,11 @@ toggle-selection@^1.0.6:
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz"
@ -5769,6 +5831,11 @@ vitest@^1.0.1:
vite-node "1.0.1"
why-is-node-running "^2.2.2"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
vscode-jsonrpc@6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz"
@ -5825,6 +5892,11 @@ walker@^1.0.8:
dependencies:
makeerror "1.0.12"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webm-duration-fix@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/webm-duration-fix/-/webm-duration-fix-1.0.4.tgz#fef235cb3d3ed3363507f705a7577dbb9fdedae6"
@ -5835,6 +5907,14 @@ webm-duration-fix@^1.0.4:
events "^3.3.0"
int64-buffer "^1.0.1"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"