Is XTranslate safe?

Medium risk

XTranslate sends translated text and a persistent installation UUID to its own servers and usage events to Google Analytics.

Every translation request — including the text being translated — is transmitted to xtranslate.dev, where it is linked to a per-installation anonymous identifier stored in synced browser storage. Authenticated users' requests include session cookies via credentials:include, associating translated content with their account. Separately, the extension reports translation provider, language pairs, and user actions to Google Analytics using the same persistent UUID.

Roman Escov5.17.2Chrome Web Store
49Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

Free-trial translations post text with a persistent UUID

XTranslate's free-trial flow posts text to xtranslate.dev with a persistent anonymous ID from sync storage.

A traffic capture missed this endpoint, but the source wires the trial button to it and updates the local quota from the response.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You use the free-trial AI translation option.

The UI path is the popup's free-trial action button, which calls the trial translation method.

The extension did this

The extension code builds a POST request containing the text to translate and the stored anonymous ID.

The request is sent to xtranslate.dev and the returned quota values update the extension's stored trial state.

Captured request
POSThttps://www.xtranslate.dev/api/translate/demo

The endpoint and method are from the shipped source code; a separate traffic observation saw xtranslate.dev pricing and user-plan requests but did not capture this demo request.

Headers
Content-Type
application/json
Fields built for the demo translation POST
  • Anonymous trial ID
    f47ac10b-58cc-4372-a567-0e02b2c3d479

    This lets the service connect free-trial translation requests to the same extension installation.

  • Text to translate
    Quarterly roadmap review starts at 14:00

    This is the selected page text or entered text submitted for translation.

  • Source language
    auto

    This records the language the extension is translating from.

  • Target language
    en

    This records the language you asked the extension to translate into.

The code that does this

Free-trial state, button path, and POST body

Readable version

Readable background bundle: trial state

background.js
const ti = ys("xtranslate_pro_trial", {  area: "sync",  autoLoad: !0,  saveDefaultWhenEmpty: !0,  defaultValue: {    anonId: crypto.randomUUID?.() ?? Math.random().toString(36).substring(2),    todayRemain: 2,    totalRemain: 5,    finished: !1,    showBanner: !0  }});

Readable background bundle: trial POST

background.js
async translateTrial(e) {  const t = ti.get(),    n = this.request({      url: `${this.apiUrl}/translate/demo`,      requestInit: {        method: "POST",        headers: {          "Content-Type": "application/json"        },        body: JSON.stringify({          anon_id: t.anonId,          langFrom: e.from,          langTo: e.to,          text: e.texts ?? [e.text]        })      }    });  try {    const {      limits: r,      result: s    } = await n, {      remaining_today: i,      remaining_total: a    } = r;    return t.todayRemain = i, t.totalRemain = a, t.finished = 0 === a, i || $s("promo_free_ai_translation_limit_daily", {}), a || $s("promo_free_ai_translation_limit_total", {}), this.toTranslationResult(s, e)  } catch (e) {    this.handleFreeApiError(e)  } finally {    $s("promo_free_ai_translation_used", {})  }}

Readable content script: button calls trial translation

content-script-injectable.js
async translateWithFreeTrial() {  this.lastParams.provider = T.Qk.XTRANSLATE_PRO;  const e = this.getPayloadParams(this.lastParams);  await this.translate(e, (() => (0, T.q4)().translateTrial(e))), this.isFreeTrialUsed = !0}
External destination
    • www.xtranslate.dev

    Receives the free-trial demo translation request with the anonymous trial ID, language settings, and text array.

Usage telemetry posts to Google Analytics with a persistent ID

XTranslate posts usage telemetry to Google Analytics after a screen view, tying it to a persistent GA client ID in sync storage, with parameters like platform and screen name.

The same sender also handles translation, history, TTS events.

Severity
Medium unwanted
Type
Unexpected
CWE
CWE-359
Source
Dynamic sandbox
What actually happens
You did this

You open or use a translation-related feature in the extension.

The observed event was a screen view for the translate screen; the same code also sends translation, history, text-to-speech, and promotion events.

The extension did this

The extension posts a Google Analytics event tied to the same stored installation ID.

The event includes a platform label and feature-specific parameters, such as screen name or language-pair details.

Captured request
POSThttps://www.google-analytics.com/mp/collect?measurement_id=G-HKWWGL29S8&api_secret=<redacted>

Observed during dynamic analysis: the request body contained a client_id beginning with a120cc5d and events[{name:screen_view, params:{platform:extension, screen_name:translate}}].

Headers
Content-Type
application/json
Telemetry fields sent in the request
  • Persistent installation ID
    client_id beginning with a120cc5d

    This lets multiple extension events be associated with the same browser installation.

  • Extension platform
    extension

    This marks the event as coming from the browser extension rather than another app or website.

  • Screen viewed
    translate

    This records which extension screen you opened.

  • Translation event details
    translate_used with provider google, lang_from auto, lang_to en

    For translation events, this describes the provider and language pair used for the request.

The code that does this

Stored GA client ID and Measurement Protocol sender

Readable version

Readable background bundle

background.js
const Ls = ys("ga_client_id", {  area: "sync",  defaultValue: crypto.randomUUID?.() ?? Math.random().toString(36).substring(2),  saveDefaultWhenEmpty: !0}),$s = Rr({  messageType: pr.GA_METRICS_SEND_EVENT,  handler: async function(e, t) {    try {      await Ls.load();      const n = "https://www.google-analytics.com/mp/collect?measurement_id=G-HKWWGL29S8&api_secret=<redacted>",        r = {          client_id: Ls.get(),          events: [{            name: e,            params: {              platform: "extension",              ...t            }          }]        };      return await fetch(n, {        method: "POST",        body: JSON.stringify(r),        headers: {          "Content-Type": "application/json"        }      })    } catch (n) {      console.warn(`GA event "${e}" failed to send: ${n}`, t)    }  }});

Screen-view event source

app.js
static bindPageIdWatcher() {  return (0, f.reaction)((() => mr().page), (e => {    jt("screen_view", {      screen_name: e    })  }), {    fireImmediately: !0  })}

Translation event source

app.js
async translate(e = {  ...this.params}) {  if (this.summarized = "", e.text && !se()(this.lastParams, e)) {    this.input?.focus(), this.input?.setValue(e.text), this.translation = null, this.error = null, this.isLoading = !0;    try {      this.lastParams = e;      const t = await Kt(e.provider).translate({        from: e.from,        to: e.to,        text: e.text.trim(),        mode: "lookup"      });      se()(e, this.params) && (this.translation = t, jt("translate_used", {        source: "translate_tab",        provider: e.provider,        lang_from: e.from,        lang_to: e.to      }))    } catch (t) {      se()(e, this.params) && (this.error = t, jt("translate_error", {        error: String(this.error?.message ?? t),        source: "translate_tab",        provider: e.provider,        lang_from: e.from,        lang_to: e.to      }))    } finally {      this.isLoading = !1, this.updateUrl(e)    }  }}
External destination
    • www.google-analytics.com

    Receives Measurement Protocol events containing the persistent client ID and extension usage parameters.

Where it sends data

Destinations our analysis observed XTranslate 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

    XTranslate sends data to www.google-analytics.com. A widely used service: 346 other extensions we have analysed send data here.

  • www.xtranslate.dev

    XTranslate sends data to www.xtranslate.dev. No other extension we have analysed sends data here.

Updated 30 September 2026gfgpkepllngchpmcippidfhmbhlljhoo