Is DeepTranslate - Full Page & Bilingual Translator & AI-Powered Translation safe?
DeepTranslate routes all translated page text through its own proxy server and tracks each installation with a persistent UUID sent on every request.
When a user triggers translation, the full text content of the current page is sent to deeptranslate.ai servers as JSON โ bypassing any user-configured OpenAI key. Every request to deeptranslate.ai carries a device-unique X-Plugins-Id header generated at install time, along with the user's timezone, browser language, and version. The extension also periodically fetches remote per-site configuration rules from deeptranslate.ai, which can alter how it behaves on any URL, and fires Google Analytics events on translation actions using a second persistent client ID.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
AI Translation Sends Page Text to DeepTranslate Proxy
Using OpenAI page translation posts page text to deeptranslate.ai/trans/.../textTranslate.
The wrapper adds a persistent plugin ID, version, timezone, and auth token if logged in.
No captured body existed; details are from source.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-359
- Source
- Dynamic sandbox
You use the OpenAI page translation feature on a website.
The content script starts translating the current page after the feature is invoked.
The extension sends page text chunks and page context to DeepTranslate for translation.
The code path sends the request through the extension developer's proxy instead of calling an OpenAI endpoint directly.
- Page text chunksQuarterly roadmap: customer onboarding improvements (illustrative)
The text you asked to translate leaves the page and is sent to the translation proxy.
- Page URLhttps://intranet.example.com/quarterly-roadmap (illustrative)
The request can tie translated text to the page where you were reading it.
- Chunk identifiers0-3
These identifiers let the service match returned translations to the original page fragments.
- Target languageJapanese
This tells the service which language you selected for the translated page.
- Persistent plugin IDf6d01c54-7b0c-4c3d-9eaf-2e13b2a15c89 (illustrative)
This can link multiple translation requests from the same browser installation.
- Extension version1.7.5
This adds software-version context to each request.
- TimezoneAmerica/New_York
This adds regional context that can narrow where you are using the browser.
- Signed-in tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature (illustrative)
When you are logged in, the request can include an account token for the translation service.
- Static request signaturePC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU=
The request includes a fixed signature value used by the extension's proxy API.
No captured textTranslate request body was present in the evidence rows; the request body structure is shown in the code block.
- Sign
- PC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU=
- X-Timezone
- America/New_York (illustrative)
- Content-Type
- application/json
- X-Plugins-Id
- f6d01c54-7b0c-4c3d-9eaf-2e13b2a15c89 (illustrative)
- X-Client-Version
- 1.7.5
Source path from page text collection to DeepTranslate proxy request
Content script collects page text and forwards it for translation
content-scripts/content.js async collectNodesToTranslate() { const l = document.body.querySelectorAll("iframe"); for (const f of l) if (f.contentDocument?.body) { const g = (await io(f.contentDocument.body, this.ctx)).map(h => new Yn(h)); this.piecesToTranslate.push(...g) } const u = (await io(document.body, this.ctx)).map(f => new Yn(f)); this.piecesToTranslate.push(...u), await this.collectShadowRootNodesToTranslate() } async collectShadowRootNodesToTranslate() { const l = Vi(); for (const r of l) { ro(r); for (const u of _s(r)) { const d = (await io(u, this.ctx)).map(g => new Yn(g)); this.piecesToTranslate.push(...d) } } } restoreShadowRootInfluence() { for (const l of Vi()) { yv(l); for (const r of _s(l)) ji(r) } } getDocumentTextForLanguageDetection() { const l = [document.body?.innerText ?? ""]; for (const r of Vi()) { const u = Array.from(r.children).map(f => f instanceof HTMLElement ? (f.innerText || f.textContent || "").trim() : "").filter(Boolean).join(``); u && l.push(u) } return l.join(``).trim() } translatePageTitle() { const l = document.querySelector("title"); l && !_a(l, !1) || document.title.trim().length < 1 || (this.originalPageTitle = document.title, os(this.currentPageTranslatorService, this.currentTargetLanguage, this.originalPageTitle).then(r => { r && (document.title = r) })) } async translateDynamically() { try { if (this.piecesToTranslate && this.pageIsVisible) { const l = this.ctx.pageSpecialConfig?.preHeight ?? [0, 0], r = l.length === 2 ? l : [...l, ...l], u = g => { if (!g) return !1; const h = g.getBoundingClientRect(); return h.top >= 0 && h.top <= window.innerHeight + r[0] * window.innerHeight }, f = g => { if (!g) return !1; const h = g.getBoundingClientRect(); return h.bottom >= -r[1] * window.innerHeight && h.bottom <= window.innerHeight }, d = []; this.piecesToTranslate.forEach(g => { if (!g.isTranslated) { const h = g.target; (this.isPDFAndTranslateAllText || this.isEpub || f(h) || u(h)) && (g.isTranslated = !0, d.push(g)) } }), this.piecesToTranslate = this.piecesToTranslate.filter(g => !g.isTranslated), await this.splitAndTranslatePieces(d), this.firstTranslate !== null && !this.firstTranslate && (window.postMessage({ action: "translated", done: !0 }, "*"), this.firstTranslate = !0), this.mutationHandler.takeRecords() } } catch {} setTimeout(() => this.translateDynamically(), 200) } async splitAndTranslatePieces(l, r = {}) { const u = this.fooCount, f = await Ce.getServiceMaxTextLength(this.currentPageTranslatorService), d = new Map; let g = [0, 50, 200, 500]; f && (g = g.filter(p => p < f), g.push(f)); const h = p => { for (let b = 0; b < g.length; b++) if (p <= g[b + 1]) return g[b]; return g[g.length - 1] }; for (const p of l) { const b = p.simplifiedInnerHTML.length; if (f && b > f) continue; const x = { textLength: h(b), langCode: "auto" }, O = JSON.stringify(x); d.has(O) || d.set(O, []), d.get(O).push(p) } const v = []; for (const [p, b] of d) { const x = JSON.parse(p); v.push(this.translatePiecesToTranslate(b, u, { sourceLanguage: x.langCode, timeout: x.textLength > 500 ? -1 : 15e3, ...r })) } await Promise.allSettled(v) } async translatePiecesToTranslate(l, r, u = {}) { const f = { translateUrl: this.ctx.tabUrl, ...u }; if (l.length !== 0) try { const d = await Promise.all(l.map(async h => { const v = h.parent; return id(v), await Nv(h.simplifiedInnerHTML) })), g = await rs(this.currentPageTranslatorService, d, { sourceLanguage: "auto", targetLanguage: this.currentTargetLanguage, dontSortResults: this.dontSortResults, ...f }); if (this.pageLanguageState === "translated" && r === this.fooCount) for (let h = 0; h < l.length; h++) this.translatePtt(l[h], g[h]) } catch (d) { if (this.pageLanguageState === "translated" && r === this.fooCount) for (const g of l) this.handleResultError(g, d) } }AI translation service builds the textTranslate payload
background.js class at extends Ge { constructor(e) { super(e, tt + "/portal/translate/textTranslate", "POST", function(a) { function n(o) { const i = /[\uDC30-\uDC39]/g, c = /[\u{1F600}-\u{1F64F}|\u{1F300}-\u{1F5FF}|\u{1F680}-\u{1F6FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF}]/gu; return o.replace(i, "").replace(c, "") } const s = a.map((o, i) => ({ content: H.escapeHTML(n(o)), id: i.toString() })); return JSON.stringify(s) }, function(a) { const n = [], s = a?.data; if (!s) throw new M({ source: "server", message: a.message ?? "Empty response", code: a.code, traceId: a.traceId }); const o = new Map; for (let i = 0; i < s.texts.length; i++) { const c = s.texts[i], h = c.id.split("-"), f = h[0]; o.has(f) || o.set(f, []), o.get(f).push({ content: c.translation, id: h[1] }) } for (const [i, c] of o) n[i] = { text: JSON.stringify(c), detectedLanguage: s.targetLanguage }; return n }, function(a, n) { if (!a) return []; const s = JSON.parse(a), o = []; for (const i of s) typeof i.content == "string" && (o[i.id] = H.unescapeHTML(i.content)); return o }, function(a, n) { return "" }, function(a, n) { let { sourceLanguage: s, targetLanguage: o, translateUrl: i, translatorCode: c = "1", promptBuilderCode: h = 0 } = n; o === "ja" && (o = "Japanese"); const f = []; a.map((b, v) => { const R = JSON.parse(b.originalText); for (const g of R) g.content.length > 0 && f.push({ id: v + "-" + g.id, content: g.content }) }); const w = { translateUrl: i, targetLanguage: o, translatorCode: c, promptBuilderCode: h, texts: f }; return s !== "auto" && (w.sourceLanguage = s), JSON.stringify(w) }, function() { return { "Content-Type": "application/json", Sign: "PC6/JXgymOhlSbt9gEMq03LgdduKB0++YN8iH5CWnyU=" } }) } async translate(e, t) { if (this.serviceName === "openai") { const n = await E.checkAndConsumeOpenAIQuota(); if (console.log("๐ ~ AI Service ~ translate ~ quotaCheck:", n), !n.allowed) { try { const s = await chrome.tabs.query({ active: !0, currentWindow: !0 }); s.length > 0 && s[0].id && chrome.tabs.sendMessage(s[0].id, { action: y.SHOW_LOGIN_POPUP_FOR_QUOTA_EXCEEDED }) } catch (s) { console.error("๐ ~ Failed to send quota exceeded message to content script:", s) } throw new M({ source: "client", message: `ๆช็ปๅฝ็จๆทๅ
่ดนOpenAI็ฟป่ฏๆฌกๆฐๅทฒ็จๅฎ๏ผ่ฏท็ปๅฝไฝฟ็จๆดๅค็ฟป่ฏๆๅกใๅฉไฝๆฌกๆฐ: ${n.remaining}`, code: 403 }) } n.message === "quota_consumed" && n.remaining >= 0 && console.log(`๐ ~ OpenAI translation used, remaining: ${n.remaining} free uses`) } const a = await this.getTranslatorOptions(); return t = { ...t, ...a }, super.translate(e, t) } async getTranslatorOptions() { const [e, t] = await Promise.all([Ce.getValue(), this.getTranslatorCode()]); return { translatorCode: t, promptBuilderCode: e.selectCode } } async getTranslatorCode() { const t = (await z.getValue() || []).find(o => o.provider.toLowerCase() === this.serviceName.toLowerCase()); if (!t) return console.warn(`๐ ~ Service settings not found for: ${this.serviceName}, using default translator code`), "1"; if (!t.translatorItemList || t.translatorItemList.length === 0) return console.warn(`๐ ~ No translator items found for: ${this.serviceName}, using default translator code`), "1"; let a = t.translatorItemList.find(o => o.modelName === t.selectModel) || t.translatorItemList[0]; const n = async () => { const o = await V.getValue(); return !o || typeof o.vipExpiryTimestamp > "u" || o.vipExpiryTimestamp === 0 ? !1 : o.vipExpiryTimestamp > Date.now() }, s = a; if (s && s.vipLevel === 1 && !await n()) { console.warn(`๐ ~ User is not Pro but selected Pro model: ${a.modelName}, switching to free model`); const o = t.translatorItemList.find(i => i.vipLevel === 0); if (o) { a = o; const c = (await z.getValue()).map(h => h.provider.toLowerCase() === this.serviceName.toLowerCase() ? { ...h, selectModel: o.modelName } : h); await z.setValue(c), console.log(`๐ ~ Automatically switched to free model: ${o.modelName}`) } } return !a || !a.modelCode ? (console.warn(`๐ ~ Selected translator not found for: ${this.serviceName}, using default translator code`), "1") : a.modelCode }Shared request wrapper adds plugin, version, timezone, and auth headers
background.js async request(e, t) { const [a, n, s] = await Promise.all([K.getValue(), Ie.getValue(), V.getValue()]); console.log("๐ ~ ApiService ~ request ~ baseUrl:", j.env.apiBaseUrl); const o = typeof e == "string" && e.startsWith("/") ? j.env.apiBaseUrl + e : e, { timeout: i = -1, parseData: c = !0, cacheTTL: h = -1, forceUpdateCache: f = !1, ...w } = t, b = JSON.stringify({ url: o, options: w }); if (h > 0 && !f) { const v = await this.cache.get(b); if (v) return console.log("๐ ~ ApiService ~ request ~ cache hit:", { ...JSON.parse(b), cacheData: v }), v } try { const v = new AbortController, R = i > 0 ? setTimeout(() => v.abort(), i) : null, d = { "Accept-Language": this.mapToApiLanguageCode(a), "X-Plugins-Id": n, "X-Browser-Language": chrome.i18n.getUILanguage(), "X-Client-Version": chrome.runtime.getManifest().version, "X-Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone }; s?.token && (d.Authorization = s.token), console.log("๐ ~ ApiService ~ request ~ commonHeaders", d); const u = await fetch(o, { ...w, headers: { "Content-Type": "application/json", ...d, ...w?.headers }, signal: v.signal }); if (R && clearTimeout(R), console.log("๐ ~ ApiService ~ request ~ response", u), !u.ok) throw new M({ source: "server", message: u.statusText, code: u.status }); const m = await u.json(); if (m.code && m.code !== "S000000") throw m.code === "C000302" && (console.log("๐ ~ ApiService ~ C000302 error detected, showing VIP dialog"), this.handleVipUpgradeRequired()), new M({ source: "server", message: m.message, code: m.code }); console.log("๐ ~ ApiService ~ request ~ res:", m); const p = c ? m.data : m; return h > 0 && this.cache.set(b, p, h), console.log("๐ ~ ApiService ~ request ~ success", { url: o, options: t, result: p }), p } catch (v) { throw console.error(`๐ ~ ApiService ~ request ${o} ~ error`, v), v.name === "AbortError" ? new M({ source: "network", message: "Request timed out" }) : new M({ source: "network", message: v.message, code: v.code, traceId: v.traceId }) } }- deeptranslate.ai
Receives page text chunks, page URL context, translation settings, plugin ID, client metadata, and signed-in authorization when present for the textTranslate proxy request.
Persistent UUID links every translation request to your browser across sessions
Every translation request includes a permanent random ID assigned at install.
With your timezone and language, this builds a cross-session history of what you translated, when, and from which browser, with no opt-in.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-200
- Source
- Dynamic sandbox
You visit a page and the extension translates it.
This fires automatically on navigation, no button press required.
The extension sends your permanent browser UUID to deeptranslate.ai with every API call.
The same UUID is attached whether you are logged in or not, allowing server-side linkage across sessions.
Translation result returned. UUID present on all 8 observed requests in the same session; value identical to chrome.storage.local.pluginsId.
- X-Timezone
- Europe/London
- Content-Type
- application/json
- X-Plugins-Id
- a299c53b-5e50-4f3b-8fc4-4b2d63b63f43
- X-Client-Version
- 1.7.5
- X-Browser-Language
- en-US
- Permanent browser IDa299c53b-5e50-4f3b-8fc4-4b2d63b63f43
A UUID generated once at install and never rotated. Ties every request you make across all sessions to a single browser identity.
- Your timezoneEurope/London
Your local timezone, which narrows your geographic location and correlates with device settings.
- Browser languageen-US
The UI language Chrome is set to on your device.
- Extension version1.7.5
The version of DeepTranslate installed, which also correlates with your Chrome update cadence.
UUID generated at install and sent on every request
Install handler โ UUID creation
background.js:4759chrome.runtime.onInstalled.addListener(async (details) => { if (details.reason === 'install') { // Generate a permanent UUID and store it on first install chrome.storage.local.set({ pluginsId: self.crypto.randomUUID() }); ... }});Request builder โ four identifiers added to every API call
background.js:3299const commonHeaders = { 'Accept-Language': this.mapToApiLanguageCode(acceptLanguage), 'X-Plugins-Id': pluginsId, // permanent UUID from storage 'X-Browser-Language': chrome.i18n.getUILanguage(), 'X-Client-Version': chrome.runtime.getManifest().version, 'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone};Written once at install, never changed. Matches the X-Plugins-Id header on every captured request, confirming reuse across sessions.
- Location
- chrome.storage.local key 'pluginsId'
a299c53b-5e50-4f3b-8fc4-4b2d63b63f43- deeptranslate.ai
The developer's own translation API. Receives the UUID plus timezone, language, version on every translation, quota, user-info, and rule-fetch call.
Where it sends data
Destinations our analysis observed DeepTranslate contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- www.google-analytics.comwidely used
DeepTranslate sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.
- deeptranslate.ai
DeepTranslate sends data to deeptranslate.ai. No other extension we have analysed sends data here.