From 658e0a497fed9dfdb440897f930f798553cc01ba Mon Sep 17 00:00:00 2001 From: wsh Date: Sat, 17 Aug 2024 19:20:42 +0800 Subject: [PATCH] add internationalization base configuration and internationalize App.tsx --- frontend/package.json | 4 + frontend/public/locales/en/translation.json | 10 +++ frontend/public/locales/zh/translation.json | 10 +++ frontend/src/App.tsx | 27 +++---- .../languageSelector/LanguageSelector.css | 20 +++++ .../languageSelector/LanguageSelector.tsx | 43 ++++++++++ .../components/settings/SettingsDialog.tsx | 6 +- frontend/src/i18n.ts | 18 +++++ frontend/src/main.tsx | 1 + frontend/yarn.lock | 80 +++++++++++++++++++ 10 files changed, 202 insertions(+), 17 deletions(-) create mode 100644 frontend/public/locales/en/translation.json create mode 100644 frontend/public/locales/zh/translation.json create mode 100644 frontend/src/components/languageSelector/LanguageSelector.css create mode 100644 frontend/src/components/languageSelector/LanguageSelector.tsx create mode 100644 frontend/src/i18n.ts diff --git a/frontend/package.json b/frontend/package.json index 4652dc7..7b14477 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json new file mode 100644 index 0000000..fd751be --- /dev/null +++ b/frontend/public/locales/en/translation.json @@ -0,0 +1,10 @@ +{ + "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." + } + } +} \ No newline at end of file diff --git a/frontend/public/locales/zh/translation.json b/frontend/public/locales/zh/translation.json new file mode 100644 index 0000000..93ac621 --- /dev/null +++ b/frontend/public/locales/zh/translation.json @@ -0,0 +1,10 @@ +{ + "app": { + "title": "截图转代码", + "errors": { + "emptyInstruction": "请为AI提供一些关于更新内容的指示。", + "noCurrentVersion": "未设置当前版本。请联系支持或在Github上开issue。", + "invalidHistory": "版本历史无效。这不应该发生。请联系支持或在Github上开issue。" + } + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9c20df3..be98d1a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() {
{/* Header with access to settings */}
-

Screenshot to Code

+

{t('app.title')}

@@ -414,4 +409,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/languageSelector/LanguageSelector.css b/frontend/src/components/languageSelector/LanguageSelector.css new file mode 100644 index 0000000..282b9a9 --- /dev/null +++ b/frontend/src/components/languageSelector/LanguageSelector.css @@ -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; +} diff --git a/frontend/src/components/languageSelector/LanguageSelector.tsx b/frontend/src/components/languageSelector/LanguageSelector.tsx new file mode 100644 index 0000000..0655026 --- /dev/null +++ b/frontend/src/components/languageSelector/LanguageSelector.tsx @@ -0,0 +1,43 @@ +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'; + +const LanguageSelector: React.FC = () => { + const getBrowserLanguage = () => { + const browserLang = navigator.language.split('-')[0]; + return (['en', 'zh'].includes(browserLang) ? browserLang : 'en') as Languages; + }; + + const [selectedLang, setSelectedLang] = useState(i18n.language as Languages); + + useEffect(() => { + const defaultLang = getBrowserLanguage(); + if (defaultLang !== i18n.language) { + i18n.changeLanguage(defaultLang); + setSelectedLang(defaultLang); + } + }, []); + + const handleLanguageChange = (value: string) => { + const nextLang = value as Languages; + i18n.changeLanguage(nextLang); + setSelectedLang(nextLang); + }; + + return ( + + ); +}; + +export default LanguageSelector; \ No newline at end of file diff --git a/frontend/src/components/settings/SettingsDialog.tsx b/frontend/src/components/settings/SettingsDialog.tsx index 4283ad4..9bbc59a 100644 --- a/frontend/src/components/settings/SettingsDialog.tsx +++ b/frontend/src/components/settings/SettingsDialog.tsx @@ -22,6 +22,7 @@ import { AccordionItem, AccordionTrigger, } from "../ui/accordion"; +import LanguageSelector from "../languageSelector/LanguageSelector"; interface Props { settings: Settings; @@ -131,7 +132,10 @@ function SettingsDialog({ settings, setSettings }: Props) { } />
- +
+ + +
Screenshot by URL Config diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..28e90c9 --- /dev/null +++ b/frontend/src/i18n.ts @@ -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; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c4224f4..8fc6816 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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( diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e1254e8..361edc7 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -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"