diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dda1670 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug, review +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5aafbad --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[ISSUE]" +labels: feature, review +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 0000000..0eee6c0 --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,26 @@ +name: Welcome First-Time Contributors + +on: + pull_request_target: + types: [opened] + issues: + types: [opened] + +jobs: + greet: + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: | + Hello @${{ github.actor }}! 👋 + Thank you so much for taking the time to open your first issue with CodeTranslateAI. We appreciate you helping us make the project better! + We'll take a look and get back to you soon. + pr-message: | + Hi @${{ github.actor }}! 🎉 + Welcome, and thank you for submitting your first pull request to CodeTranslateAI! We're thrilled to have your contribution. + Please make sure you've read our CONTRIBUTING.md guide. We'll review your changes shortly. diff --git a/backend/src/index.ts b/backend/src/index.ts index 73194ad..ce40246 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,54 +1,137 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; export interface Env { + RATE_LIMIT: KVNamespace; GEMINI_API_KEY: string; } -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'POST, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type', - }; +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', +}; - if (request.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } +const MAX_REQUESTS_ALLOWED = 10; +const DURATION = 60_000; + +async function checkRateLimit(ip: string, env: Env) { + const key = `ip_key:${ip}`; + const now = Date.now(); + let value = await env.RATE_LIMIT.get(key); + let data = { count: 0, time: now }; + if (value) { try { - const { code, targetLanguage } = await request.json<{ code: string; targetLanguage: string }>(); + data = JSON.parse(value); + } catch { + data = { count: 0, time: now }; + } + } - if (!code || !targetLanguage) { - return new Response(JSON.stringify({ error: "Missing 'code' or 'targetLanguage' in request body." }), { - status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - }); - } + if (now - data.time > DURATION) { + data.count = 0; + data.time = now; + } - const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY); + data.count += 1; + await env.RATE_LIMIT.put(key, JSON.stringify(data), { expirationTtl: 65 }); - const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); + return data.count <= MAX_REQUESTS_ALLOWED; +} +async function handleTranslate(request: Request, model: ReturnType) { + const { code, targetLanguage } = await request.json<{ code: string; targetLanguage: string }>(); + + if (!code || !targetLanguage) { + return new Response(JSON.stringify({ error: "Missing 'code' or 'targetLanguage' in request body." }), { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } - const prompt = `Translate the following code snippet to ${targetLanguage}. + const prompt = `Translate the following code snippet to ${targetLanguage}. Do not add any explanation, commentary, or markdown formatting like \`\`\` around the code. -**IMPORTANT: Preserve all original comments and their exact placement in the translated code. do not add extra spaces in between.** +**IMPORTANT: Preserve all original comments and their exact placement in the translated code. Do not add extra spaces in between.** Only provide the raw, translated code itself. Original Code: ${code}`; - const result = await model.generateContent(prompt); - const geminiResponse = result.response; - const translatedCode = geminiResponse.text(); + const result = await model.generateContent(prompt); + const translatedCode = result.response.text(); + + return new Response(JSON.stringify({ translation: translatedCode }), { + status: 200, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); +} + +async function handleExplain(request: Request, model: ReturnType) { + const { code } = await request.json<{ code: string }>(); + + if (!code) { + return new Response(JSON.stringify({ error: "Missing 'code' in request body." }), { + status: 400, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + + const prompt = `Explain the following code snippet in detail: +1. Provide a clear breakdown of what each part (functions, variables, logic blocks) does. +2. If applicable, describe the overall purpose or intent of the code. +3. Offer a step-by-step explanation of how the code executes. +4. If the code is executable, show a sample input and the corresponding output. +5. Keep the explanation beginner-friendly but technically accurate. + +Code: +${code}`; + + const result = await model.generateContent(prompt); + const explanation = result.response.text(); + + return new Response(JSON.stringify({ explanation }), { + status: 200, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + if (request.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + try { + const ip = request.headers.get('CF-Connecting-IP') || 'unknown'; + const allowed = await checkRateLimit(ip, env); + if (!allowed) { + return new Response(JSON.stringify({ error: "Too many requests. Try again later." }), { + status: 429, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + const url = new URL(request.url); + const path = url.pathname; + const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY); + const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); + + if(path==="/test-rate-limit"){ + return new Response(JSON.stringify("Proceed !")) + } + if (path === '/' || path === '/v1/translate') { + return await handleTranslate(request, model); + } + + if (path === '/v1/explain') { + return await handleExplain(request, model); + } - return new Response(JSON.stringify({ translation: translatedCode }), { - status: 200, + return new Response(JSON.stringify({ error: 'Route not found.' }), { + status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); } catch (error) { - console.error('Error during translation:', error); - return new Response(JSON.stringify({ error: 'An error occurred while translating the code.' }), { + console.error('Error during request:', error); + return new Response(JSON.stringify({ error: 'An internal error occurred.' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }); diff --git a/backend/worker-configuration.d.ts b/backend/worker-configuration.d.ts index 33f5779..af03300 100644 --- a/backend/worker-configuration.d.ts +++ b/backend/worker-configuration.d.ts @@ -3,6 +3,7 @@ // Runtime types generated with workerd@1.20250712.0 2025-07-15 declare namespace Cloudflare { interface Env { + RATE_LIMIT: KVNamespace; } } interface Env extends Cloudflare.Env {} diff --git a/backend/wrangler.jsonc b/backend/wrangler.jsonc index e6d2565..aa2104c 100644 --- a/backend/wrangler.jsonc +++ b/backend/wrangler.jsonc @@ -9,7 +9,14 @@ "compatibility_date": "2025-07-15", "observability": { "enabled": true - } + }, + "kv_namespaces": [ + { + "binding": "RATE_LIMIT", + "id": "" + } + ] + /** * Smart Placement * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit new file mode 100644 index 0000000..0312b76 --- /dev/null +++ b/frontend/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..1422e41 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +dist +node_modules +packages +package-lock.json \ No newline at end of file diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..63c660c --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "es5" +} diff --git a/frontend/background.js b/frontend/background.js index 57ab698..9e3c132 100644 --- a/frontend/background.js +++ b/frontend/background.js @@ -1,41 +1,52 @@ - chrome.runtime.onMessage.addListener((request, _, sendResponse) => { - if (request.type === "TRANSLATE_CODE") { - const BACKEND_URL = process.env.BACKEND_URL; - - chrome.storage.sync.get(['targetLanguage'], (result) => { - const targetLanguage = result.targetLanguage || 'Java'; - fetch(BACKEND_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - code: request.code, - targetLanguage: targetLanguage, - }), - }) - .then(response => { - if (!response.ok) { - throw new Error(`Network response was not ok: ${response.statusText}`); - } - return response.json(); - }) - .then(data => { - - if (data.error) { - sendResponse({ error: data.error }); - } else { - sendResponse({ translation: data.translation }); - } - }) - .catch(error => { + if (request.type === "TRANSLATE_CODE") { + const BACKEND_URL = process.env.BACKEND_URL; - console.error("Error calling backend:", error); - sendResponse({ error: `Failed to connect to the translation service: ${error.message}` }); - }); + chrome.storage.sync.get(["targetLanguage"], (result) => { + const targetLanguage = result.targetLanguage || "Java"; + fetch(BACKEND_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + code: request.code, + targetLanguage: targetLanguage, + }), + }) + .then((response) => { + if (!response.ok) { + throw new Error( + `Network response was not ok: ${response.statusText}` + ); + } + return response.json(); + }) + .then((data) => { + if (data.error) { + sendResponse({ error: data.error }); + } else { + sendResponse({ translation: data.translation }); + } + }) + .catch((error) => { + console.error("Error calling backend:", error); + sendResponse({ + error: `Failed to connect to the translation service: ${error.message}`, + }); }); + }); - return true; - } -}); \ No newline at end of file + return true; + } +}); +//Default commmand = Alt+T , Mac = Option+T +chrome.commands.onCommand.addListener((command) => { + if (command === "translate") { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { type: "ENABLE_PICKER" }); + } + }); + } +}); diff --git a/frontend/build.js b/frontend/build.js index a27e7bb..ed7fd08 100644 --- a/frontend/build.js +++ b/frontend/build.js @@ -1,16 +1,17 @@ -import esbuild from 'esbuild'; -import 'dotenv/config'; +import esbuild from "esbuild"; +import "dotenv/config"; -const define = {}; -for (const k in process.env) { - define[`process.env.${k}`] = JSON.stringify(process.env[k]); -} +const define = { + "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL || ""), +}; -esbuild.build({ - entryPoints: ['scripts/content.js', 'background.js'], +esbuild + .build({ + entryPoints: ["scripts/content.js", "background.js"], bundle: true, - outdir: 'dist', + outdir: "dist", define: define, -}).catch(() => process.exit(1)); + }) + .catch(() => process.exit(1)); -console.log('Build complete. Files are in the /dist folder.'); \ No newline at end of file +console.log("Build complete. Files are in the /dist folder."); diff --git a/frontend/manifest.json b/frontend/manifest.json index b9827f2..9d4ae3f 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -35,8 +35,17 @@ }, "web_accessible_resources": [ { - "resources": ["packages/prism.css"], + "resources": ["packages/prism.css","packages/prism-light.css"], "matches": [""] } - ] + ], + "commands": { + "translate": { + "suggested_key": { + "default": "Alt+Shift+A", + "mac": "Alt+Shift+A" + }, + "description": "Translate selected text" + } + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ff3f3a0..8d748a2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,10 @@ "version": "1.2.0", "devDependencies": { "dotenv": "^17.2.1", - "esbuild": "^0.25.8" + "esbuild": "^0.25.8", + "husky": "^9.1.7", + "lint-staged": "^16.1.4", + "prettier": "3.6.2" } }, "node_modules/@esbuild/aix-ppc64": { @@ -428,6 +431,132 @@ "node": ">=18" } }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/dotenv": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", @@ -440,6 +569,24 @@ "url": "https://dotenvx.com" } }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -480,6 +627,407 @@ "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lint-staged": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.4.tgz", + "integrity": "sha512-xy7rnzQrhTVGKMpv6+bmIA3C0yET31x8OhKBYfvGo0/byeZ6E0BjGARrir3Kg/RhhYHutpsi01+2J5IpfVoueA==", + "dev": true, + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^9.0.1", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz", + "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 167c63d..2b678c7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,10 +5,19 @@ "author": "Dinesh Kumar Sutihar", "type": "module", "scripts": { - "build": "node build.js" + "build": "node build.js", + "format": "prettier --write .", + "check-format": "prettier --check .", + "prepare": "husky" + }, + "lint-staged": { + "**/*.{js,css,md,json}": "prettier --write" }, "devDependencies": { "dotenv": "^17.2.1", - "esbuild": "^0.25.8" + "esbuild": "^0.25.8", + "husky": "^9.1.7", + "lint-staged": "^16.1.4", + "prettier": "3.6.2" } } diff --git a/frontend/packages/prism-light.css b/frontend/packages/prism-light.css new file mode 100644 index 0000000..cd20098 --- /dev/null +++ b/frontend/packages/prism-light.css @@ -0,0 +1,116 @@ +/* PrismJS 1.30.0 - Light Mode Theme +https://prismjs.com/download#themes=prism-tomorrow&languages=markup+css+clike+javascript+c+csharp+cpp+go+java+kotlin+markup-templating+php+python+ruby+rust+swift+typescript */ + +code[class*=language-], +pre[class*=language-] { + color: #333; + background: 0 0; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*=language-] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre) > code[class*=language-], +pre[class*=language-] { + background: #f8f8f8; + border: 1px solid #e8e8e8; +} + +:not(pre) > code[class*=language-] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.block-comment, +.token.cdata, +.token.comment, +.token.doctype, +.token.prolog { + color: #708090; +} + +.token.punctuation { + color: #999; +} + +.token.attr-name, +.token.deleted, +.token.namespace, +.token.tag { + color: #d73a49; +} + +.token.function-name { + color: #005cc5; +} + +.token.boolean, +.token.function, +.token.number { + color: #e36209; +} + +.token.class-name, +.token.constant, +.token.property, +.token.symbol { + color: #b08800; +} + +.token.atrule, +.token.builtin, +.token.important, +.token.keyword, +.token.selector { + color: #d73a49; +} + +.token.attr-value, +.token.char, +.token.regex, +.token.string, +.token.variable { + color: #22863a; +} + +.token.entity, +.token.operator, +.token.url { + color: #005cc5; +} + +.token.bold, +.token.important { + font-weight: 700; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: #28a745; +} \ No newline at end of file diff --git a/frontend/popup.css b/frontend/popup.css index 5ff45d7..6e6a4ad 100644 --- a/frontend/popup.css +++ b/frontend/popup.css @@ -1,6 +1,7 @@ :root { - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, - Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + --font-sans: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; --bg-color: #f7f7f8; --text-color: #2c2c2e; --subtle-text-color: #6a6a6e; @@ -15,7 +16,7 @@ body.dark-mode { --text-color: #f2f2f7; --subtle-text-color: #98989d; --primary-color: #0a84ff; - --primary-hover-color: #389dff; + --primary-hover-color: #5e9edd; --container-bg-color: #2c2c2e; --border-color: #3a3a3c; } @@ -27,7 +28,9 @@ body { width: 300px; padding: 16px; margin: 0; - transition: background-color 0.2s ease, color 0.2s ease; + transition: + background-color 0.2s ease, + color 0.2s ease; } .container { diff --git a/frontend/popup.html b/frontend/popup.html index fb37c5e..081ad19 100644 --- a/frontend/popup.html +++ b/frontend/popup.html @@ -1,4 +1,4 @@ - + @@ -25,6 +25,7 @@

CodeTranslateAI

+ diff --git a/frontend/popup.js b/frontend/popup.js index 3c8c405..23ca7b0 100644 --- a/frontend/popup.js +++ b/frontend/popup.js @@ -1,45 +1,45 @@ -const enablePickerBtn = document.getElementById('enable-picker-btn'); -const languageSelect = document.getElementById('language-select'); -const themeToggle = document.getElementById('theme-toggle'); +const enablePickerBtn = document.getElementById("enable-picker-btn"); +const languageSelect = document.getElementById("language-select"); +const themeToggle = document.getElementById("theme-toggle"); const body = document.body; -enablePickerBtn.addEventListener('click', () => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - chrome.tabs.sendMessage(tabs[0].id, { type: "ENABLE_PICKER" }); - window.close(); - }); +enablePickerBtn.addEventListener("click", () => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + chrome.tabs.sendMessage(tabs[0].id, { type: "ENABLE_PICKER" }); + window.close(); + }); }); -languageSelect.addEventListener('change', () => { - chrome.storage.sync.set({ targetLanguage: languageSelect.value }); +languageSelect.addEventListener("change", () => { + chrome.storage.sync.set({ targetLanguage: languageSelect.value }); }); -themeToggle.addEventListener('change', () => { - if (themeToggle.checked) { - body.classList.add('dark-mode'); - chrome.storage.sync.set({ theme: 'dark' }); - } else { - body.classList.remove('dark-mode'); - chrome.storage.sync.set({ theme: 'light' }); - } +themeToggle.addEventListener("change", () => { + if (themeToggle.checked) { + body.classList.add("dark-mode"); + chrome.storage.sync.set({ theme: "dark" }); + } else { + body.classList.remove("dark-mode"); + chrome.storage.sync.set({ theme: "light" }); + } }); function initializePopup() { - chrome.storage.sync.get(['targetLanguage'], (result) => { - if (result.targetLanguage) { - languageSelect.value = result.targetLanguage; - } - }); + chrome.storage.sync.get(["targetLanguage"], (result) => { + if (result.targetLanguage) { + languageSelect.value = result.targetLanguage; + } + }); - chrome.storage.sync.get(['theme'], (result) => { - if (result.theme === 'dark') { - body.classList.add('dark-mode'); - themeToggle.checked = true; - } else { - body.classList.remove('dark-mode'); - themeToggle.checked = false; - } - }); + chrome.storage.sync.get(["theme"], (result) => { + if (result.theme === "dark") { + body.classList.add("dark-mode"); + themeToggle.checked = true; + } else { + body.classList.remove("dark-mode"); + themeToggle.checked = false; + } + }); } -document.addEventListener('DOMContentLoaded', initializePopup); \ No newline at end of file +document.addEventListener("DOMContentLoaded", initializePopup); diff --git a/frontend/scripts/cache.js b/frontend/scripts/cache.js index a0cc5f8..49f449b 100644 --- a/frontend/scripts/cache.js +++ b/frontend/scripts/cache.js @@ -1,25 +1,25 @@ export function hashCode(str) { - let hash = 0; - for (let i = 0, len = str.length; i < len; i++) { - let chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; - } - return hash; + let hash = 0; + for (let i = 0, len = str.length; i < len; i++) { + let chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; + } + return hash; } export async function saveToCache(key, data, daysToExpire) { - const expirationMs = daysToExpire * 24 * 60 * 60 * 1000; - const cacheItem = { data: data, expiresAt: Date.now() + expirationMs }; - await chrome.storage.local.set({ [key]: cacheItem }); + const expirationMs = daysToExpire * 24 * 60 * 60 * 1000; + const cacheItem = { data: data, expiresAt: Date.now() + expirationMs }; + await chrome.storage.local.set({ [key]: cacheItem }); } export async function getFromCache(key) { - const result = await chrome.storage.local.get(key); - const cacheItem = result[key]; - if (!cacheItem || Date.now() > cacheItem.expiresAt) { - if (cacheItem) await chrome.storage.local.remove(key); - return null; - } - return cacheItem.data; -} \ No newline at end of file + const result = await chrome.storage.local.get(key); + const cacheItem = result[key]; + if (!cacheItem || Date.now() > cacheItem.expiresAt) { + if (cacheItem) await chrome.storage.local.remove(key); + return null; + } + return cacheItem.data; +} diff --git a/frontend/scripts/content.js b/frontend/scripts/content.js index bc6a776..7ba0c58 100644 --- a/frontend/scripts/content.js +++ b/frontend/scripts/content.js @@ -1,57 +1,65 @@ -import { enablePicker } from './picker.js'; -import { hashCode, getFromCache, saveToCache } from './cache.js'; -import { injectOrUpdateTranslations } from './ui.js'; +import { enablePicker } from "./picker.js"; +import { hashCode, getFromCache, saveToCache } from "./cache.js"; +import { injectOrUpdateTranslations } from "./ui.js"; chrome.runtime.onMessage.addListener((message) => { - - if (message.type === "ENABLE_PICKER") { - enablePicker(handleElementClick); - } - return true; + if (message.type === "ENABLE_PICKER") { + enablePicker(handleElementClick); + } + return true; }); async function handleElementClick(e) { - e.preventDefault(); - e.stopPropagation(); + e.preventDefault(); + e.stopPropagation(); + + const clickedElement = e.target; + const selectedCode = clickedElement.textContent?.trim(); - const clickedElement = e.target; - const selectedCode = clickedElement.textContent?.trim(); + if (!selectedCode) return; - if (!selectedCode) return; + const cacheKey = `translation_${hashCode(selectedCode)}`; + const originalWidth = clickedElement.getBoundingClientRect().width; + const { targetLanguage, theme } = await chrome.storage.sync.get(["targetLanguage", "theme"]); + const lang = targetLanguage; + const cachedData = await getFromCache(cacheKey); - const cacheKey = `translation_${hashCode(selectedCode)}`; - const originalWidth = clickedElement.getBoundingClientRect().width; - const { targetLanguage } = await chrome.storage.sync.get('targetLanguage'); - const lang = targetLanguage; - const cachedData = await getFromCache(cacheKey); + if (cachedData && cachedData[lang]) { + injectOrUpdateTranslations(cachedData, clickedElement, originalWidth,theme); + return; + } - if (cachedData && cachedData[lang]) { - injectOrUpdateTranslations(cachedData, clickedElement, originalWidth); + const loadingDiv = document.createElement("div"); + loadingDiv.className = "translator-loading"; + loadingDiv.textContent = `Translating to ${lang}...`; + loadingDiv.style.width = `${originalWidth}px`; + clickedElement.parentNode.insertBefore( + loadingDiv, + clickedElement.nextSibling + ); + + chrome.runtime.sendMessage( + { type: "TRANSLATE_CODE", code: selectedCode }, + async (response) => { + loadingDiv.remove(); + if (chrome.runtime.lastError || !response) { + alert(`Error: Could not connect to the translation service.`); + console.error(chrome.runtime.lastError?.message); return; - } + } - const loadingDiv = document.createElement('div'); - loadingDiv.className = 'translator-loading'; - loadingDiv.textContent = `Translating to ${lang}...`; - loadingDiv.style.width = `${originalWidth}px`; - clickedElement.parentNode.insertBefore(loadingDiv, clickedElement.nextSibling); - - chrome.runtime.sendMessage({ type: "TRANSLATE_CODE", code: selectedCode }, async (response) => { - loadingDiv.remove(); - if (chrome.runtime.lastError || !response) { - alert(`Error: Could not connect to the translation service.`); - console.error(chrome.runtime.lastError?.message); - return; - } - - if (response.error) { - alert(`Error: ${response.error}`); - } else if (response.translation) { - const cleaned = response.translation.replace(/```[a-z]*\n/g, '').replace(/```/g, '').trim(); - const newData = cachedData || {}; - newData[lang] = cleaned; - await saveToCache(cacheKey, newData, 10); - injectOrUpdateTranslations(newData, clickedElement, originalWidth); - } - }); -} \ No newline at end of file + if (response.error) { + alert(`Error: ${response.error}`); + } else if (response.translation) { + const cleaned = response.translation + .replace(/```[a-z]*\n/g, "") + .replace(/```/g, "") + .trim(); + const newData = cachedData || {}; + newData[lang] = cleaned; + await saveToCache(cacheKey, newData, 10); + injectOrUpdateTranslations(newData, clickedElement, originalWidth,theme); + } + } + ); +} diff --git a/frontend/scripts/picker.js b/frontend/scripts/picker.js index bfd9012..ee1f81c 100644 --- a/frontend/scripts/picker.js +++ b/frontend/scripts/picker.js @@ -2,34 +2,34 @@ let hoveredElement = null; let currentClickHandler = null; function onMouseOver(e) { - hoveredElement = e.target; - hoveredElement.classList.add('translator-highlight'); + hoveredElement = e.target; + hoveredElement.classList.add("translator-highlight"); } function onMouseOut(e) { - e.target.classList.remove('translator-highlight'); - hoveredElement = null; + e.target.classList.remove("translator-highlight"); + hoveredElement = null; } function disablePicker() { - document.body.style.cursor = 'default'; - if (hoveredElement) { - hoveredElement.classList.remove('translator-highlight'); - } - document.removeEventListener('mouseover', onMouseOver); - document.removeEventListener('mouseout', onMouseOut); - if (currentClickHandler) { - document.removeEventListener('click', currentClickHandler, true); - } + document.body.style.cursor = "default"; + if (hoveredElement) { + hoveredElement.classList.remove("translator-highlight"); + } + document.removeEventListener("mouseover", onMouseOver); + document.removeEventListener("mouseout", onMouseOut); + if (currentClickHandler) { + document.removeEventListener("click", currentClickHandler, true); + } } export function enablePicker(onClickCallback) { - document.body.style.cursor = 'crosshair'; - currentClickHandler = (e) => { - disablePicker(); - onClickCallback(e); - }; - document.addEventListener('mouseover', onMouseOver); - document.addEventListener('mouseout', onMouseOut); - document.addEventListener('click', currentClickHandler, true); -} \ No newline at end of file + document.body.style.cursor = "crosshair"; + currentClickHandler = (e) => { + disablePicker(); + onClickCallback(e); + }; + document.addEventListener("mouseover", onMouseOver); + document.addEventListener("mouseout", onMouseOut); + document.addEventListener("click", currentClickHandler, true); +} diff --git a/frontend/scripts/ui.js b/frontend/scripts/ui.js index e01a36c..387ca85 100644 --- a/frontend/scripts/ui.js +++ b/frontend/scripts/ui.js @@ -1,5 +1,10 @@ -export function injectOrUpdateTranslations(translations, originalElement, width) { - const componentStyles = ` +export function injectOrUpdateTranslations( + translations, + originalElement, + width, + currentTheme +) { + const componentStyles = ` .tab-nav { display: flex; border-bottom: 1px solid #ccc; @@ -29,82 +34,172 @@ export function injectOrUpdateTranslations(translations, originalElement, width) .tab-content.active { display: block; } + .code-wrapper{ + position:relative + } + .copy-button { + position: absolute; + top: 8px; + right: 8px; + padding: 6px 12px; + font-size: 14px; + background-color: rgba(0, 0, 0, 0.05); /* soft overlay */ + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 4px; + color: #333; + cursor: pointer; + transition: background-color 0.3s, border-color 0.3s, color 0.3s; + z-index: 10; + } + .copy-button:hover { + background-color: rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.2); + color: #000; + } + .copy-button:active { + background-color: rgba(0, 0, 0, 0.15); + } pre { margin: 0; white-space: pre-wrap; word-wrap: break-word; + } code { font-family: monospace; font-size: 0.8em; } + .dark .tab-nav { + border-bottom: 1px solid #3e3e42; + background-color: #2d2d30; + } + .dark .tab-link { + color: #969696; + } + .dark .tab-link:hover { + background-color: #3e3e42; + } + .dark .tab-link.active { + color: #4a9eff; + border-bottom: 3px solid #4a9eff; + } + .dark .copy-button { + background-color: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.2); + color: #f0f0f0; + } + .dark .copy-button:hover { + background-color: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + color: #ffffff; + } + .dark .copy-button:active { + background-color: rgba(255, 255, 255, 0.2); + } + .dark pre { + background-color: #1e1e1e; + color: #cccccc; + } + .dark code { + color: #cccccc; + } `; + let container = originalElement.nextElementSibling; - let container = originalElement.nextElementSibling; - - if (!container || container.id !== 'my-code-translator-container') { - container = document.createElement('div'); - container.id = 'my-code-translator-container'; - const shadowRoot = container.attachShadow({ mode: 'open' }); - const prismTheme = document.createElement('link'); - prismTheme.rel = 'stylesheet'; - prismTheme.href = chrome.runtime.getURL('packages/prism.css'); - shadowRoot.appendChild(prismTheme); - const styleElement = document.createElement('style'); - styleElement.textContent = componentStyles; - shadowRoot.appendChild(styleElement); - const uiWrapper = document.createElement('div'); - uiWrapper.className = 'ui-wrapper'; - shadowRoot.appendChild(uiWrapper); - originalElement.parentNode.insertBefore(container, originalElement.nextSibling); + if (!container || container.id !== "my-code-translator-container") { + container = document.createElement("div"); + container.id = "my-code-translator-container"; + const shadowRoot = container.attachShadow({ mode: "open" }); + const prismTheme = document.createElement("link"); + prismTheme.rel = "stylesheet"; + if(currentTheme==="dark"){ + prismTheme.href = chrome.runtime.getURL("packages/prism.css"); + }else{ + prismTheme.href = chrome.runtime.getURL("packages/prism-light.css"); } + + shadowRoot.appendChild(prismTheme); + const styleElement = document.createElement("style"); + styleElement.textContent = componentStyles; + shadowRoot.appendChild(styleElement); + const uiWrapper = document.createElement("div"); + uiWrapper.className = "ui-wrapper"; + uiWrapper.classList.add(currentTheme === "dark" ? "dark" : "light"); + shadowRoot.appendChild(uiWrapper); + originalElement.parentNode.insertBefore( + container, + originalElement.nextSibling + ); + } - container.style.width = `${width}px`; - container.style.boxSizing = 'border-box'; - const shadowRoot = container.shadowRoot; - const uiWrapper = shadowRoot.querySelector('.ui-wrapper'); - uiWrapper.innerHTML = ''; - const tabNav = document.createElement('div'); - tabNav.className = 'tab-nav'; - const contentArea = document.createElement('div'); - contentArea.className = 'tab-content-area'; - uiWrapper.appendChild(tabNav); - uiWrapper.appendChild(contentArea); - Object.keys(translations).forEach(lang => { - const contentPanel = document.createElement('div'); - contentPanel.className = 'tab-content'; - contentPanel.dataset.lang = lang; - const langClass = `language-${lang.toLowerCase()}`; - const pre = document.createElement('pre'); - pre.className = langClass; - const code = document.createElement('code'); - code.className = langClass; - code.textContent = translations[lang]; - pre.appendChild(code); - contentPanel.appendChild(pre); - contentArea.appendChild(contentPanel); + container.style.width = `${width}px`; + container.style.boxSizing = "border-box"; + const shadowRoot = container.shadowRoot; + const uiWrapper = shadowRoot.querySelector(".ui-wrapper"); + uiWrapper.innerHTML = ""; + const tabNav = document.createElement("div"); + tabNav.className = "tab-nav"; + const contentArea = document.createElement("div"); + contentArea.className = "tab-content-area"; + uiWrapper.appendChild(tabNav); + uiWrapper.appendChild(contentArea); + Object.keys(translations).forEach((lang) => { + const contentPanel = document.createElement("div"); + contentPanel.className = "tab-content"; + contentPanel.dataset.lang = lang; + const codeWrapper = document.createElement("div"); + codeWrapper.className = "code-wrapper"; + const copyButton = document.createElement("div"); + copyButton.className = "copy-button"; + copyButton.innerText = "Copy"; + copyButton.addEventListener("click", () => { + navigator.clipboard.writeText(translations[lang]).then(() => { + copyButton.innerText = "Copied!"; + setTimeout(() => (copyButton.innerText = "Copy"), 2000); + }); }); + const langClass = `language-${lang.toLowerCase()}`; + const pre = document.createElement("pre"); + pre.className = langClass; + const code = document.createElement("code"); + code.className = langClass; + code.textContent = translations[lang]; - Object.keys(translations).forEach((lang, index) => { - const tabButton = document.createElement('button'); - tabButton.className = 'tab-link'; - tabButton.textContent = lang; - tabButton.addEventListener('click', () => { - shadowRoot.querySelectorAll('.tab-link').forEach(btn => btn.classList.remove('active')); - shadowRoot.querySelectorAll('.tab-content').forEach(panel => panel.classList.remove('active')); - tabButton.classList.add('active'); - shadowRoot.querySelector(`.tab-content[data-lang="${lang}"]`).classList.add('active'); - }); - tabNav.appendChild(tabButton); - if (index === 0) { - tabButton.click(); - } + pre.appendChild(code); + codeWrapper.appendChild(copyButton); + codeWrapper.appendChild(pre); + contentPanel.appendChild(codeWrapper); + contentArea.appendChild(contentPanel); + }); + + Object.keys(translations).forEach((lang, index) => { + const tabButton = document.createElement("button"); + tabButton.className = "tab-link"; + tabButton.textContent = lang; + tabButton.addEventListener("click", () => { + shadowRoot + .querySelectorAll(".tab-link") + .forEach((btn) => btn.classList.remove("active")); + shadowRoot + .querySelectorAll(".tab-content") + .forEach((panel) => panel.classList.remove("active")); + tabButton.classList.add("active"); + shadowRoot + .querySelector(`.tab-content[data-lang="${lang}"]`) + .classList.add("active"); }); - try { - if (window.Prism) { - contentArea.querySelectorAll(`pre[class*="language-"]`).forEach(element => window.Prism.highlightElement(element)); - } - } catch (e) { - console.error('CodeTranslateAI: Error highlighting syntax.', e); + tabNav.appendChild(tabButton); + if (index === 0) { + tabButton.click(); + } + }); + try { + if (window.Prism) { + contentArea + .querySelectorAll(`pre[class*="language-"]`) + .forEach((element) => window.Prism.highlightElement(element)); } -} \ No newline at end of file + } catch (e) { + console.error("CodeTranslateAI: Error highlighting syntax.", e); + } +} diff --git a/frontend/styles.css b/frontend/styles.css index 15e9459..616a271 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -12,4 +12,5 @@ border-radius: 4px; color: #0056b3; font-family: sans-serif; + text-align: center; }