Is Manga Translator safe?
Manga Translator is high risk. Clicking Manga Translator's screenshot button makes the content script capture the area via the service worker and send the PNG data URL to api.ismanga.com/graphql, with the page URL, flagged as a screenshot translation.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Screenshot translation uploads the active-tab image
Clicking Manga Translator's screenshot button makes the content script capture the area via the service worker and send the PNG data URL to api.ismanga.com/graphql, with the page URL, flagged as a screenshot translation.
You click the screenshot translation button in the extension overlay.
The button is rendered as clickToTranslateButton in the content script.
The extension captures the visible tab image and submits it with the page URL.
The request goes to the Manga Translator GraphQL API for screenshot-based translation.
| Field | Value | Why it matters | |
|---|---|---|---|
Visible page image | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII= (illustrative) | Lets the translation service see the portion of the page captured for screenshot translation. | |
Current page URL | https://mangadex.org/chapter/7f3b0c2e-1a3d-4c5b-98f0-23a2b41c9d75/12 (illustrative) | Ties the screenshot to the page you were viewing when you asked for translation. | |
Screenshot marker | isScreenshot: true | Tells the service that this upload came from screenshot translation rather than URL translation. | |
Device identifier | 1d7d2df7-65d7-41e2-8f41-8a6e6b1c2d49 (illustrative) | Can link multiple translation requests from the same browser profile over time. |
| Accept | application/json |
| Content-Type | application/json |
The overlay click leads to a service-worker screenshot and GraphQL upload
onClick: function() {
var t = (0, e.A)(s().mark((function e(t) {
var n, a, r, o;
return s().wrap((function(e) {
for (;;) switch (e.prev = e.next) {
case 0:
if (t.preventDefault(), t.stopPropagation(), w) {
e.next = 1;
break
}
return window.open("".concat(k.ISMANGA_SUBSCRIPTION_PRICING_ENDPOINT, "/").concat((null == p ? void 0 : p.uid) || ""), "_blank"), e.abrupt("return");
case 1:
if (b.current && d && f) {
e.next = 2;
break
}
return e.abrupt("return");
case 2:
return b.current.style.visibility = "hidden", e.next = 3, (0, g.getTargetDataUrl)({
targetElement: d,
screenshotViewport: f
});
case 3:
return n = e.sent, a = (0, y.getDataUrlId)(n), r = Boolean, e.next = 4, c({
imgDataUrl: n,
imgDataUrlId: a
});
case 4:
o = e.sent, r(o) && (b.current.style.visibility = "visible");
case 5:
case "end":
return e.stop()
}
}), e)
})));
return function(e) {
return t.apply(this, arguments)
}
}()var ml = function() {
var e = t(r().mark((function e() {
var t, n;
return r().wrap((function(e) {
for (;;) switch (e.prev = e.next) {
case 0:
return e.next = 2, chrome.runtime.sendMessage({
code: "take_screenshot"
});
case 2:
return t = e.sent, n = t.payload, e.abrupt("return", n);
case 5:
case "end":
return e.stop()
}
}), e)
})));
return function() {
return e.apply(this, arguments)
}
}()chrome.runtime.onMessage.addListener((function(e, t, r) {
return "take_screenshot" === e.code && (n(s().mark((function e() {
var t;
return s().wrap((function(e) {
for (;;) switch (e.prev = e.next) {
case 0:
return e.next = 2, chrome.tabs.captureVisibleTab();
case 2:
t = e.sent, r({
code: xr,
payload: t
});
case 4:
case "end":
return e.stop()
}
}), e)
})))(), !0)
}))function(e) {
var t = e.imgDataUrl,
n = e.imgDataUrlId,
a = e.originUrl,
r = e.toLang,
s = void 0 === r ? o.LANG_CODE.EN_US : r,
l = e.fromLang,
u = e.isScreenshot,
c = void 0 !== u && u,
d = e.shouldSkipCache,
p = void 0 !== d && d,
f = e.shouldForceRefresh,
m = void 0 !== f && f,
g = e.idToken,
h = e.visitId,
b = e.deviceId,
y = JSON.stringify({
query: 'mutation {\n createMangaByDataUrl(\n dataUrl: "'.concat(t, '",\n dataUrlId: "').concat(n, '",\n originUrl: "').concat(a, '",\n langCode: "').concat(s, '",\n selectedLangCode: "').concat(l, '",\n isScreenshot: ').concat(c, ",\n shouldSkipCache: ").concat(p, ",\n shouldForceRefresh: ").concat(m, "\n ) { \n code\n payload {\n x, \n y, \n width \n height \n fromLang {\n code\n script\n }\n toLang {\n code\n script\n }\n imgWidth\n imgHeight\n inpaintingUrl\n colors {\n text\n background\n }\n }\n taskPayload {\n taskId\n url\n isPseudoUrl\n isHighQuality\n langCode\n shouldCache\n }\n }\n }\n ")
});
return (0, i.post)({
idToken: g,
visitId: h,
deviceId: b,
init: {
body: y
}
}).then((function(e) {
return e.json()
}))
}- api.ismanga.com
Receives the GraphQL createMangaByDataUrl request containing the screenshot PNG data URL, current page URL, language settings, and request metadata.
Tracked UI Clicks Post Event Metadata
Manga Translator attaches click tracking to UI elements and posts a logEvent mutation to api.ismanga.com/graphql.
The builder can serialize URL parts when supplied; the inspected handler omits URL.
Dynamic analysis saw no logEvent traffic.
You click a Manga Translator UI control on a page.
The click must occur on an element, or a parent element, with Manga Translator component metadata.
The extension prepares a GraphQL event record and posts it to Manga Translator's API.
The record includes the click event name, component, container, platform, and visit identifier; URL slots are populated only if the caller supplies a URL.
| Field | Value | Why it matters | |
|---|---|---|---|
Event name | Click | Describes the kind of interaction you made with the extension interface. | |
Clicked component | translateNowButton | Identifies which Manga Translator control you clicked. | |
Container | controlPanel | Adds the surrounding Manga Translator UI area for the clicked control when that metadata exists. | |
Visit identifier | 8f2f3d0a-5f7a-4e4f-9a21-0e9c9817d118 | Lets the service connect the click event to the same extension visit session. | |
Optional URL fields | hostname=mangadex.org; pathname=/chapter/6f4c8f3b; query=?tab=comments | Can describe the page path, host, and query string if a URL is supplied to the tracking helper. |
| Accept | application/json |
| Content-Type | application/json |
Click metadata extraction and logEvent POST construction
window.addEventListener("click", (function(e) {
var t;
t = {
event: e,
visitId: n,
idToken: l
}, (0, Da.handleClickTracking)(function(e) {
for (var t = 1; t < arguments.length; t++) {
var n = null != arguments[t] ? arguments[t] : {};
t % 2 ? lr(Object(n), !0).forEach((function(t) {
Ca(e, t, n[t])
})) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : lr(Object(n)).forEach((function(t) {
Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t))
}))
}
return e
}({
platform: "extension"
}, t))
}), {
capture: !0,
passive: !0
})i = function(e, t) {
for (var n = e instanceof HTMLElement ? e : null; n;) {
var a, r = null === (a = n.dataset) || void 0 === a ? void 0 : a[t];
if (r) return r;
n = n.parentElement
}
return null
};
s = function(n) {
var a = n.platform,
r = n.event,
o = n.idToken,
s = n.visitId,
l = n.url,
u = i(r.target, "ismangaComponent");
if (u) {
var c = i(r.target, "ismangaContainer"),
d = function(e) {
if (e) {
if ("string" != typeof e) return e;
try {
var t = new URL(e);
return {
pathname: t.pathname,
hostname: t.hostname,
query: t.search
}
} catch (e) {
return
}
}
}(l);
(0, e.default)({
eventName: t.TRACKING_EVENTS.CLICK,
idToken: o,
visitId: s,
component: u,
container: null != c ? c : "",
platform: null != a ? a : "",
url: d
})
}
}function(t) {
var n, a, r, s, l, u, c, d, p, f, m, g;
n = t.eventName;
a = t.url;
r = t.component;
s = t.container;
l = t.idToken;
u = t.visitId;
c = t.platform;
d = t.extra;
p = void 0 === d ? "" : d;
f = t.sessionId;
m = t.sessionDurationSeconds;
g = JSON.stringify({
query: 'mutation {\n logEvent(\n platform: "'.concat(c, '", \n url: {\n pathname: "').concat(null == a ? void 0 : a.pathname, '",\n hostname: "').concat(null == a ? void 0 : a.hostname, '",\n query: "').concat(null == a ? void 0 : a.query, '"\n },\n eventName: "').concat(n, '",\n component: "').concat(r, '",\n container: "').concat(s, '",\n extra: "').concat(p, '"\n ').concat(f ? ", sessionId: ".concat(f) : "", "\n ".concat(m ? ", sessionDurationSeconds: ".concat(m) : "", "\n ) {\n success\n message\n }\n }")
});
return (0, i.post)({
visitId: u,
idToken: l,
init: {
body: g
}
})
}- api.ismanga.com
Receives Manga Translator GraphQL requests, including logEvent when tracked UI controls call the event helper.
Persistent cross-session tracking UUID sent on every API call
Dynamic analysis captured 25 POSTs to api.ismanga.com/graphql in two sessions, each with the same ismanga-visit-id header, a UUID stored in chrome.storage.sync.
Sign-in triggers mergeTrackingId, linking anonymous history to your account.
You install the extension and visit any web page containing manga images.
The extension's content script is injected on all URLs.
The extension attaches a permanent UUID to every POST request it sends to its translation server.
The ismanga-visit-id header carries the same value on every request, in every tab, across every browsing session.
| Accept | application/json |
| Content-Type | application/json |
| Authorization | Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qif... |
| ismanga-visit-id | 196c1ba2-c14c-49cd-afda-dfbe923edb30 |
{
"query": "query { pollTranslationQueue(taskId: \"abc123\", isHighQuality: false, url: \"https://example-manga-site.com/chapter/1/page/5.jpg\", isPseudoUrl: false, langCode: \"en\", shouldCache: true) { code payload { x y width height } } }"
}The extension's permanent device identifier. Never cleared or rotated; syncs across every Chrome profile on the same Google account.
chrome.storage.sync key 'visitId'{
"visitId": "196c1ba2-c14c-49cd-afda-dfbe923edb30"
}UUID generation and persistence (content.bundle.js ~line 125139-125167)
var getOrCreateVisitId = async function(callback) {
try {
const stored = await chrome.storage.sync.get([VISIT_ID_KEY]);
let visitId = stored[VISIT_ID_KEY];
if (!visitId) {
visitId = uuidv4();
chrome.storage.sync.set({ [VISIT_ID_KEY]: visitId });
}
await callback?.(visitId);
} catch (err) {
throw new Error('Failed to get visitId', { cause: err });
}
};Anonymous-to-account linking on sign-in (content.bundle.js ~line 26979-26991)
var mergeTrackingId = function({ idToken, visitId }) {
const body = JSON.stringify({
query: `mutation {
mergeTrackingId {
success
}
}`
});
return post({ visitId, idToken, init: { body } });
};
// Called immediately after successful email or Google sign-in:
// ur((visitId) => mergeTrackingId({ idToken, visitId }));- api.ismanga.com
IsManga's GraphQL API. Receives every translation request and analytics event with the ismanga-visit-id UUID and, for signed-in users, a Firebase ID token.