Translate Fastmail message views inline with built-in web translators.
// ==UserScript==
// @name FastTrMail
// @namespace https://github.com/dylanwang/FastTrMail-Tampermonkey
// @version 0.1.1
// @author slippindylan
// @license MIT
// @description Translate Fastmail message views inline with built-in web translators.
// @icon https://raw.githubusercontent.com/SlippinDylan/FastTrMail-Tampermonkey/main/assets/app.png
// @match https://app.fastmail.com/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect edge.microsoft.com
// @connect api.cognitive.microsofttranslator.com
// @connect translate.googleapis.com
// ==/UserScript==
(() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// src/config/defaults.js
var require_defaults = __commonJS({
"src/config/defaults.js"(exports, module) {
module.exports = {
DEFAULT_SETTINGS: Object.freeze({
targetLanguage: "zh-CN",
preferredProvider: "edge-web"
}),
DOM_CONSTANTS: Object.freeze({
BUTTON_CLASS: "fmt-translate-button",
BUTTON_ACTIVE_CLASS: "fmt-translate-button-active",
INLINE_TRANSLATION_CLASS: "fmt-inline-translation",
TITLE_TRANSLATION_CLASS: "fmt-title-translation",
MESSAGE_STATUS_CLASS: "fmt-message-status",
SEGMENT_ANCHOR_CLASS: "fmt-segment-anchor",
TRANSLATION_WRAPPER_CLASS: "fmt-translation-wrapper",
MESSAGE_INSTANCE_ATTRIBUTE: "data-fmt-message-instance-id",
SEGMENT_ATTRIBUTE: "data-fmt-segment-id",
MODAL_OVERLAY_CLASS: "fmt-settings-overlay",
MODAL_CLASS: "fmt-settings-modal",
STYLES_ATTRIBUTE: "data-fasttrmail-styles"
})
};
}
});
// src/config/languages.js
var require_languages = __commonJS({
"src/config/languages.js"(exports, module) {
function freezeLanguageDefinition(definition) {
return Object.freeze({ ...definition });
}
module.exports = {
LANGUAGE_DEFINITIONS: Object.freeze([
freezeLanguageDefinition({ id: "zh-CN", label: "简体中文", microsoft: "zh-Hans", google: "zh-CN" }),
freezeLanguageDefinition({ id: "zh-TW", label: "繁體中文", microsoft: "zh-Hant", google: "zh-TW" }),
freezeLanguageDefinition({ id: "en", label: "English", microsoft: "en", google: "en" }),
freezeLanguageDefinition({ id: "ja", label: "日本語", microsoft: "ja", google: "ja" })
])
};
}
});
// src/config/providers.js
var require_providers = __commonJS({
"src/config/providers.js"(exports, module) {
function freezeProviderDefinition(definition) {
return Object.freeze({ ...definition });
}
module.exports = {
PROVIDER_DEFINITIONS: Object.freeze([
freezeProviderDefinition({ id: "edge-web" }),
freezeProviderDefinition({ id: "google-web" })
])
};
}
});
// src/core/runtime-state.js
var require_runtime_state = __commonJS({
"src/core/runtime-state.js"(exports, module) {
function createTitleState() {
return {
sourceText: "",
translatedText: "",
status: "idle",
error: "",
requestId: 0,
settingsRevision: 0
};
}
function createRuntimeState2({ getLocationKey }) {
const state = {
threadStates: /* @__PURE__ */ new WeakMap(),
activeThreadStates: /* @__PURE__ */ new Set(),
observerMuteDepth: 0,
observerControl: null,
documentRefreshScheduled: false,
observedThreadRoots: /* @__PURE__ */ new Set(),
observedThreadFlushTimer: 0,
messageInstanceCounter: 0,
segmentCounter: 0,
threadStateCounter: 0,
currentLocationKey: getLocationKey(),
documentGeneration: 0,
settingsRevision: 0
};
function nextSegmentId() {
const segmentId = `fmt-segment-${state.segmentCounter}`;
state.segmentCounter += 1;
return segmentId;
}
function nextMessageInstanceId() {
const messageInstanceId = `fmt-message-${state.messageInstanceCounter}`;
state.messageInstanceCounter += 1;
return messageInstanceId;
}
function nextThreadStateKey() {
const threadStateKey = `fmt-thread-${state.threadStateCounter}`;
state.threadStateCounter += 1;
return threadStateKey;
}
function withObserverMuted(task) {
const shouldSuspendObserver = state.observerMuteDepth === 0;
state.observerMuteDepth += 1;
if (shouldSuspendObserver) {
state.observerControl?.suspend?.();
}
try {
return task();
} finally {
state.observerMuteDepth -= 1;
if (state.observerMuteDepth === 0) {
state.observerControl?.resume?.();
}
}
}
function setObserverControl(observerControl) {
state.observerControl = observerControl || null;
if (!state.observerControl) {
return;
}
if (state.observerMuteDepth > 0) {
state.observerControl.suspend?.();
return;
}
state.observerControl.resume?.();
}
function createThreadState() {
return {
key: nextThreadStateKey(),
active: false,
cancelled: false,
generation: state.documentGeneration,
runId: 0,
root: null,
messages: /* @__PURE__ */ new Map(),
refreshTimer: 0,
processing: false,
pendingRefresh: false,
pendingRefreshDelayMs: 0,
pendingRequestIds: /* @__PURE__ */ new Set(),
title: createTitleState()
};
}
function createMessageState({ key, identity, instanceId }) {
return {
key,
identity,
instanceId,
status: "idle",
segmentSignature: "",
translatedSegments: null,
requestSerial: 0,
error: "",
settingsRevision: 0
};
}
function clearRefreshTimer(threadState) {
if (!threadState?.refreshTimer) {
return;
}
globalThis.clearTimeout(threadState.refreshTimer);
threadState.refreshTimer = 0;
}
function activateThreadState(threadState) {
if (!threadState) {
return;
}
threadState.active = true;
threadState.cancelled = false;
threadState.generation = state.documentGeneration;
state.activeThreadStates.add(threadState);
}
function deactivateThreadState(threadState) {
if (!threadState) {
return;
}
clearRefreshTimer(threadState);
threadState.active = false;
threadState.cancelled = true;
threadState.processing = false;
threadState.pendingRefresh = false;
threadState.pendingRefreshDelayMs = 0;
threadState.runId += 1;
state.activeThreadStates.delete(threadState);
if (threadState.title.status === "translating") {
threadState.title.status = threadState.title.translatedText ? "done" : "idle";
threadState.title.error = "";
}
for (const messageState of threadState.messages.values()) {
if (messageState.status === "translating") {
messageState.status = Array.isArray(messageState.translatedSegments) ? "translated" : "idle";
messageState.error = "";
}
}
}
function isThreadStateCurrent(threadState) {
return Boolean(threadState) && threadState.generation === state.documentGeneration;
}
function trackThreadRequest(threadState, requestId) {
if (threadState && requestId) {
threadState.pendingRequestIds.add(requestId);
}
}
function releaseThreadRequest(threadState, requestId) {
if (threadState && requestId) {
threadState.pendingRequestIds.delete(requestId);
}
}
function beginThreadRun(threadState) {
activateThreadState(threadState);
threadState.runId += 1;
return {
threadKey: threadState.key,
generation: threadState.generation,
runId: threadState.runId
};
}
function isRunCurrent(threadState, runToken) {
if (!threadState || !runToken) {
return false;
}
return threadState.active === true && threadState.cancelled !== true && threadState.generation === runToken.generation && threadState.runId === runToken.runId && state.currentLocationKey === getLocationKey();
}
function hasLocationChanged() {
return state.currentLocationKey !== getLocationKey();
}
function syncCurrentLocationKey() {
state.currentLocationKey = getLocationKey();
return state.currentLocationKey;
}
function advanceDocumentGeneration() {
syncCurrentLocationKey();
state.documentGeneration += 1;
return state.documentGeneration;
}
function advanceSettingsRevision() {
state.settingsRevision += 1;
return state.settingsRevision;
}
return {
state,
nextSegmentId,
nextMessageInstanceId,
nextThreadStateKey,
withObserverMuted,
setObserverControl,
createTitleState,
createThreadState,
createMessageState,
clearRefreshTimer,
activateThreadState,
deactivateThreadState,
isThreadStateCurrent,
trackThreadRequest,
releaseThreadRequest,
beginThreadRun,
isRunCurrent,
hasLocationChanged,
syncCurrentLocationKey,
advanceDocumentGeneration,
advanceSettingsRevision
};
}
module.exports = { createRuntimeState: createRuntimeState2 };
}
});
// src/diagnostics/diagnostics.js
var require_diagnostics = __commonJS({
"src/diagnostics/diagnostics.js"(exports, module) {
var MAX_ENTRY_COUNT = 200;
var MAX_STRING_LENGTH = 160;
var CONSOLE_SUPPRESSED_EVENTS = /* @__PURE__ */ new Set([
"thread.refresh.start",
"thread.refresh.collected"
]);
function createNoopDiagnostics2() {
return {
isEnabled() {
return false;
},
setEnabled() {
},
record() {
},
recordError() {
},
getEntries() {
return [];
}
};
}
function createDiagnostics2({
enabled = false,
maxEntries = MAX_ENTRY_COUNT,
now = Date.now,
console: console2 = globalThis.console
} = {}) {
let isEnabled = Boolean(enabled);
let sequence = 0;
const entries = [];
function pushEntry(entry) {
entries.push(entry);
if (entries.length > maxEntries) {
entries.shift();
}
}
function trimString(value) {
if (typeof value !== "string" || value.length <= MAX_STRING_LENGTH) {
return value;
}
return `${value.slice(0, MAX_STRING_LENGTH)}…`;
}
function sanitizeValue(value) {
if (Array.isArray(value)) {
return value.map((item) => sanitizeValue(item));
}
if (!value || typeof value !== "object") {
return trimString(value);
}
const output = {};
for (const [key, entryValue] of Object.entries(value)) {
output[key] = sanitizeValue(entryValue);
}
return output;
}
function summarizeError(error) {
return sanitizeValue({
code: typeof error?.code === "string" ? error.code : "",
message: typeof error?.message === "string" ? error.message : String(error || ""),
name: typeof error?.name === "string" ? error.name : "",
status: typeof error?.status === "number" ? error.status : void 0,
statusText: typeof error?.statusText === "string" ? error.statusText : void 0,
metadata: error?.metadata && typeof error.metadata === "object" ? error.metadata : void 0
});
}
function record(event, payload = {}) {
if (!isEnabled) {
return null;
}
return emit(event, payload);
}
function emit(event, payload = {}) {
const entry = {
sequence: sequence + 1,
timestamp: now(),
event,
...sanitizeValue(payload)
};
sequence += 1;
pushEntry(entry);
if (!CONSOLE_SUPPRESSED_EVENTS.has(event)) {
console2?.info?.(`[FastTrMail][diagnostics][${event}]`, entry);
}
return entry;
}
function recordError(event, error, payload = {}) {
return record(event, {
...payload,
error: summarizeError(error)
});
}
return {
isEnabled() {
return isEnabled;
},
setEnabled(nextValue) {
const nextEnabled = Boolean(nextValue);
if (nextEnabled === isEnabled) {
return nextEnabled;
}
isEnabled = nextEnabled;
emit("diagnostics.state.changed", {
enabled: nextEnabled
});
return nextEnabled;
},
record,
recordError,
getEntries() {
return entries.slice();
}
};
}
module.exports = {
createDiagnostics: createDiagnostics2,
createNoopDiagnostics: createNoopDiagnostics2
};
}
});
// src/core/app.js
var require_app = __commonJS({
"src/core/app.js"(exports, module) {
var { createNoopDiagnostics: createNoopDiagnostics2 } = require_diagnostics();
function createApp2({
defaults,
document,
diagnostics = createNoopDiagnostics2(),
i18n,
policies,
renderer,
requestRegistry,
runtimeState,
segmenter,
settingsModal,
settingsStore,
threadDom,
translateService,
window = globalThis.window
}) {
let currentSettings = {
targetLanguage: defaults.targetLanguage,
preferredProvider: defaults.preferredProvider
};
let settingsLoadPromise = null;
async function ensureSettings() {
if (!settingsLoadPromise) {
settingsLoadPromise = readSettingsSnapshot().then((value) => {
currentSettings = value;
return value;
}).finally(() => {
settingsLoadPromise = null;
});
}
return settingsLoadPromise;
}
async function ensureTargetLanguageId() {
const settings = await ensureSettings();
return settings.targetLanguage;
}
async function persistSettings(nextSettings) {
currentSettings = await writeSettingsSnapshot(nextSettings);
runtimeState.advanceSettingsRevision();
invalidateActiveThreadTranslations();
refreshActiveThreads();
return currentSettings;
}
function getLanguageDefinition() {
return translateService.getLanguageDefinition(currentSettings.targetLanguage);
}
function injectButtons(root = document) {
threadDom.injectButtons(root, onTranslateClick);
}
async function runOpenSettings() {
const resolvedSettings = await ensureSettings();
settingsModal.open({
currentSettings: resolvedSettings,
onSave: persistSettings
});
}
function openSettings() {
return runOpenSettings().catch((error) => {
diagnostics.recordError("ui.settings.open.unexpected-error", error, {});
});
}
function cancelPendingThreadRequests(threadState) {
if (!threadState?.pendingRequestIds || threadState.pendingRequestIds.size === 0) {
return;
}
const requestIds = Array.from(threadState.pendingRequestIds);
threadState.pendingRequestIds.clear();
requestRegistry.cancelMany(requestIds);
}
function invalidateThreadTranslationState(threadState) {
if (!threadState) {
return;
}
const nextSettingsRevision = runtimeState.state.settingsRevision;
threadState.title.translatedText = "";
threadState.title.status = "idle";
threadState.title.error = "";
threadState.title.requestId += 1;
threadState.title.settingsRevision = nextSettingsRevision;
for (const messageState of threadState.messages.values()) {
messageState.segmentSignature = "";
messageState.translatedSegments = null;
messageState.status = "idle";
messageState.error = "";
messageState.requestSerial += 1;
messageState.settingsRevision = nextSettingsRevision;
}
}
function invalidateActiveThreadTranslations() {
for (const threadState of Array.from(runtimeState.state.activeThreadStates)) {
cancelPendingThreadRequests(threadState);
invalidateThreadTranslationState(threadState);
if (threadState.root instanceof globalThis.HTMLElement) {
renderer.clearThreadRenderArtifacts(threadState.root);
}
}
}
function restoreThread(threadRoot) {
const threadState = threadDom.getExistingThreadState(threadRoot, { includeStale: true });
if (threadState) {
cancelPendingThreadRequests(threadState);
}
threadDom.resetThreadState(threadRoot);
threadDom.syncThreadButtons(threadRoot, false);
renderer.clearThreadRenderArtifacts(threadRoot);
threadDom.clearThreadDomState(threadRoot);
}
function disposeThread(threadRoot) {
const threadState = threadDom.getExistingThreadState(threadRoot, { includeStale: true });
if (threadState) {
cancelPendingThreadRequests(threadState);
}
threadDom.resetThreadState(threadRoot);
renderer.clearThreadRenderArtifacts(threadRoot);
threadDom.clearThreadDomState(threadRoot);
}
function reportLifecycleError(event, error) {
diagnostics.recordError(event, error, {});
}
async function runTranslateClick(event) {
const button = event.currentTarget;
if (!(button instanceof globalThis.HTMLElement)) {
diagnostics.record("ui.translate-click.invalid-target", {
targetType: typeof button
});
return;
}
diagnostics.record("ui.translate-click.received", {
buttonClassName: button.className || ""
});
const threadRoot = threadDom.findThreadRoot(button);
if (!threadRoot) {
diagnostics.record("ui.translate-click.thread-missing", {
buttonClassName: button.className || ""
});
return;
}
const threadState = threadDom.getExistingThreadState(threadRoot);
const shouldRestoreOriginal = threadState?.active === true || renderer.hasThreadRenderArtifacts(threadRoot);
if (shouldRestoreOriginal) {
diagnostics.record("ui.translate-click.restore", {
threadKey: threadState?.key || ""
});
restoreThread(threadRoot);
return;
}
await ensureSettings();
const nextThreadState = threadDom.ensureThreadState(threadRoot);
diagnostics.record("ui.translate-click.activate", {
threadKey: nextThreadState.key
});
runtimeState.activateThreadState(nextThreadState);
threadDom.syncThreadButtons(threadRoot, true);
scheduleThreadRefresh(threadRoot, { immediate: true });
}
function onTranslateClick(event) {
return runTranslateClick(event).catch((error) => {
diagnostics.recordError("ui.translate-click.unexpected-error", error, {});
});
}
function scheduleThreadRefresh(threadRoot, { immediate = false, delayMs } = {}) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return;
}
const state = threadDom.getExistingThreadState(threadRoot);
if (!state?.active) {
return;
}
if (state.processing) {
state.pendingRefresh = true;
return;
}
if (state.refreshTimer) {
return;
}
const delay = typeof delayMs === "number" ? Math.max(0, delayMs) : immediate ? 0 : 80;
state.refreshTimer = window.setTimeout(() => {
state.refreshTimer = 0;
void refreshThreadSafely(threadRoot, state);
}, delay);
}
async function refreshThreadSafely(threadRoot, threadState) {
try {
await refreshThread(threadRoot, threadState);
} catch (error) {
diagnostics.recordError("thread.refresh.unexpected-error", error, {
threadKey: threadState?.key || ""
});
}
}
async function refreshThread(threadRoot, threadState) {
if (!(threadRoot instanceof globalThis.HTMLElement) || !threadRoot.isConnected) {
return;
}
const liveThreadState = threadDom.getExistingThreadState(threadRoot, { includeStale: true });
if (liveThreadState !== threadState || threadState.active !== true || threadState.cancelled === true) {
return;
}
if (threadState.processing) {
threadState.pendingRefresh = true;
return;
}
threadState.processing = true;
const runToken = runtimeState.beginThreadRun(threadState);
const textCache = typeof segmenter.createTextExtractionCache === "function" ? segmenter.createTextExtractionCache() : null;
diagnostics.record("thread.refresh.start", {
threadKey: threadState.key,
runId: runToken.runId
});
try {
threadDom.syncThreadButtons(threadRoot, true);
const descriptors = threadDom.collectMessageDescriptors(threadRoot, { textCache });
threadDom.reconcileMessageStates(threadState, descriptors);
diagnostics.record("thread.refresh.collected", {
threadKey: threadState.key,
runId: runToken.runId,
messageCount: descriptors.length
});
if (!runtimeState.isRunCurrent(threadState, runToken)) {
return;
}
await Promise.all([
processThreadTitle(threadRoot, threadState, runToken),
processMessageDescriptors(threadRoot, threadState, descriptors, runToken, { textCache })
]);
} finally {
threadState.processing = false;
if (threadState.pendingRefresh && threadState.active && !threadState.cancelled) {
const delay = threadState.pendingRefreshDelayMs || 0;
threadState.pendingRefresh = false;
threadState.pendingRefreshDelayMs = 0;
scheduleThreadRefresh(threadRoot, { delayMs: delay });
}
}
}
async function processThreadTitle(threadRoot, threadState, runToken) {
const titleState = threadState.title;
const settingsRevision = runtimeState.state.settingsRevision;
const titleElement = threadDom.findTitleElement(threadRoot);
const titleText = normalizeTranslationText(titleElement?.textContent || "");
if (!(titleElement instanceof globalThis.HTMLElement) || !titleText || !policies.hasTranslatableText(titleText)) {
threadState.title = runtimeState.createTitleState();
renderer.clearTitleTranslation(threadRoot);
return;
}
if (titleState.sourceText === titleText && titleState.status === "done" && titleState.translatedText && titleState.settingsRevision === settingsRevision) {
if (runtimeState.isRunCurrent(threadState, runToken)) {
renderer.renderTitleTranslation(threadRoot, titleElement, titleState.translatedText, "done");
}
return;
}
const requestId = titleState.requestId + 1;
titleState.sourceText = titleText;
titleState.translatedText = "";
titleState.status = "translating";
titleState.error = "";
titleState.requestId = requestId;
titleState.settingsRevision = settingsRevision;
renderer.renderTitleTranslation(threadRoot, titleElement, i18n.t("content.loading"), "loading");
const translationRequestId = `title:${threadState.key}:${runToken.runId}:${requestId}`;
const request = translateService.translateSegments(
[titleText],
getLanguageDefinition(),
{ preferredProvider: currentSettings.preferredProvider }
);
requestRegistry.register(translationRequestId, request);
runtimeState.trackThreadRequest(threadState, translationRequestId);
try {
const response = await request;
const translatedTitle = Array.isArray(response?.translatedSegments) ? response.translatedSegments[0] : "";
if (!translatedTitle) {
throw new Error(i18n.t("content.titleTranslationEmpty"));
}
if (!isTitleRequestCurrent(threadState, requestId, titleText, settingsRevision)) {
return;
}
threadState.title.translatedText = translatedTitle;
threadState.title.status = "done";
threadState.title.error = "";
if (!runtimeState.isRunCurrent(threadState, runToken)) {
return;
}
const liveTitleElement = threadDom.findTitleElement(threadRoot);
if (liveTitleElement instanceof globalThis.HTMLElement) {
renderer.renderTitleTranslation(threadRoot, liveTitleElement, translatedTitle, "done");
}
} catch (error) {
if (!isTitleRequestCurrent(threadState, requestId, titleText, settingsRevision)) {
return;
}
const message = resolveErrorMessage(error, i18n.t("content.titleTranslationFailed"));
threadState.title.translatedText = "";
threadState.title.status = "error";
threadState.title.error = message;
if (!runtimeState.isRunCurrent(threadState, runToken)) {
return;
}
const liveTitleElement = threadDom.findTitleElement(threadRoot);
if (liveTitleElement instanceof globalThis.HTMLElement) {
renderer.renderTitleTranslation(threadRoot, liveTitleElement, message, "error");
}
} finally {
requestRegistry.release(translationRequestId);
runtimeState.releaseThreadRequest(threadState, translationRequestId);
}
}
async function processMessageDescriptors(threadRoot, threadState, descriptors, runToken, { textCache } = {}) {
for (const descriptor of descriptors) {
if (!runtimeState.isRunCurrent(threadState, runToken)) {
return;
}
const messageState = descriptor.state;
const liveElements = threadDom.findLiveMessageElements(threadRoot, descriptor, { textCache });
descriptor.body = liveElements.body;
descriptor.contentRoot = liveElements.contentRoot || descriptor.body;
renderer.clearMessageStatus(descriptor);
if (!descriptor.body) {
renderer.clearMessageTranslations?.(descriptor);
if (threadDom.isMessageBodyDeferred(descriptor)) {
messageState.status = "pending-body";
messageState.error = "";
} else {
messageState.status = "pending-body";
messageState.error = i18n.t("content.bodyNotFound");
renderer.renderMessageStatus(descriptor, messageState.error, "error");
}
continue;
}
const segments = segmenter.collectTranslatableSegments(descriptor.contentRoot || descriptor.body, { textCache });
if (segments.length === 0) {
renderer.clearMessageTranslations?.(descriptor);
messageState.status = "no-segments";
messageState.error = i18n.t("content.noSegments");
renderer.renderMessageStatus(descriptor, messageState.error, "error");
continue;
}
const segmentSignature = segmenter.getSegmentSignature(segments);
descriptor.segments = segments;
descriptor.segmentSignature = segmentSignature;
renderer.pruneMessageTranslations?.(descriptor, segments);
if (Array.isArray(messageState.translatedSegments) && messageState.translatedSegments.length === segments.length && messageState.segmentSignature === segmentSignature && messageState.settingsRevision === runtimeState.state.settingsRevision) {
const renderResult = renderer.renderTranslatedSegments(segments, messageState.translatedSegments);
messageState.status = renderResult.ok ? "translated" : "render-pending";
messageState.error = renderResult.ok ? "" : "cached-render-failed";
continue;
}
await translateMessageDescriptor(threadState, descriptor, runToken);
}
}
async function translateMessageDescriptor(threadState, descriptor, runToken) {
const messageState = descriptor.state;
const segments = descriptor.segments || [];
const settingsRevision = runtimeState.state.settingsRevision;
if (segments.length === 0 || !runtimeState.isRunCurrent(threadState, runToken)) {
return;
}
messageState.status = "translating";
messageState.error = "";
messageState.settingsRevision = settingsRevision;
renderer.renderLoadingTranslations(segments);
messageState.requestSerial += 1;
const requestSerial = messageState.requestSerial;
const requestId = `message:${threadState.key}:${messageState.instanceId}:${requestSerial}`;
const request = translateService.translateSegments(
segments.map((segment) => segment.text),
getLanguageDefinition(),
{ preferredProvider: currentSettings.preferredProvider }
);
diagnostics.record("thread.message.translate.start", {
messageKey: messageState.key,
segmentCount: segments.length,
requestId
});
requestRegistry.register(requestId, request);
runtimeState.trackThreadRequest(threadState, requestId);
try {
const response = await request;
if (!runtimeState.isRunCurrent(threadState, runToken) || messageState.requestSerial !== requestSerial || messageState.settingsRevision !== settingsRevision) {
return;
}
messageState.segmentSignature = descriptor.segmentSignature || "";
messageState.translatedSegments = response.translatedSegments;
messageState.status = "translated";
messageState.error = "";
renderer.clearMessageStatus(descriptor);
const renderResult = renderer.renderTranslatedSegments(segments, response.translatedSegments);
if (!renderResult?.ok) {
diagnostics.record("thread.message.render.failed", {
messageKey: messageState.key,
phase: "fresh-translation",
failedSegments: renderResult?.failedSegments || []
});
}
} catch (error) {
if (!runtimeState.isRunCurrent(threadState, runToken) || messageState.requestSerial !== requestSerial || messageState.settingsRevision !== settingsRevision) {
return;
}
const message = resolveErrorMessage(error, i18n.t("content.translationFailed"));
messageState.status = "error";
messageState.error = message;
diagnostics.recordError("thread.message.translate.error", error, {
messageKey: messageState.key,
requestId
});
renderer.renderSegmentError(segments, message);
} finally {
requestRegistry.release(requestId);
runtimeState.releaseThreadRequest(threadState, requestId);
}
}
function resetDocumentTranslationState() {
runtimeState.syncCurrentLocationKey();
runtimeState.advanceDocumentGeneration();
for (const threadState of Array.from(runtimeState.state.activeThreadStates)) {
cancelPendingThreadRequests(threadState);
runtimeState.deactivateThreadState(threadState);
}
runtimeState.state.activeThreadStates.clear();
renderer.clearThreadRenderArtifacts(document.body || document.documentElement);
threadDom.clearThreadDomState(document);
}
function refreshActiveThreads() {
for (const threadState of Array.from(runtimeState.state.activeThreadStates)) {
if (threadState.root instanceof globalThis.HTMLElement && threadState.root.isConnected) {
scheduleThreadRefresh(threadState.root, { immediate: true });
}
}
}
function isTitleRequestCurrent(threadState, requestId, titleText, settingsRevision) {
return threadState.title.requestId === requestId && threadState.title.sourceText === titleText && threadState.title.settingsRevision === settingsRevision;
}
function normalizeTranslationText(text) {
return String(text || "").replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
}
async function readSettingsSnapshot() {
if (typeof settingsStore.getSettings === "function") {
return settingsStore.getSettings();
}
const [targetLanguage, preferredProvider] = await Promise.all([
typeof settingsStore.getTargetLanguage === "function" ? settingsStore.getTargetLanguage() : defaults.targetLanguage,
typeof settingsStore.getPreferredProvider === "function" ? settingsStore.getPreferredProvider() : defaults.preferredProvider
]);
return { targetLanguage, preferredProvider };
}
async function writeSettingsSnapshot(nextSettings) {
if (typeof settingsStore.setSettings === "function") {
return settingsStore.setSettings(nextSettings);
}
const targetLanguage = typeof settingsStore.setTargetLanguage === "function" ? await settingsStore.setTargetLanguage(nextSettings?.targetLanguage) : defaults.targetLanguage;
const preferredProvider = typeof settingsStore.setPreferredProvider === "function" ? await settingsStore.setPreferredProvider(nextSettings?.preferredProvider) : defaults.preferredProvider;
return { targetLanguage, preferredProvider };
}
function resolveErrorMessage(error, fallbackMessage) {
if (typeof error?.message === "string" && error.message.trim()) {
return error.message.trim();
}
return fallbackMessage;
}
return {
ensureTargetLanguageId,
disposeThread,
injectButtons,
onTranslateClick,
openSettings,
reportLifecycleError,
refreshActiveThreads,
refreshThread,
resetDocumentTranslationState,
restoreThread,
scheduleThreadRefresh
};
}
module.exports = { createApp: createApp2 };
}
});
// src/core/lifecycle.js
var require_lifecycle = __commonJS({
"src/core/lifecycle.js"(exports, module) {
function createLifecycle2({
app,
constants,
document,
runtimeState,
threadDom,
window = globalThis.window
}) {
const TRANSLATE_CLICK_HANDLED = /* @__PURE__ */ Symbol.for("fasttrmail.translateClickHandled");
let observer = null;
let initialized = false;
let observing = false;
let translationSelectionGestureActive = false;
function initialize() {
if (initialized) {
return;
}
initialized = true;
observer = new MutationObserver((mutations) => safelyRunLifecycleWork("lifecycle.observer.unexpected-error", () => {
if (runtimeState.state.observerMuteDepth > 0) {
return;
}
if (runtimeState.hasLocationChanged()) {
scheduleDocumentRefresh();
return;
}
if (isTranslationSelectionActive()) {
return;
}
const affectedThreadRoots = collectAffectedThreadRoots(mutations);
if (affectedThreadRoots.size > 0) {
scheduleObservedThreadRefresh(affectedThreadRoots);
}
}));
runtimeState.setObserverControl({
suspend: suspendObserver,
resume: resumeObserver
});
app.injectButtons(document);
document.addEventListener("click", handleDocumentClick, true);
document.addEventListener("mousedown", handleDocumentMouseDown, true);
document.addEventListener("mouseup", handleDocumentMouseUp, true);
document.addEventListener("selectionchange", handleDocumentSelectionChange, true);
window.addEventListener?.("blur", clearTranslationSelectionGesture, true);
resumeObserver();
}
function suspendObserver() {
if (!observer || !observing) {
return;
}
observer.disconnect();
observing = false;
}
function resumeObserver() {
if (!initialized || !observer || observing || runtimeState.state.observerMuteDepth > 0) {
return;
}
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ["class", "aria-hidden", "hidden", "style", "inert"]
});
observing = true;
}
function handleDocumentClick(event) {
if (event?.[TRANSLATE_CLICK_HANDLED]) {
return;
}
const target = event.target;
if (!(target instanceof globalThis.Element)) {
return;
}
const button = target.closest(`.${constants.BUTTON_CLASS}`);
if (!(button instanceof globalThis.HTMLElement)) {
return;
}
if (threadDom.isManagedTranslateButton?.(button)) {
return;
}
event[TRANSLATE_CLICK_HANDLED] = true;
void app.onTranslateClick({
currentTarget: button
});
}
function handleDocumentMouseDown(event) {
translationSelectionGestureActive = isManagedTranslationNode(event?.target);
}
function handleDocumentMouseUp() {
window.setTimeout(clearTranslationSelectionGestureIfInactive, 0);
}
function handleDocumentSelectionChange() {
clearTranslationSelectionGestureIfInactive();
}
function clearTranslationSelectionGesture() {
translationSelectionGestureActive = false;
}
function clearTranslationSelectionGestureIfInactive() {
if (!isSelectionInsideManagedTranslation()) {
clearTranslationSelectionGesture();
}
}
function scheduleDocumentRefresh() {
if (runtimeState.state.documentRefreshScheduled) {
return;
}
runtimeState.state.documentRefreshScheduled = true;
window.setTimeout(() => {
runtimeState.state.documentRefreshScheduled = false;
safelyRunLifecycleWork("lifecycle.document-refresh.unexpected-error", () => {
if (runtimeState.hasLocationChanged()) {
app.resetDocumentTranslationState();
}
clearObservedThreadRefreshQueue();
app.injectButtons(document);
disposeDetachedThreadStates();
});
}, 0);
}
function scheduleObservedThreadRefresh(threadRoots) {
for (const threadRoot of threadRoots) {
if (threadRoot instanceof globalThis.HTMLElement) {
runtimeState.state.observedThreadRoots.add(threadRoot);
}
}
if (runtimeState.state.observedThreadFlushTimer) {
return;
}
runtimeState.state.observedThreadFlushTimer = window.setTimeout(() => {
const roots = Array.from(runtimeState.state.observedThreadRoots);
clearObservedThreadRefreshQueue();
safelyRunLifecycleWork("lifecycle.thread-refresh.unexpected-error", () => {
if (runtimeState.hasLocationChanged()) {
scheduleDocumentRefresh();
return;
}
disposeDetachedThreadStates();
for (const threadRoot of roots) {
if (!(threadRoot instanceof globalThis.HTMLElement) || !threadRoot.isConnected) {
continue;
}
app.injectButtons(threadRoot);
const state = threadDom.getExistingThreadState(threadRoot);
if (state?.active) {
app.scheduleThreadRefresh(threadRoot, { immediate: true });
}
}
});
}, 0);
}
function safelyRunLifecycleWork(event, task) {
try {
return task();
} catch (error) {
app.reportLifecycleError?.(event, error);
return void 0;
}
}
function disposeDetachedThreadStates() {
const detachedThreadRoots = typeof threadDom.collectDetachedActiveThreadRoots === "function" ? threadDom.collectDetachedActiveThreadRoots() : [];
for (const threadRoot of detachedThreadRoots) {
app.disposeThread?.(threadRoot);
}
threadDom.pruneDetachedThreadStates();
}
function clearObservedThreadRefreshQueue() {
if (runtimeState.state.observedThreadFlushTimer) {
window.clearTimeout(runtimeState.state.observedThreadFlushTimer);
runtimeState.state.observedThreadFlushTimer = 0;
}
runtimeState.state.observedThreadRoots.clear();
}
function collectAffectedThreadRoots(mutations) {
const affectedThreadRoots = /* @__PURE__ */ new Set();
for (const mutation of mutations) {
if (isInternalMutation(mutation)) {
continue;
}
if (mutation.type === "attributes") {
if (shouldReactToAttributeMutation(mutation.target)) {
addAffectedThreadRootsFromNode(mutation.target, affectedThreadRoots);
}
continue;
}
if (mutation.type === "characterData") {
addAffectedThreadRootsFromNode(getMutationTargetElement(mutation.target), affectedThreadRoots);
continue;
}
addAffectedThreadRootsFromNode(mutation.target, affectedThreadRoots);
for (const node of mutation.addedNodes || []) {
addAffectedThreadRootsFromNode(node, affectedThreadRoots);
}
for (const node of mutation.removedNodes || []) {
addAffectedThreadRootsFromNode(node, affectedThreadRoots);
}
}
return affectedThreadRoots;
}
function addAffectedThreadRootsFromNode(node, affectedThreadRoots) {
if (!(node instanceof globalThis.HTMLElement) || isInternalNode(node)) {
return;
}
const directThreadRoot = node.matches(".v-Thread") ? node : node.closest(".v-Thread");
if (directThreadRoot instanceof globalThis.HTMLElement) {
affectedThreadRoots.add(directThreadRoot);
}
const shouldResolveRelatedThreadRoot = node.classList.contains(constants.BUTTON_CLASS) || node.closest?.(`.${constants.BUTTON_CLASS}`) !== null;
const relatedThreadRoot = shouldResolveRelatedThreadRoot ? threadDom.findThreadRoot(node) : null;
if (relatedThreadRoot instanceof globalThis.HTMLElement) {
affectedThreadRoots.add(relatedThreadRoot);
}
node.querySelectorAll(".v-Thread").forEach((threadRoot) => {
if (threadRoot instanceof globalThis.HTMLElement) {
affectedThreadRoots.add(threadRoot);
}
});
}
function getMutationTargetElement(node) {
if (node instanceof globalThis.HTMLElement) {
return node;
}
if (node instanceof globalThis.Node) {
return node.parentElement;
}
return null;
}
function isInternalMutation(mutation) {
const nodes = [
...Array.from(mutation.addedNodes || []),
...Array.from(mutation.removedNodes || [])
];
if (mutation.type === "attributes") {
return isInternalNode(mutation.target);
}
if (mutation.type === "characterData") {
return isInternalNode(getMutationTargetElement(mutation.target));
}
return nodes.length > 0 && nodes.every((node) => isInternalChildMutationNode(node));
}
function isInternalChildMutationNode(node) {
if (!(node instanceof globalThis.HTMLElement)) {
return false;
}
const translateButton = getTranslateButtonNode(node);
if (translateButton && !threadDom.isManagedTranslateButton?.(translateButton)) {
return false;
}
return isInternalNode(node);
}
function getTranslateButtonNode(node) {
if (!(node instanceof globalThis.HTMLElement)) {
return null;
}
if (node.classList.contains(constants.BUTTON_CLASS)) {
return node;
}
return node.closest?.(`.${constants.BUTTON_CLASS}`) || null;
}
function isInternalNode(node) {
if (!(node instanceof globalThis.HTMLElement)) {
return false;
}
return node.classList.contains(constants.BUTTON_CLASS) || node.classList.contains(constants.INLINE_TRANSLATION_CLASS) || node.classList.contains(constants.MESSAGE_STATUS_CLASS) || node.classList.contains(constants.SEGMENT_ANCHOR_CLASS) || node.classList.contains(constants.TITLE_TRANSLATION_CLASS) || node.classList.contains(constants.TRANSLATION_WRAPPER_CLASS) || node.classList.contains(constants.MODAL_OVERLAY_CLASS) || node.closest?.(`.${constants.BUTTON_CLASS}`) !== null || node.closest?.(`.${constants.INLINE_TRANSLATION_CLASS}`) !== null || node.closest?.(`.${constants.MESSAGE_STATUS_CLASS}`) !== null || node.closest?.(`.${constants.MODAL_OVERLAY_CLASS}`) !== null;
}
function isTranslationSelectionActive() {
return translationSelectionGestureActive || isSelectionInsideManagedTranslation();
}
function isSelectionInsideManagedTranslation() {
const selection = typeof document.getSelection === "function" ? document.getSelection() : null;
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
return false;
}
for (let index = 0; index < selection.rangeCount; index += 1) {
const range = selection.getRangeAt(index);
if (isManagedTranslationNode(range.commonAncestorContainer)) {
return true;
}
}
return isManagedTranslationNode(selection.anchorNode) || isManagedTranslationNode(selection.focusNode);
}
function isManagedTranslationNode(node) {
if (node instanceof globalThis.HTMLElement) {
return Boolean(
node.closest(`.${constants.INLINE_TRANSLATION_CLASS}`) || node.closest(`.${constants.TITLE_TRANSLATION_CLASS}`)
);
}
if (node instanceof globalThis.Node) {
return isManagedTranslationNode(node.parentElement);
}
return false;
}
function shouldReactToAttributeMutation(target) {
if (!(target instanceof globalThis.HTMLElement)) {
return false;
}
if (target.closest(`.${constants.INLINE_TRANSLATION_CLASS}`) || target.closest(`.${constants.TITLE_TRANSLATION_CLASS}`) || target.closest(`.${constants.MESSAGE_STATUS_CLASS}`) || target.classList.contains(constants.BUTTON_CLASS)) {
return false;
}
return Boolean(
target.closest(".v-Message") || target.closest(".v-MessageCard") || target.closest(".v-Thread")
);
}
return { initialize };
}
module.exports = { createLifecycle: createLifecycle2 };
}
});
// src/diagnostics/store.js
var require_store = __commonJS({
"src/diagnostics/store.js"(exports, module) {
function assertConfig(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function normalizeEnabled(value) {
return value === true || value === 1 || value === "1" || value === "true";
}
function createDiagnosticsStore2({ tmApi }) {
assertConfig(tmApi && typeof tmApi.getValue === "function", "tmApi.getValue must be a function");
assertConfig(tmApi && typeof tmApi.setValue === "function", "tmApi.setValue must be a function");
async function getEnabled() {
return normalizeEnabled(await tmApi.getValue("diagnosticsEnabled", false));
}
async function setEnabled(value) {
const normalizedValue = Boolean(value);
await tmApi.setValue("diagnosticsEnabled", normalizedValue);
return normalizedValue;
}
async function toggleEnabled() {
const nextValue = !await getEnabled();
return setEnabled(nextValue);
}
return {
getEnabled,
setEnabled,
toggleEnabled
};
}
module.exports = { createDiagnosticsStore: createDiagnosticsStore2 };
}
});
// src/fastmail/segmenter.js
var require_segmenter = __commonJS({
"src/fastmail/segmenter.js"(exports, module) {
function normalizeKeyText(text) {
return String(text || "").replace(/\s+/g, " ").trim();
}
function normalizeSegmentText(text) {
return String(text || "").replace(/\r\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
}
function shouldSkipSegmentText(text) {
const compact = normalizeKeyText(text);
if (!compact) {
return true;
}
return /^--\s*reply above this line\s*--$/i.test(compact);
}
function createSegmenter2({
constants,
policies,
runtimeState,
document = globalThis.document
}) {
function createTextExtractionCache() {
return /* @__PURE__ */ new WeakMap();
}
function getTextExtractionCache(options) {
if (options instanceof WeakMap) {
return options;
}
return options?.textCache instanceof WeakMap ? options.textCache : null;
}
function createElementSegment(element, text) {
const segmentId = getOrAssignSegmentId(element);
return {
id: segmentId,
element,
sourceElement: element,
text
};
}
function createAnchoredSegment(referenceNode, sourceElement, text) {
const anchor = ensureSegmentAnchor(referenceNode);
const segmentId = getOrAssignSegmentId(anchor);
return {
id: segmentId,
referenceNode,
sourceElement,
text
};
}
function getOrAssignSegmentId(node) {
if (!(node instanceof globalThis.HTMLElement)) {
return runtimeState.nextSegmentId();
}
const existingId = node.getAttribute(constants.SEGMENT_ATTRIBUTE);
if (existingId) {
return existingId;
}
const segmentId = runtimeState.nextSegmentId();
node.setAttribute(constants.SEGMENT_ATTRIBUTE, segmentId);
return segmentId;
}
function getExistingSegmentAnchor(referenceNode) {
let sibling = referenceNode?.nextSibling || null;
while (sibling) {
if (sibling instanceof globalThis.HTMLElement && sibling.classList.contains(constants.SEGMENT_ANCHOR_CLASS)) {
return sibling;
}
if (sibling.nodeType === globalThis.Node.TEXT_NODE && !(sibling.textContent || "").trim()) {
sibling = sibling.nextSibling;
continue;
}
break;
}
return null;
}
function ensureSegmentAnchor(referenceNode) {
const existingAnchor = getExistingSegmentAnchor(referenceNode);
if (existingAnchor) {
return existingAnchor;
}
if (!referenceNode?.parentNode) {
return null;
}
const anchor = document.createElement("span");
anchor.className = constants.SEGMENT_ANCHOR_CLASS;
anchor.setAttribute("aria-hidden", "true");
runtimeState.withObserverMuted(() => {
referenceNode.parentNode.insertBefore(anchor, referenceNode.nextSibling);
});
return anchor;
}
function extractBodyText(element, options = void 0) {
const textCache = getTextExtractionCache(options);
if (textCache && element instanceof globalThis.HTMLElement && textCache.has(element)) {
return textCache.get(element);
}
const clone = element?.cloneNode?.(true);
if (!(clone instanceof globalThis.HTMLElement)) {
return "";
}
clone.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}, .${constants.SEGMENT_ANCHOR_CLASS}, .${constants.TRANSLATION_WRAPPER_CLASS}, script, style, noscript, button`).forEach((node) => node.remove());
const text = clone.innerText || clone.textContent || "";
const normalizedText = text.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+\n/g, "\n").trim();
if (textCache && element instanceof globalThis.HTMLElement) {
textCache.set(element, normalizedText);
}
return normalizedText;
}
function isVisible(element) {
if (!(element instanceof globalThis.HTMLElement)) {
return false;
}
const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : { width: 1, height: 1 };
return rect.width > 0 || rect.height > 0;
}
function isBlockSegmentCandidate(element) {
const tagName = element.tagName.toLowerCase();
return ["p", "div", "li", "blockquote", "td", "th", "h1", "h2", "h3", "h4", "pre"].includes(tagName);
}
function isStandaloneBlock(element, options = void 0) {
const tagName = element.tagName.toLowerCase();
if (tagName === "div" && element.querySelector("p, li, blockquote, td, th, h1, h2, h3, h4, pre")) {
return false;
}
if (element.querySelector("table")) {
return false;
}
for (const child of element.children) {
if (!(child instanceof globalThis.HTMLElement)) {
continue;
}
if (child.closest(`.${constants.INLINE_TRANSLATION_CLASS}`)) {
continue;
}
if (!isBlockSegmentCandidate(child)) {
continue;
}
const childText = extractBodyText(child, options);
if (isVisible(child) && childText.length > 0) {
return false;
}
}
return true;
}
function isTranslatableSegment(text, element) {
if (!text || text.length < 2) {
return false;
}
if (element?.querySelector?.("img, video, iframe")) {
return false;
}
const compact = text.replace(/\s+/g, " ").trim();
if (compact.length < 2) {
return false;
}
if (/^https?:\/\/\S+$/i.test(compact)) {
return false;
}
return policies.hasTranslatableText(compact);
}
function hasNestedTranslatableBlocks(element, options = void 0) {
const descendants = element.querySelectorAll("p, div, li, blockquote, td, th, h1, h2, h3, h4, pre");
for (const descendant of descendants) {
if (!(descendant instanceof globalThis.HTMLElement) || descendant === element) {
continue;
}
if (descendant.closest(`.${constants.INLINE_TRANSLATION_CLASS}`)) {
continue;
}
if (!isVisible(descendant)) {
continue;
}
const text = extractBodyText(descendant, options);
if (isTranslatableSegment(text, descendant)) {
return true;
}
}
return false;
}
function collectFlowSegments(container, options = void 0) {
if (!(container instanceof globalThis.HTMLElement)) {
return [];
}
const segments = [];
let textParts = [];
let lastContentNode = null;
let consecutiveBreaks = 0;
const flushSegment = () => {
const text = normalizeSegmentText(textParts.join(""));
if (!text || !lastContentNode || shouldSkipSegmentText(text) || !isTranslatableSegment(text, container)) {
textParts = [];
lastContentNode = null;
consecutiveBreaks = 0;
return;
}
if (!lastContentNode?.parentNode) {
textParts = [];
lastContentNode = null;
consecutiveBreaks = 0;
return;
}
segments.push(createAnchoredSegment(lastContentNode, container, text));
textParts = [];
lastContentNode = null;
consecutiveBreaks = 0;
};
for (const node of Array.from(container.childNodes)) {
if (node instanceof globalThis.HTMLElement && node.classList.contains(constants.SEGMENT_ANCHOR_CLASS)) {
continue;
}
if (node instanceof globalThis.HTMLElement && node.classList.contains(constants.INLINE_TRANSLATION_CLASS)) {
continue;
}
if (node instanceof globalThis.HTMLBRElement) {
consecutiveBreaks += 1;
if (consecutiveBreaks >= 2) {
flushSegment();
} else {
textParts.push("\n");
}
continue;
}
if (node instanceof globalThis.HTMLHRElement) {
flushSegment();
continue;
}
if (node.nodeType === globalThis.Node.TEXT_NODE) {
const text = node.textContent || "";
textParts.push(text);
if (text.trim()) {
lastContentNode = node;
}
consecutiveBreaks = 0;
continue;
}
if (node instanceof globalThis.HTMLElement) {
if (isBlockSegmentCandidate(node)) {
flushSegment();
if (!isVisible(node)) {
consecutiveBreaks = 0;
continue;
}
const text2 = extractBodyText(node, options);
if (!text2 || shouldSkipSegmentText(text2) || !isTranslatableSegment(text2, node) || hasNestedTranslatableBlocks(node, options)) {
consecutiveBreaks = 0;
continue;
}
segments.push(createElementSegment(node, text2));
consecutiveBreaks = 0;
continue;
}
const text = extractBodyText(node, options);
textParts.push(text);
if (text.trim()) {
lastContentNode = node;
}
consecutiveBreaks = 0;
}
}
flushSegment();
return segments;
}
function collectBlockSegments(root, options = void 0) {
if (!(root instanceof globalThis.HTMLElement)) {
return [];
}
const candidates = Array.from(
root.querySelectorAll("p, div, li, blockquote, td, th, h1, h2, h3, h4, pre")
);
const segments = [];
for (const candidate of candidates) {
if (!(candidate instanceof globalThis.HTMLElement)) {
continue;
}
if (candidate.closest(`.${constants.INLINE_TRANSLATION_CLASS}`)) {
continue;
}
if (candidate.tagName.toLowerCase() === "pre") {
segments.push(...collectFlowSegments(candidate, options));
continue;
}
if (!isVisible(candidate) || !isStandaloneBlock(candidate, options)) {
continue;
}
const text = extractBodyText(candidate, options);
if (shouldSkipSegmentText(text) || !isTranslatableSegment(text, candidate) || hasNestedTranslatableBlocks(candidate, options)) {
continue;
}
segments.push(createElementSegment(candidate, text));
}
return segments;
}
function findFlowSegmentRoot(bodyElement) {
if (!(bodyElement instanceof globalThis.HTMLElement)) {
return null;
}
const preferredRoot = bodyElement.querySelector(".u-article, article, pre");
return preferredRoot instanceof globalThis.HTMLElement ? preferredRoot : bodyElement;
}
function shouldUseFlowSegmentation(container, options = void 0) {
if (!(container instanceof globalThis.HTMLElement)) {
return false;
}
if (container.tagName.toLowerCase() === "pre") {
return true;
}
for (const node of Array.from(container.childNodes)) {
if (node instanceof globalThis.HTMLElement && node.classList.contains(constants.SEGMENT_ANCHOR_CLASS)) {
continue;
}
if (node instanceof globalThis.HTMLElement && node.classList.contains(constants.INLINE_TRANSLATION_CLASS)) {
continue;
}
if (node.nodeType === globalThis.Node.TEXT_NODE && (node.textContent || "").trim()) {
return true;
}
if (node instanceof globalThis.HTMLBRElement || node instanceof globalThis.HTMLHRElement) {
return true;
}
if (node instanceof globalThis.HTMLElement && !isBlockSegmentCandidate(node)) {
const text = extractBodyText(node, options);
if (text.trim()) {
return true;
}
}
}
return false;
}
function createWholeRootFallbackSegment(root, options = void 0) {
if (!(root instanceof globalThis.HTMLElement)) {
return null;
}
const text = extractBodyText(root, options);
if (!text || shouldSkipSegmentText(text) || !policies.hasTranslatableText(text)) {
return null;
}
const lastContentNode = findLastMeaningfulNode(root, options);
if (lastContentNode?.parentNode) {
return createAnchoredSegment(lastContentNode, root, text);
}
return createElementSegment(root, text);
}
function findLastMeaningfulNode(root, options = void 0) {
const nodes = Array.from(root.childNodes).reverse();
for (const node of nodes) {
if (node instanceof globalThis.HTMLElement && node.classList.contains(constants.INLINE_TRANSLATION_CLASS)) {
continue;
}
if (node.nodeType === globalThis.Node.TEXT_NODE && !(node.textContent || "").trim()) {
continue;
}
if (node instanceof globalThis.HTMLElement) {
const text = extractBodyText(node, options);
if (!text) {
continue;
}
}
return node;
}
return null;
}
function collectFallbackSegments(bodyElement, flowRoot, options = void 0) {
const roots = [flowRoot, bodyElement].filter((root, index, items) => {
return root instanceof globalThis.HTMLElement && items.indexOf(root) === index;
});
for (const root of roots) {
const segment = createWholeRootFallbackSegment(root, options);
if (segment) {
return [segment];
}
}
return [];
}
function collectTranslatableSegments(bodyElement, options = void 0) {
const flowRoot = findFlowSegmentRoot(bodyElement);
if (flowRoot && shouldUseFlowSegmentation(flowRoot, options)) {
const flowSegments = collectFlowSegments(flowRoot, options);
if (flowSegments.length > 0) {
return flowSegments;
}
}
const blockSegments = collectBlockSegments(bodyElement, options);
if (blockSegments.length > 0) {
return blockSegments;
}
return collectFallbackSegments(bodyElement, flowRoot, options);
}
function getSegmentSignature(segments) {
return segments.map((segment) => normalizeKeyText(segment.text)).join("");
}
return {
collectTranslatableSegments,
createTextExtractionCache,
ensureSegmentAnchor,
extractBodyText,
getExistingSegmentAnchor,
getSegmentSignature,
normalizeSegmentText,
shouldSkipSegmentText
};
}
function collectSegmentsFromTextBlocks(blocks) {
return blocks.map((block, index) => ({
id: `segment-${index}`,
text: normalizeSegmentText(block)
})).filter((segment) => !shouldSkipSegmentText(segment.text) && segment.text);
}
module.exports = {
collectSegmentsFromTextBlocks,
createSegmenter: createSegmenter2,
normalizeSegmentText,
shouldSkipSegmentText
};
}
});
// src/fastmail/page-locator.js
var require_page_locator = __commonJS({
"src/fastmail/page-locator.js"(exports, module) {
var FASTMAIL_ORIGIN = "https://app.fastmail.com";
function isFastmailUrl(locationLike) {
return locationLike?.origin === FASTMAIL_ORIGIN;
}
function getLocationKey(locationLike = globalThis.location) {
return `${locationLike?.pathname || ""}${locationLike?.search || ""}`;
}
function findPageRoot(element) {
if (!element || typeof element.closest !== "function") {
return null;
}
return element.closest(".v-Page");
}
function collectPageRoots(root) {
const pageRoots = /* @__PURE__ */ new Set();
if (root?.nodeType === 9 && root.documentElement) {
root = root.body || root.documentElement;
}
if (root instanceof globalThis.HTMLElement && root.matches(".v-Page")) {
pageRoots.add(root);
}
const directRoot = root instanceof globalThis.HTMLElement ? findPageRoot(root) : null;
if (directRoot) {
pageRoots.add(directRoot);
}
if (root && typeof root.querySelectorAll === "function") {
root.querySelectorAll(".v-Page").forEach((pageRoot) => {
if (pageRoot instanceof globalThis.HTMLElement) {
pageRoots.add(pageRoot);
}
});
}
return Array.from(pageRoots);
}
module.exports = {
FASTMAIL_ORIGIN,
collectPageRoots,
findPageRoot,
getLocationKey,
isFastmailUrl
};
}
});
// src/fastmail/thread-dom.js
var require_thread_dom = __commonJS({
"src/fastmail/thread-dom.js"(exports, module) {
function normalizeKeyText(text) {
return String(text || "").replace(/\s+/g, " ").trim();
}
function createThreadDom2({
constants,
i18n,
pageLocator: pageLocator2,
policies,
runtimeState,
segmenter,
toolbarButtonApi: toolbarButtonApi2,
document = globalThis.document
}) {
const TRANSLATE_CLICK_HANDLED = /* @__PURE__ */ Symbol.for("fasttrmail.translateClickHandled");
const toolbarButtonThreadRoots = /* @__PURE__ */ new WeakMap();
const toolbarButtonClickHandlers = /* @__PURE__ */ new WeakMap();
const MOUSEUP_CLICK_DEDUP_WINDOW_MS = 400;
function normalizeIdentityValue(value, maxLength = 240) {
return normalizeKeyText(value || "").slice(0, maxLength);
}
function buildIdentity(prefix, parts, fallback = "unknown") {
const normalizedParts = parts.map((part) => normalizeIdentityValue(part)).filter(Boolean);
if (normalizedParts.length > 0) {
return `${prefix}::${normalizedParts.join("::")}`;
}
return `${prefix}::${fallback}`;
}
function findThreadRoot(element) {
if (!(element instanceof globalThis.HTMLElement)) {
return null;
}
if (element.classList.contains(constants.BUTTON_CLASS)) {
const mappedThreadRoot = toolbarButtonThreadRoots.get(element);
if (mappedThreadRoot instanceof globalThis.HTMLElement) {
return mappedThreadRoot;
}
}
const directThreadRoot = element.closest(".v-Thread");
if (directThreadRoot instanceof globalThis.HTMLElement) {
return directThreadRoot;
}
const pageRoot = pageLocator2.findPageRoot(element);
if (!(pageRoot instanceof globalThis.HTMLElement)) {
return null;
}
return findPrimaryThreadRoot(pageRoot);
}
function findToolbarForPageRoot(pageRoot, threadRoot = null) {
if (!(pageRoot instanceof globalThis.HTMLElement)) {
return null;
}
if (threadRoot instanceof globalThis.HTMLElement) {
const localToolbar = findToolbarNearThread(pageRoot, threadRoot);
if (localToolbar) {
return localToolbar;
}
}
const toolbar = pageRoot.querySelector(".v-Toolbar");
return toolbar instanceof globalThis.HTMLElement ? toolbar : null;
}
function findToolbarNearThread(pageRoot, threadRoot) {
const threadToolbar = threadRoot.querySelector(".v-Toolbar");
if (threadToolbar instanceof globalThis.HTMLElement) {
return threadToolbar;
}
for (let node = threadRoot; node && node !== pageRoot; node = node.parentElement) {
for (let sibling = node.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
if (!(sibling instanceof globalThis.HTMLElement)) {
continue;
}
if (sibling.matches(".v-Toolbar")) {
return sibling;
}
const nestedToolbar = sibling.querySelector(".v-Toolbar");
if (nestedToolbar instanceof globalThis.HTMLElement) {
return nestedToolbar;
}
}
}
return null;
}
function findPrimaryThreadRoot(pageRoot) {
if (!(pageRoot instanceof globalThis.HTMLElement)) {
return null;
}
const candidates = Array.from(pageRoot.querySelectorAll(".v-Page-content .v-Thread")).filter(
(candidate) => candidate instanceof globalThis.HTMLElement
);
if (candidates.length === 0) {
return null;
}
let bestCandidate = null;
let bestScore = Number.NEGATIVE_INFINITY;
candidates.forEach((candidate, index) => {
const title = candidate.querySelector(".v-Thread-title h1");
const titleText = normalizeIdentityValue(title?.textContent || "", 320);
const messageCardCount = candidate.querySelectorAll(".v-MessageCard.app-contentCard").length;
const rect = typeof candidate.getBoundingClientRect === "function" ? candidate.getBoundingClientRect() : { width: 1, height: 1 };
const isVisible = rect.width > 0 || rect.height > 0;
const score = (titleText ? 1e3 : 0) + (isVisible ? 100 : 0) + Math.min(messageCardCount, 20) * 10 + index;
if (score >= bestScore) {
bestScore = score;
bestCandidate = candidate;
}
});
return bestCandidate instanceof globalThis.HTMLElement ? bestCandidate : null;
}
function findTitleElement(threadRoot) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return null;
}
const title = threadRoot.querySelector(".v-Thread-title h1");
return title instanceof globalThis.HTMLElement ? title : null;
}
function getTitleText(threadRoot) {
return normalizeIdentityValue(findTitleElement(threadRoot)?.textContent || "", 320);
}
function ensureThreadState(threadRoot) {
let state = getExistingThreadState(threadRoot);
if (state) {
return state;
}
const staleState = getExistingThreadState(threadRoot, { includeStale: true });
if (staleState) {
runtimeState.deactivateThreadState(staleState);
}
state = runtimeState.createThreadState();
state.root = threadRoot;
runtimeState.state.threadStates.set(threadRoot, state);
return state;
}
function getExistingThreadState(threadRoot, { includeStale = false } = {}) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return null;
}
const state = runtimeState.state.threadStates.get(threadRoot) || null;
if (!includeStale && state && !runtimeState.isThreadStateCurrent(state)) {
return null;
}
return state;
}
function resetThreadState(threadRoot) {
const threadState = getExistingThreadState(threadRoot, { includeStale: true });
if (threadState) {
runtimeState.deactivateThreadState(threadState);
runtimeState.state.threadStates.delete(threadRoot);
}
}
function isThreadActive(threadRoot) {
return getExistingThreadState(threadRoot)?.active === true;
}
function ensureTranslateDivider(toolbar) {
const existingDivider = toolbar.querySelector(".fmt-translate-divider");
if (existingDivider instanceof globalThis.HTMLElement) {
return existingDivider;
}
const divider = document.createElement("span");
divider.className = "v-Toolbar-divider fmt-translate-divider";
return divider;
}
function ensureTranslateButton(toolbar, onTranslateClick) {
const buttons = Array.from(toolbar.querySelectorAll(`.${constants.BUTTON_CLASS}`)).filter((candidate) => {
return candidate instanceof globalThis.HTMLElement && candidate.closest(".v-Toolbar") === toolbar;
});
const [existingButton, ...duplicates] = buttons;
duplicates.forEach((duplicate) => duplicate.remove());
const button = existingButton instanceof globalThis.HTMLButtonElement ? existingButton : toolbarButtonApi2.createToolbarButton({
document,
constants,
i18n
});
const divider = ensureTranslateDivider(toolbar);
bindTranslateButton(button, onTranslateClick);
toolbarButtonApi2.placeToolbarButton(toolbar, button, divider);
return button;
}
function removeToolbarTranslateButtons(toolbar) {
if (!(toolbar instanceof globalThis.HTMLElement)) {
return;
}
toolbar.querySelectorAll(`.${constants.BUTTON_CLASS}`).forEach((button) => {
if (!(button instanceof globalThis.HTMLElement)) {
return;
}
unbindTranslateButton(button);
toolbarButtonThreadRoots.delete(button);
button.remove();
});
toolbar.querySelectorAll(".fmt-translate-divider").forEach((divider) => {
if (divider instanceof globalThis.HTMLElement) {
divider.remove();
}
});
}
function bindTranslateButton(button, onTranslateClick) {
if (!(button instanceof globalThis.HTMLButtonElement) || typeof onTranslateClick !== "function") {
return;
}
const binding = toolbarButtonClickHandlers.get(button) || null;
if (binding?.onTranslateClick === onTranslateClick) {
return;
}
if (typeof binding?.clickHandler === "function") {
button.removeEventListener("click", binding.clickHandler);
}
if (typeof binding?.mouseUpHandler === "function") {
button.removeEventListener("mouseup", binding.mouseUpHandler);
}
const activationState = {
lastMouseUpTimeStamp: Number.NEGATIVE_INFINITY
};
const invokeTranslateClick = (event) => {
if (event?.[TRANSLATE_CLICK_HANDLED]) {
return;
}
event[TRANSLATE_CLICK_HANDLED] = true;
void onTranslateClick({
currentTarget: button,
type: event?.type || "click",
originalEvent: event
});
};
const mouseUpHandler = (event) => {
if (!isPrimaryMouseActivation(event)) {
return;
}
activationState.lastMouseUpTimeStamp = getEventTimeStamp(event);
invokeTranslateClick(event);
};
const clickHandler = (event) => {
if (shouldSkipClickAfterMouseUp(event, activationState.lastMouseUpTimeStamp)) {
event[TRANSLATE_CLICK_HANDLED] = true;
return;
}
invokeTranslateClick(event);
};
button.addEventListener("mouseup", mouseUpHandler);
button.addEventListener("click", clickHandler);
toolbarButtonClickHandlers.set(button, {
clickHandler,
mouseUpHandler,
onTranslateClick
});
}
function unbindTranslateButton(button) {
if (!(button instanceof globalThis.HTMLButtonElement)) {
return;
}
const binding = toolbarButtonClickHandlers.get(button) || null;
if (typeof binding?.clickHandler === "function") {
button.removeEventListener("click", binding.clickHandler);
}
if (typeof binding?.mouseUpHandler === "function") {
button.removeEventListener("mouseup", binding.mouseUpHandler);
}
toolbarButtonClickHandlers.delete(button);
}
function isManagedTranslateButton(button) {
return button instanceof globalThis.HTMLButtonElement && toolbarButtonClickHandlers.has(button);
}
function isPrimaryMouseActivation(event) {
if (!event) {
return false;
}
return event.button === void 0 || event.button === 0;
}
function getEventTimeStamp(event) {
return typeof event?.timeStamp === "number" ? event.timeStamp : Number.NEGATIVE_INFINITY;
}
function shouldSkipClickAfterMouseUp(event, lastMouseUpTimeStamp) {
if (!event || event.detail <= 0 || !Number.isFinite(lastMouseUpTimeStamp)) {
return false;
}
const clickTimeStamp = getEventTimeStamp(event);
return clickTimeStamp >= lastMouseUpTimeStamp && clickTimeStamp - lastMouseUpTimeStamp <= MOUSEUP_CLICK_DEDUP_WINDOW_MS;
}
function injectButtons(root, onTranslateClick) {
runtimeState.withObserverMuted(() => {
const pageRoots = pageLocator2.collectPageRoots(root);
for (const pageRoot of pageRoots) {
if (!(pageRoot instanceof globalThis.HTMLElement)) {
continue;
}
const threadRoot = findPrimaryThreadRoot(pageRoot);
const toolbar = findToolbarForPageRoot(pageRoot, threadRoot);
const threadTitle = threadRoot instanceof globalThis.HTMLElement ? threadRoot.querySelector(".v-Thread-title") : null;
if (!(toolbar instanceof globalThis.HTMLElement) || !(threadTitle instanceof globalThis.HTMLElement)) {
removeToolbarTranslateButtons(toolbar);
continue;
}
const button = ensureTranslateButton(toolbar, onTranslateClick);
toolbarButtonThreadRoots.set(button, threadRoot);
updateButtonState(button, isThreadActive(threadRoot));
}
});
}
function getThreadMessageEntries(threadRoot) {
const cards = Array.from(threadRoot.querySelectorAll(".v-MessageCard.app-contentCard")).filter(
(card) => card instanceof globalThis.HTMLElement
);
return cards.map((card) => ({
card,
header: card.querySelector(".v-MessageCard-header"),
messageNode: findMessageNodeForCard(card)
}));
}
function findMessageNodeForCard(card) {
if (!(card instanceof globalThis.HTMLElement)) {
return null;
}
const nestedMessage = card.querySelector(".v-Message");
if (nestedMessage instanceof globalThis.HTMLElement) {
return nestedMessage;
}
for (let sibling = card.nextElementSibling; sibling; sibling = sibling.nextElementSibling) {
if (!(sibling instanceof globalThis.HTMLElement)) {
continue;
}
if (sibling.matches(".v-MessageCard.app-contentCard")) {
break;
}
if (sibling.matches(".v-Message")) {
return sibling;
}
const nestedSiblingMessage = sibling.querySelector(".v-Message");
if (nestedSiblingMessage instanceof globalThis.HTMLElement) {
return nestedSiblingMessage;
}
}
return null;
}
function getMessageDataId(entry) {
return normalizeIdentityValue(
entry.messageNode?.getAttribute("data-message-id") || entry.card?.getAttribute("data-message-id") || "",
320
);
}
function getMessageDetailsFingerprint(messageNode) {
if (!(messageNode instanceof globalThis.HTMLElement)) {
return "";
}
const detailsList = messageNode.querySelector(".v-Message-detailsList");
if (detailsList instanceof globalThis.HTMLElement) {
return normalizeIdentityValue(detailsList.textContent || "", 400);
}
return "";
}
function getMessageInstanceId(entry) {
return normalizeIdentityValue(
entry.card?.getAttribute(constants.MESSAGE_INSTANCE_ATTRIBUTE) || entry.messageNode?.getAttribute(constants.MESSAGE_INSTANCE_ATTRIBUTE) || "",
320
);
}
function getOrAssignMessageInstanceId(entry) {
const existingId = getMessageInstanceId(entry);
if (existingId) {
return existingId;
}
const instanceId = runtimeState.nextMessageInstanceId();
runtimeState.withObserverMuted(() => {
if (entry.card instanceof globalThis.HTMLElement) {
entry.card.setAttribute(constants.MESSAGE_INSTANCE_ATTRIBUTE, instanceId);
}
if (entry.messageNode instanceof globalThis.HTMLElement) {
entry.messageNode.setAttribute(constants.MESSAGE_INSTANCE_ATTRIBUTE, instanceId);
}
});
return instanceId;
}
function buildMessageIdentity(entry, bodyElement, { textCache } = {}) {
const dataMessageId = getMessageDataId(entry);
const detailsFingerprint = getMessageDetailsFingerprint(entry.messageNode);
const fromText = normalizeIdentityValue(
entry.card?.querySelector(".v-MessageCard-from")?.getAttribute("title") || entry.card?.querySelector(".v-MessageCard-from")?.textContent || ""
);
const toText = normalizeIdentityValue(
entry.card?.querySelector(".v-Message-toName")?.getAttribute("title") || entry.card?.querySelector(".v-Message-toName")?.textContent || ""
);
const exactDateText = normalizeIdentityValue(entry.card?.querySelector(".v-MessageCard-time")?.getAttribute("title") || "");
const bodyTextSource = bodyElement instanceof globalThis.HTMLElement ? bodyElement : entry.messageNode instanceof globalThis.HTMLElement ? entry.messageNode : null;
const bodyExcerpt = !dataMessageId && !detailsFingerprint && bodyTextSource ? normalizeIdentityValue(segmenter.extractBodyText(bodyTextSource, { textCache }).slice(0, 200), 200) : "";
return buildIdentity("message", [
dataMessageId,
detailsFingerprint,
fromText,
toText,
exactDateText,
bodyExcerpt
], entry.card?.id || entry.messageNode?.id || "message");
}
function collectMessageDescriptors(threadRoot, { textCache } = {}) {
return getThreadMessageEntries(threadRoot).map((entry) => {
const instanceId = getOrAssignMessageInstanceId(entry);
const body = findBodyElement(entry.messageNode, { textCache });
const contentRoot = body instanceof globalThis.HTMLElement ? findPrimaryContentRoot(body, { textCache }) || body : null;
const identity = buildMessageIdentity(entry, body, { textCache });
return {
instanceId,
identity,
card: entry.card,
header: entry.header,
messageNode: entry.messageNode,
body,
contentRoot
};
});
}
function reconcileMessageStates(threadState, descriptors) {
const liveKeys = /* @__PURE__ */ new Set();
for (const descriptor of descriptors) {
descriptor.key = `${threadState.key}::${descriptor.instanceId}`;
liveKeys.add(descriptor.key);
let messageState = threadState.messages.get(descriptor.key);
if (!messageState) {
messageState = runtimeState.createMessageState({
key: descriptor.key,
identity: descriptor.identity,
instanceId: descriptor.instanceId
});
threadState.messages.set(descriptor.key, messageState);
}
const identityChanged = messageState.identity && messageState.identity !== descriptor.identity;
if (identityChanged) {
messageState.status = "idle";
messageState.segmentSignature = "";
messageState.translatedSegments = null;
messageState.error = "";
messageState.requestSerial += 1;
}
messageState.identity = descriptor.identity;
messageState.instanceId = descriptor.instanceId;
descriptor.state = messageState;
}
for (const key of threadState.messages.keys()) {
if (!liveKeys.has(key)) {
threadState.messages.delete(key);
}
}
}
function findMessageEntryByInstanceId(threadRoot, instanceId) {
if (!(threadRoot instanceof globalThis.HTMLElement) || !instanceId) {
return null;
}
return getThreadMessageEntries(threadRoot).find((entry) => {
return getMessageInstanceId(entry) === instanceId;
}) || null;
}
function findLiveMessageElements(threadRoot, descriptor, { textCache } = {}) {
const directBody = findBodyElement(descriptor.messageNode, { textCache });
if (directBody instanceof globalThis.HTMLElement && directBody.isConnected) {
return {
body: directBody,
contentRoot: findPrimaryContentRoot(directBody, { textCache }) || directBody
};
}
const liveEntry = findMessageEntryByInstanceId(threadRoot, descriptor.state?.instanceId || descriptor.instanceId);
if (!liveEntry) {
return {
body: null,
contentRoot: null
};
}
const liveBody = findBodyElement(liveEntry.messageNode, { textCache });
return {
body: liveBody instanceof globalThis.HTMLElement ? liveBody : null,
contentRoot: liveBody instanceof globalThis.HTMLElement ? findPrimaryContentRoot(liveBody, { textCache }) || liveBody : null
};
}
function findBodyElement(messageNode, { textCache } = {}) {
if (!(messageNode instanceof globalThis.HTMLElement)) {
return null;
}
const explicitSelectors = [
".v-Message-body",
".v-Message-bodyContents",
".v-MessageBody",
".u-containSelection.v-Message-body"
];
for (const selector of explicitSelectors) {
const found = messageNode.matches(selector) ? messageNode : messageNode.querySelector(selector);
if (found instanceof globalThis.HTMLElement && policies.isExplicitBodyText(segmenter.extractBodyText(found, { textCache }))) {
return found;
}
}
let bestCandidate = null;
let bestScore = 0;
for (const candidate of messageNode.querySelectorAll(".u-article, article, section, pre, blockquote, div")) {
if (!(candidate instanceof globalThis.HTMLElement)) {
continue;
}
if (candidate.closest(`.${constants.INLINE_TRANSLATION_CLASS}`)) {
continue;
}
const text = segmenter.extractBodyText(candidate, { textCache });
if (!policies.isCandidateBodyText(text)) {
continue;
}
const rect = candidate.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
continue;
}
const score = text.length - candidate.querySelectorAll("button, input, svg").length * 20;
if (score > bestScore) {
bestScore = score;
bestCandidate = candidate;
}
}
if (!(bestCandidate instanceof globalThis.HTMLElement)) {
return null;
}
return bestCandidate.closest(".v-Message-body, .v-Message-bodyContents, .v-MessageBody") || bestCandidate;
}
function isMessageBodyDeferred(descriptor) {
const card = descriptor?.card;
if (!(card instanceof globalThis.HTMLElement)) {
return false;
}
if (card.classList.contains("is-collapsed")) {
return true;
}
return Boolean(card.querySelector(".v-MessageCard-loadingBody"));
}
function findPrimaryContentRoot(container, { textCache } = {}) {
if (!(container instanceof globalThis.HTMLElement)) {
return null;
}
const preferredSelectors = [".u-article", "article", "pre", "blockquote", "section", "div"];
let bestCandidate = null;
let bestScore = 0;
for (const selector of preferredSelectors) {
for (const candidate of container.querySelectorAll(selector)) {
if (!(candidate instanceof globalThis.HTMLElement)) {
continue;
}
if (candidate.closest(`.${constants.INLINE_TRANSLATION_CLASS}`)) {
continue;
}
const text = segmenter.extractBodyText(candidate, { textCache });
if (text.length < 20) {
continue;
}
if (text.length > bestScore) {
bestScore = text.length;
bestCandidate = candidate;
}
}
}
return bestCandidate;
}
function syncThreadButtons(threadRoot, isActive) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return;
}
threadRoot.querySelectorAll(`.${constants.BUTTON_CLASS}`).forEach((button) => {
if (button instanceof globalThis.HTMLElement) {
updateButtonState(button, isActive);
}
});
const pageRoot = pageLocator2.findPageRoot(threadRoot);
const toolbar = findToolbarForPageRoot(pageRoot, threadRoot);
if (!(toolbar instanceof globalThis.HTMLElement)) {
return;
}
const candidates = toolbar.querySelectorAll(`.${constants.BUTTON_CLASS}`);
for (const candidate of candidates) {
if (!(candidate instanceof globalThis.HTMLElement)) {
continue;
}
if (toolbarButtonThreadRoots.get(candidate) === threadRoot) {
updateButtonState(candidate, isActive);
}
}
}
function updateButtonState(button, isActive) {
runtimeState.withObserverMuted(() => {
toolbarButtonApi2.updateToolbarButtonState(button, isActive, { constants, i18n });
});
}
function pruneDetachedThreadStates() {
for (const threadState of Array.from(runtimeState.state.activeThreadStates)) {
if (threadState.root instanceof globalThis.HTMLElement && threadState.root.isConnected) {
continue;
}
runtimeState.deactivateThreadState(threadState);
if (threadState.root instanceof globalThis.HTMLElement) {
runtimeState.state.threadStates.delete(threadState.root);
}
}
}
function collectDetachedActiveThreadRoots() {
const roots = [];
for (const threadState of Array.from(runtimeState.state.activeThreadStates)) {
if (threadState.root instanceof globalThis.HTMLElement && !threadState.root.isConnected) {
roots.push(threadState.root);
}
}
return roots;
}
function clearThreadDomState(root) {
if (!root || typeof root.querySelectorAll !== "function") {
return;
}
runtimeState.withObserverMuted(() => {
root.querySelectorAll(`[${constants.MESSAGE_INSTANCE_ATTRIBUTE}], [${constants.SEGMENT_ATTRIBUTE}]`).forEach((node) => {
if (!(node instanceof globalThis.HTMLElement)) {
return;
}
node.removeAttribute(constants.MESSAGE_INSTANCE_ATTRIBUTE);
node.removeAttribute(constants.SEGMENT_ATTRIBUTE);
});
});
}
return {
findThreadRoot,
findPrimaryThreadRoot,
findToolbarForPageRoot,
findTitleElement,
getTitleText,
ensureThreadState,
getExistingThreadState,
resetThreadState,
isThreadActive,
injectButtons,
getThreadMessageEntries,
collectMessageDescriptors,
reconcileMessageStates,
findLiveMessageElements,
findBodyElement,
findPrimaryContentRoot,
isMessageBodyDeferred,
syncThreadButtons,
updateButtonState,
isManagedTranslateButton,
collectDetachedActiveThreadRoots,
pruneDetachedThreadStates,
clearThreadDomState
};
}
module.exports = { createThreadDom: createThreadDom2 };
}
});
// src/fastmail/text-policies.js
var require_text_policies = __commonJS({
"src/fastmail/text-policies.js"(exports, module) {
var CANDIDATE_BODY_MIN_LENGTH = 3;
function normalizeText(text) {
return String(text || "").replace(/\s+/g, " ").trim();
}
function hasTranslatableText(text) {
const compact = normalizeText(text);
if (!compact) {
return false;
}
if (/^https?:\/\/\S+$/i.test(compact)) {
return false;
}
return /\p{Letter}/u.test(compact);
}
function isExplicitBodyText(text) {
return hasTranslatableText(text);
}
function isCandidateBodyText(text) {
const compact = normalizeText(text);
if (compact.length < CANDIDATE_BODY_MIN_LENGTH) {
return false;
}
return hasTranslatableText(compact);
}
module.exports = {
hasTranslatableText,
isExplicitBodyText,
isCandidateBodyText,
normalizeText
};
}
});
// src/platform/tm-api.js
var require_tm_api = __commonJS({
"src/platform/tm-api.js"(exports, module) {
var REQUIRED_CAPABILITIES = Object.freeze([
"GM_getValue",
"GM_setValue",
"GM_registerMenuCommand",
"GM_xmlhttpRequest"
]);
function validateCapability(scope, capability) {
if (typeof scope?.[capability] !== "function") {
throw new Error(`Missing Tampermonkey capability: ${capability}`);
}
}
function createTmApi2(scope = globalThis) {
for (const capability of REQUIRED_CAPABILITIES) {
validateCapability(scope, capability);
}
return {
getValue(key, fallback) {
return scope.GM_getValue(key, fallback);
},
setValue(key, value) {
return scope.GM_setValue(key, value);
},
registerMenuCommand(label, handler) {
return scope.GM_registerMenuCommand(label, handler);
},
xmlHttpRequest(details) {
return scope.GM_xmlhttpRequest(details);
}
};
}
module.exports = { createTmApi: createTmApi2 };
}
});
// src/platform/xhr.js
var require_xhr = __commonJS({
"src/platform/xhr.js"(exports, module) {
function createXhr2({ tmApi }) {
function isSuccessStatus(status) {
return typeof status === "number" && status >= 200 && status < 300;
}
function createHttpStatusError(response) {
const error = new Error(`HTTP ${response?.status || 0}`);
error.status = response?.status;
error.statusText = response?.statusText;
error.responseText = response?.responseText;
error.finalUrl = response?.finalUrl;
error.context = response?.context;
error.response = response;
return error;
}
return {
request(details) {
let requestHandle = null;
const promise = new Promise((resolve, reject) => {
requestHandle = tmApi.xmlHttpRequest({
...details,
onload(response) {
if (isSuccessStatus(response?.status)) {
resolve(response);
return;
}
reject(createHttpStatusError(response));
},
onerror: reject,
ontimeout: reject,
onabort: reject
});
});
promise.abort = () => {
if (requestHandle && typeof requestHandle.abort === "function") {
requestHandle.abort();
}
};
return promise;
}
};
}
module.exports = { createXhr: createXhr2 };
}
});
// src/storage/settings-store.js
var require_settings_store = __commonJS({
"src/storage/settings-store.js"(exports, module) {
function assertConfig(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function createSettingsStore2({ tmApi, defaults, languages, providers }) {
const SETTINGS_KEY = "settings";
const TARGET_LANGUAGE_KEY = "targetLanguage";
const PREFERRED_PROVIDER_KEY = "preferredProvider";
assertConfig(tmApi && typeof tmApi.getValue === "function", "tmApi.getValue must be a function");
assertConfig(tmApi && typeof tmApi.setValue === "function", "tmApi.setValue must be a function");
assertConfig(defaults && typeof defaults.targetLanguage === "string", "defaults.targetLanguage must be a string");
assertConfig(defaults && typeof defaults.preferredProvider === "string", "defaults.preferredProvider must be a string");
assertConfig(Array.isArray(languages) && languages.length > 0, "languages must be a non-empty array");
assertConfig(Array.isArray(providers) && providers.length > 0, "providers must be a non-empty array");
const supportedLanguages = new Set(languages.map((language) => language.id));
const supportedProviders = new Set(providers.map((provider) => provider.id));
assertConfig(supportedLanguages.has(defaults.targetLanguage), "defaults.targetLanguage must exist in languages");
assertConfig(supportedProviders.has(defaults.preferredProvider), "defaults.preferredProvider must exist in providers");
function normalizeTargetLanguage(value) {
return supportedLanguages.has(value) ? value : defaults.targetLanguage;
}
function normalizePreferredProvider(value) {
return supportedProviders.has(value) ? value : defaults.preferredProvider;
}
function normalizeSettingsSnapshot(value) {
return {
targetLanguage: normalizeTargetLanguage(value?.targetLanguage),
preferredProvider: normalizePreferredProvider(value?.preferredProvider)
};
}
function isSettingsSnapshot(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
async function writeSettingsSnapshot(value) {
const normalizedValue = normalizeSettingsSnapshot(value);
await tmApi.setValue(SETTINGS_KEY, normalizedValue);
return normalizedValue;
}
async function readLegacySettingsSnapshot() {
const [targetLanguage, preferredProvider] = await Promise.all([
tmApi.getValue(TARGET_LANGUAGE_KEY, defaults.targetLanguage),
tmApi.getValue(PREFERRED_PROVIDER_KEY, defaults.preferredProvider)
]);
return normalizeSettingsSnapshot({ targetLanguage, preferredProvider });
}
async function resolveSettingsSnapshot({ repair }) {
const storedSnapshot = await tmApi.getValue(SETTINGS_KEY, void 0);
if (isSettingsSnapshot(storedSnapshot)) {
const normalizedSnapshot = normalizeSettingsSnapshot(storedSnapshot);
const needsRepair = normalizedSnapshot.targetLanguage !== storedSnapshot.targetLanguage || normalizedSnapshot.preferredProvider !== storedSnapshot.preferredProvider;
if (repair && needsRepair) {
await writeSettingsSnapshot(normalizedSnapshot);
}
return normalizedSnapshot;
}
if (typeof storedSnapshot !== "undefined") {
const defaultSnapshot = {
targetLanguage: defaults.targetLanguage,
preferredProvider: defaults.preferredProvider
};
if (repair) {
await writeSettingsSnapshot(defaultSnapshot);
}
return defaultSnapshot;
}
const legacySnapshot = await readLegacySettingsSnapshot();
if (repair) {
await writeSettingsSnapshot(legacySnapshot);
}
return legacySnapshot;
}
async function readSettingsSnapshot() {
return resolveSettingsSnapshot({ repair: true });
}
return {
async getTargetLanguage() {
return (await readSettingsSnapshot()).targetLanguage;
},
async setTargetLanguage(value) {
const settings = await resolveSettingsSnapshot({ repair: false });
return (await writeSettingsSnapshot({
...settings,
targetLanguage: value
})).targetLanguage;
},
async getPreferredProvider() {
return (await readSettingsSnapshot()).preferredProvider;
},
async setPreferredProvider(value) {
const settings = await resolveSettingsSnapshot({ repair: false });
return (await writeSettingsSnapshot({
...settings,
preferredProvider: value
})).preferredProvider;
},
async getSettings() {
return readSettingsSnapshot();
},
async setSettings(nextSettings) {
return writeSettingsSnapshot(nextSettings);
}
};
}
module.exports = { createSettingsStore: createSettingsStore2 };
}
});
// src/core/errors.js
var require_errors = __commonJS({
"src/core/errors.js"(exports, module) {
var ERROR_CODES = Object.freeze({
EDGE_AUTH_FAILED: "edge_auth_failed",
EDGE_TRANSLATE_FAILED: "edge_translate_failed",
GOOGLE_TRANSLATE_FAILED: "google_translate_failed",
UNSUPPORTED_TARGET_LANGUAGE: "unsupported_target_language",
NO_TRANSLATOR_AVAILABLE: "no_translator_available",
TRANSLATOR_CONTRACT_VIOLATION: "translator_contract_violation",
TRANSLATION_CANCELLED: "translation_cancelled"
});
function createError(code, message, metadata = {}, cause) {
const error = new Error(message || code);
error.code = code;
error.metadata = metadata;
if (cause !== void 0) {
error.cause = cause;
}
return error;
}
function isStableError(error) {
return Boolean(error && typeof error.code === "string" && Object.values(ERROR_CODES).includes(error.code));
}
function isAbortLikeError(error) {
return Boolean(
error && (error.code === ERROR_CODES.TRANSLATION_CANCELLED || error.name === "AbortError" || error.type === "abort" || error.error === "abort")
);
}
function normalizeError(code, message, cause, metadata = {}) {
if (isStableError(cause)) {
return cause;
}
if (isAbortLikeError(cause)) {
return createError(
ERROR_CODES.TRANSLATION_CANCELLED,
"Translation request was cancelled.",
metadata,
cause
);
}
return createError(
code,
message,
{
...metadata,
status: typeof cause?.status === "number" ? cause.status : metadata.status,
statusText: typeof cause?.statusText === "string" ? cause.statusText : metadata.statusText,
causeMessage: typeof cause?.message === "string" ? cause.message : void 0
},
cause
);
}
function attachAbort(promise, abort) {
promise.abort = typeof abort === "function" ? abort : () => {
};
return promise;
}
module.exports = { ERROR_CODES, attachAbort, createError, normalizeError };
}
});
// src/translation/edge-auth.js
var require_edge_auth = __commonJS({
"src/translation/edge-auth.js"(exports, module) {
var {
ERROR_CODES,
attachAbort,
createError,
normalizeError
} = require_errors();
var { createNoopDiagnostics: createNoopDiagnostics2 } = require_diagnostics();
var EDGE_FALLBACK_HEADERS = Object.freeze({
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
"sec-ch-ua": '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"'
});
function createEdgeAuth2({ xhr, now = Date.now, diagnostics = createNoopDiagnostics2() }) {
let cache = null;
let inflight = null;
function isJwtLike(token) {
return typeof token === "string" && /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/.test(token);
}
function decodeBase64Url(value) {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
if (typeof globalThis.atob !== "function") {
throw new Error("base64 decoder is unavailable");
}
const binary = globalThis.atob(padded);
let percentEncoded = "";
for (let index = 0; index < binary.length; index += 1) {
percentEncoded += `%${binary.charCodeAt(index).toString(16).padStart(2, "0")}`;
}
return decodeURIComponent(percentEncoded);
}
function getExpiry(token) {
const [, payload = ""] = token.split(".");
try {
const parsed = JSON.parse(decodeBase64Url(payload));
return typeof parsed.exp === "number" ? parsed.exp * 1e3 : now() + 6e5;
} catch {
return now() + 6e5;
}
}
function getFallbackHeaders() {
return { ...EDGE_FALLBACK_HEADERS };
}
return {
getToken() {
if (cache && cache.expiresAt > now() + 6e4) {
return attachAbort(Promise.resolve(cache.token), () => {
});
}
if (!inflight) {
let activeAbort = () => {
};
const operation = (async () => {
let request = xhr.request({
method: "GET",
url: "https://edge.microsoft.com/translate/auth",
nocache: true,
timeout: 8e3,
headers: {
Accept: "*/*"
}
});
diagnostics.record("edge-auth.request.start", {
attempt: 1,
timeoutMs: 8e3,
usedFallbackHeaders: false
});
activeAbort = () => request.abort?.();
let response = await request;
let token = String(response?.responseText || "").trim();
diagnostics.record("edge-auth.request.result", {
attempt: 1,
usedFallbackHeaders: false,
status: response?.status || 0,
responseTextLength: token.length,
isJwtLike: isJwtLike(token)
});
if (!isJwtLike(token)) {
request = xhr.request({
method: "GET",
url: "https://edge.microsoft.com/translate/auth",
nocache: true,
timeout: 8e3,
headers: {
Accept: "*/*",
...getFallbackHeaders()
}
});
diagnostics.record("edge-auth.request.start", {
attempt: 2,
timeoutMs: 8e3,
usedFallbackHeaders: true
});
activeAbort = () => request.abort?.();
response = await request;
token = String(response?.responseText || "").trim();
diagnostics.record("edge-auth.request.result", {
attempt: 2,
usedFallbackHeaders: true,
status: response?.status || 0,
responseTextLength: token.length,
isJwtLike: isJwtLike(token)
});
}
if (!isJwtLike(token)) {
diagnostics.record("edge-auth.token.rejected", {
reason: "token-not-jwt-like",
tokenLength: token.length
});
throw createError(ERROR_CODES.EDGE_AUTH_FAILED, "Edge auth token is invalid.");
}
cache = {
token,
expiresAt: getExpiry(token)
};
diagnostics.record("edge-auth.token.accepted", {
tokenLength: token.length,
expiresAt: cache.expiresAt
});
return token;
})().catch((error) => {
diagnostics.recordError("edge-auth.request.error", error, {
requestStage: "edge-auth"
});
throw normalizeError(
ERROR_CODES.EDGE_AUTH_FAILED,
"Edge auth request failed.",
error,
{
requestStage: "edge-auth"
}
);
}).finally(() => {
inflight = null;
});
inflight = {
consumerCount: 0,
abortTransport() {
activeAbort();
},
promise: operation
};
}
inflight.consumerCount += 1;
let isSettled = false;
let isCancelled = false;
let rejectConsumer = null;
const sharedState = inflight;
const consumerPromise = new Promise((resolve, reject) => {
rejectConsumer = reject;
sharedState.promise.then(
(token) => {
if (isCancelled) {
return;
}
isSettled = true;
resolve(token);
},
(error) => {
if (isCancelled) {
return;
}
isSettled = true;
reject(error);
}
);
});
return attachAbort(consumerPromise, () => {
if (isSettled || isCancelled) {
return;
}
isCancelled = true;
sharedState.consumerCount -= 1;
if (sharedState.consumerCount === 0) {
sharedState.abortTransport();
}
rejectConsumer(
createError(ERROR_CODES.TRANSLATION_CANCELLED, "Translation request was cancelled.")
);
});
}
};
}
module.exports = { createEdgeAuth: createEdgeAuth2 };
}
});
// src/translation/edge-translator.js
var require_edge_translator = __commonJS({
"src/translation/edge-translator.js"(exports, module) {
var {
ERROR_CODES,
attachAbort,
createError,
normalizeError
} = require_errors();
var { createNoopDiagnostics: createNoopDiagnostics2 } = require_diagnostics();
var MAX_SEGMENTS_PER_EDGE_BATCH = 20;
var MAX_CHARS_PER_EDGE_SEGMENT = 1e4;
var MAX_CHARS_PER_EDGE_BATCH = 2e4;
function createSegmentBatches(segments) {
const batches = [];
let currentBatch = [];
let currentBatchChars = 0;
segments.forEach((text, segmentIndex) => {
const segmentLength = String(text || "").length;
if (segmentLength > MAX_CHARS_PER_EDGE_SEGMENT) {
throw createError(
ERROR_CODES.EDGE_TRANSLATE_FAILED,
"Edge translate segment was too long.",
{
reason: "edge-segment-too-long",
segmentIndex,
segmentLength,
maxSegmentLength: MAX_CHARS_PER_EDGE_SEGMENT
}
);
}
const wouldExceedSegmentCount = currentBatch.length >= MAX_SEGMENTS_PER_EDGE_BATCH;
const wouldExceedCharBudget = currentBatch.length > 0 && currentBatchChars + segmentLength > MAX_CHARS_PER_EDGE_BATCH;
if (wouldExceedSegmentCount || wouldExceedCharBudget) {
batches.push(currentBatch);
currentBatch = [];
currentBatchChars = 0;
}
currentBatch.push(text);
currentBatchChars += segmentLength;
});
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
return batches;
}
function createEdgeTranslator2({ edgeAuth, xhr, diagnostics = createNoopDiagnostics2() }) {
function getTranslatedText(item) {
const text = item?.translations?.[0]?.text;
if (typeof text !== "string") {
throw createError(ERROR_CODES.EDGE_TRANSLATE_FAILED, "Edge translate response was invalid.");
}
return text;
}
return {
translateSegments(segments, language) {
let activeAbort = () => {
};
let wasAborted = false;
const operation = (async () => {
const batches = createSegmentBatches(segments);
diagnostics.record("edge-translate.request.start", {
batchCount: batches.length,
segmentCount: segments.length,
targetLanguage: language.id
});
const tokenRequest = edgeAuth.getToken();
activeAbort = () => tokenRequest.abort?.();
const token = await tokenRequest;
if (wasAborted) {
throw createError(ERROR_CODES.TRANSLATION_CANCELLED, "Translation request was cancelled.");
}
const translatedSegments = [];
for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) {
if (wasAborted) {
throw createError(ERROR_CODES.TRANSLATION_CANCELLED, "Translation request was cancelled.");
}
const batch = batches[batchIndex];
const translateRequest = xhr.request({
method: "POST",
url: `https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${encodeURIComponent(language.microsoft)}`,
nocache: true,
timeout: 15e3,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
data: JSON.stringify(batch.map((text) => ({ text })))
});
activeAbort = () => translateRequest.abort?.();
if (wasAborted) {
activeAbort();
}
const response = await translateRequest;
diagnostics.record("edge-translate.response.received", {
batchIndex,
status: response?.status || 0,
responseTextLength: String(response?.responseText || "").length
});
let payload = null;
try {
payload = JSON.parse(response?.responseText || "[]");
} catch (error) {
throw createError(
ERROR_CODES.EDGE_TRANSLATE_FAILED,
"Edge translate response was invalid.",
{ batchIndex },
error
);
}
const batchTranslations = Array.isArray(payload) ? payload.map((item) => getTranslatedText(item)) : [];
diagnostics.record("edge-translate.response.parsed", {
batchIndex,
translatedSegmentCount: batchTranslations.length
});
if (batchTranslations.length !== batch.length) {
throw createError(
ERROR_CODES.EDGE_TRANSLATE_FAILED,
"Edge translated segment count mismatch.",
{
batchIndex,
expectedSegmentCount: batch.length,
actualSegmentCount: batchTranslations.length
}
);
}
translatedSegments.push(...batchTranslations);
}
return {
provider: "edge-web",
providerLabel: "Microsoft Edge(免 Key)",
targetLanguage: language.id,
targetLanguageLabel: language.label,
translatedSegments
};
})().catch((error) => {
diagnostics.recordError("edge-translate.request.error", error, {
requestStage: "edge-translate",
segmentCount: segments.length
});
throw normalizeError(
ERROR_CODES.EDGE_TRANSLATE_FAILED,
"Edge translate request failed.",
error,
{
requestStage: "edge-translate",
segmentCount: segments.length
}
);
});
return attachAbort(operation, () => {
wasAborted = true;
activeAbort();
});
}
};
}
module.exports = { createEdgeTranslator: createEdgeTranslator2 };
}
});
// src/translation/google-translator.js
var require_google_translator = __commonJS({
"src/translation/google-translator.js"(exports, module) {
var {
ERROR_CODES,
attachAbort,
createError,
normalizeError
} = require_errors();
var { createNoopDiagnostics: createNoopDiagnostics2 } = require_diagnostics();
var SEGMENT_CONCURRENCY = 4;
var MAX_SEGMENTS_PER_REQUEST = 20;
var MAX_URL_LENGTH = 1800;
var HTML_ENTITIES = Object.freeze({
amp: "&",
lt: "<",
gt: ">",
quot: '"',
apos: "'"
});
function decodeHtmlEntities(value) {
return String(value || "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity) => {
if (entity[0] === "#") {
const isHex = entity[1]?.toLowerCase() === "x";
const digits = isHex ? entity.slice(2) : entity.slice(1);
const codePoint = Number.parseInt(digits, isHex ? 16 : 10);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
}
return HTML_ENTITIES[entity.toLowerCase()] || match;
});
}
function extractTranslatedText(payload) {
const translatedText = Array.isArray(payload?.[0]) ? payload[0].map((item) => Array.isArray(item) && typeof item[0] === "string" ? item[0] : "").join("").trim() : "";
if (!translatedText) {
throw createError(ERROR_CODES.GOOGLE_TRANSLATE_FAILED, "Google translate response was invalid.");
}
return decodeHtmlEntities(translatedText);
}
function mapWithConcurrency(items, limit, mapper) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const currentIndex = nextIndex;
nextIndex += 1;
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
}
}
return Promise.all(
Array.from({ length: Math.min(limit, items.length) }, () => worker())
).then(() => results);
}
function createGoogleTranslateUrl(segment, language) {
const endpoint = new URL("https://translate.googleapis.com/translate_a/single");
endpoint.searchParams.set("client", "gtx");
endpoint.searchParams.set("sl", "auto");
endpoint.searchParams.set("tl", language.google || language.id);
endpoint.searchParams.set("dt", "t");
endpoint.searchParams.set("q", segment);
return endpoint.toString();
}
function createGoogleWebTranslator2({ xhr, diagnostics = createNoopDiagnostics2() }) {
return {
translateSegments(segments, language) {
let wasAborted = false;
const activeRequests = /* @__PURE__ */ new Set();
const operation = (async () => {
diagnostics.record("google-translate.request.start", {
segmentCount: segments.length,
targetLanguage: language.id
});
if (segments.length > MAX_SEGMENTS_PER_REQUEST) {
diagnostics.record("google-translate.request.rejected", {
reason: "too-many-segments",
segmentCount: segments.length,
segmentLimit: MAX_SEGMENTS_PER_REQUEST
});
throw createError(
ERROR_CODES.GOOGLE_TRANSLATE_FAILED,
"Google translate request was rejected before transport.",
{
reason: "too-many-segments",
segmentCount: segments.length,
segmentLimit: MAX_SEGMENTS_PER_REQUEST
}
);
}
const urls = segments.map((segment) => createGoogleTranslateUrl(segment, language));
const oversizedUrl = urls.find((url) => url.length > MAX_URL_LENGTH);
if (oversizedUrl) {
diagnostics.record("google-translate.request.rejected", {
reason: "url-too-long",
urlLength: oversizedUrl.length,
urlLengthLimit: MAX_URL_LENGTH
});
throw createError(
ERROR_CODES.GOOGLE_TRANSLATE_FAILED,
"Google translate request was rejected before transport.",
{
reason: "url-too-long",
urlLength: oversizedUrl.length,
urlLengthLimit: MAX_URL_LENGTH
}
);
}
const translatedSegments = await mapWithConcurrency(
urls,
SEGMENT_CONCURRENCY,
async (url) => {
if (wasAborted) {
throw createError(ERROR_CODES.TRANSLATION_CANCELLED, "Translation request was cancelled.");
}
const request = xhr.request({
method: "GET",
url,
nocache: true,
timeout: 15e3
});
activeRequests.add(request);
try {
const response = await request;
diagnostics.record("google-translate.response.received", {
status: response?.status || 0,
responseTextLength: String(response?.responseText || "").length
});
let payload = null;
try {
payload = JSON.parse(response?.responseText || "null");
} catch (error) {
throw createError(ERROR_CODES.GOOGLE_TRANSLATE_FAILED, "Google translate response was invalid.", {}, error);
}
return extractTranslatedText(payload);
} finally {
activeRequests.delete(request);
}
}
);
diagnostics.record("google-translate.response.parsed", {
translatedSegmentCount: translatedSegments.length
});
if (translatedSegments.length !== segments.length) {
throw createError(ERROR_CODES.GOOGLE_TRANSLATE_FAILED, "Google translated segment count mismatch.");
}
return {
provider: "google-web",
providerLabel: "Google Web(实验性 / 免费)",
targetLanguage: language.id,
targetLanguageLabel: language.label,
translatedSegments
};
})().catch((error) => {
diagnostics.recordError("google-translate.request.error", error, {
requestStage: "google-translate",
segmentCount: segments.length
});
throw normalizeError(
ERROR_CODES.GOOGLE_TRANSLATE_FAILED,
"Google translate request failed.",
error,
{
requestStage: "google-translate",
segmentCount: segments.length
}
);
});
return attachAbort(operation, () => {
wasAborted = true;
for (const request of Array.from(activeRequests)) {
request.abort?.();
}
});
}
};
}
module.exports = { createGoogleWebTranslator: createGoogleWebTranslator2 };
}
});
// src/translation/request-registry.js
var require_request_registry = __commonJS({
"src/translation/request-registry.js"(exports, module) {
function createRequestRegistry2() {
const activeRequests = /* @__PURE__ */ new Map();
function register(requestId, request) {
if (!requestId) {
return request;
}
if (request && typeof request.abort === "function") {
activeRequests.set(requestId, request);
} else {
activeRequests.delete(requestId);
}
return request;
}
function release(requestId) {
if (requestId) {
activeRequests.delete(requestId);
}
}
function cancel(requestId) {
const request = requestId ? activeRequests.get(requestId) : null;
activeRequests.delete(requestId);
if (request && typeof request.abort === "function") {
request.abort();
}
}
function cancelMany(requestIds) {
for (const requestId of requestIds || []) {
cancel(requestId);
}
}
function cancelAll() {
cancelMany(Array.from(activeRequests.keys()));
}
return {
register,
release,
cancel,
cancelMany,
cancelAll
};
}
module.exports = { createRequestRegistry: createRequestRegistry2 };
}
});
// src/translation/translate-service.js
var require_translate_service = __commonJS({
"src/translation/translate-service.js"(exports, module) {
var {
ERROR_CODES,
attachAbort,
createError
} = require_errors();
var { createNoopDiagnostics: createNoopDiagnostics2 } = require_diagnostics();
function createTranslateService2({
diagnostics = createNoopDiagnostics2(),
edgeTranslator,
languages,
providers,
translators
}) {
const translatorMap = translators || {
"edge-web": edgeTranslator
};
const providerIds = Array.isArray(providers) && providers.length > 0 ? providers.map((provider) => provider.id) : Object.keys(translatorMap);
function getProviderOrder(preferredProvider) {
const primaryProvider = providerIds.includes(preferredProvider) ? preferredProvider : providerIds[0];
return [
primaryProvider,
...providerIds.filter((providerId) => providerId !== primaryProvider)
];
}
function isRecoverableProviderError(error) {
return error?.code === ERROR_CODES.EDGE_AUTH_FAILED || error?.code === ERROR_CODES.EDGE_TRANSLATE_FAILED || error?.code === ERROR_CODES.GOOGLE_TRANSLATE_FAILED;
}
function shouldSkipFallbackProvider(fallbackProviderId, preferredProvider) {
return fallbackProviderId === "google-web" && preferredProvider !== "google-web";
}
function assertTranslatorContract(response, providerId, expectedSegmentCount) {
if (!Array.isArray(response?.translatedSegments)) {
throw createError(
ERROR_CODES.TRANSLATOR_CONTRACT_VIOLATION,
"Translator response did not match the request.",
{
providerId,
expectedSegmentCount,
actualSegmentCount: null,
reason: "translated-segments-not-array"
}
);
}
if (response.translatedSegments.length !== expectedSegmentCount) {
throw createError(
ERROR_CODES.TRANSLATOR_CONTRACT_VIOLATION,
"Translator response did not match the request.",
{
providerId,
expectedSegmentCount,
actualSegmentCount: response.translatedSegments.length,
reason: "translated-segment-count-mismatch"
}
);
}
const invalidSegmentIndex = response.translatedSegments.findIndex((item) => typeof item !== "string");
if (invalidSegmentIndex !== -1) {
throw createError(
ERROR_CODES.TRANSLATOR_CONTRACT_VIOLATION,
"Translator response did not match the request.",
{
providerId,
expectedSegmentCount,
actualSegmentCount: response.translatedSegments.length,
invalidSegmentIndex,
reason: "translated-segment-not-string"
}
);
}
return response;
}
return {
getLanguageDefinition(languageId) {
const language = languages.find((item) => item.id === languageId) || null;
if (!language) {
throw createError(
ERROR_CODES.UNSUPPORTED_TARGET_LANGUAGE,
"Unsupported target language."
);
}
return language;
},
translateSegments(segments, language, options = {}) {
const normalizedSegments = segments.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
let activeAbort = () => {
};
let wasAborted = false;
const operation = (async () => {
let lastError = null;
const providerOrder = getProviderOrder(options.preferredProvider);
for (let index = 0; index < providerOrder.length; index += 1) {
if (wasAborted) {
throw createError(ERROR_CODES.TRANSLATION_CANCELLED, "Translation request was cancelled.");
}
const providerId = providerOrder[index];
const translator = translatorMap[providerId];
if (!translator || typeof translator.translateSegments !== "function") {
continue;
}
const request = translator.translateSegments(normalizedSegments, language);
activeAbort = () => request.abort?.();
try {
const response = await request;
return assertTranslatorContract(response, providerId, normalizedSegments.length);
} catch (error) {
lastError = error;
const fallbackProviderId = providerOrder[index + 1];
if (!fallbackProviderId || !isRecoverableProviderError(error)) {
throw error;
}
if (shouldSkipFallbackProvider(fallbackProviderId, options.preferredProvider)) {
diagnostics.record("translate-service.provider.fallback.skipped", {
fromProvider: providerId,
toProvider: fallbackProviderId,
errorCode: error.code || "",
reason: "google-web-explicit-only"
});
throw error;
}
diagnostics.record("translate-service.provider.fallback", {
fromProvider: providerId,
toProvider: fallbackProviderId,
errorCode: error.code || ""
});
}
}
throw lastError || createError(ERROR_CODES.NO_TRANSLATOR_AVAILABLE, "No translator provider was available.");
})();
return attachAbort(operation, () => {
wasAborted = true;
activeAbort();
});
}
};
}
module.exports = { createTranslateService: createTranslateService2 };
}
});
// src/ui/i18n.js
var require_i18n = __commonJS({
"src/ui/i18n.js"(exports, module) {
var MESSAGES = Object.freeze({
zh: Object.freeze({
"content.translate": "翻译",
"content.restoreOriginal": "恢复原文",
"content.loading": "翻译中…",
"content.bodyNotFound": "邮件正文尚未加载完成。",
"content.noSegments": "没有可翻译的内容。",
"content.translationFailed": "翻译失败。",
"content.titleTranslationFailed": "标题翻译失败。",
"content.titleTranslationEmpty": "标题翻译结果为空。",
"diagnostics.toggleMenu": "FastTrMail 切换诊断日志",
"settings.menu": "FastTrMail 设置",
"settings.title": "FastTrMail 设置",
"settings.targetLanguage": "目标语言",
"settings.preferredProvider": "优先翻译工具",
"settings.save": "保存",
"settings.cancel": "取消",
"settings.saved": "设置已保存。",
"settings.saveFailed": "保存失败。",
"settings.providerNotice": "免 Key 翻译接口可能变更或失效;上线使用时建议保留备用 provider。",
"providers.edge-web": "Microsoft Edge(免 Key)",
"providers.google-web": "Google Web(实验性 / 免费)"
}),
en: Object.freeze({
"content.translate": "Translate",
"content.restoreOriginal": "Restore",
"content.loading": "Translating…",
"content.bodyNotFound": "Message body has not loaded yet.",
"content.noSegments": "No translatable content was found.",
"content.translationFailed": "Translation failed.",
"content.titleTranslationFailed": "Subject translation failed.",
"content.titleTranslationEmpty": "Subject translation was empty.",
"diagnostics.toggleMenu": "Toggle FastTrMail Diagnostics",
"settings.menu": "FastTrMail Settings",
"settings.title": "FastTrMail Settings",
"settings.targetLanguage": "Target language",
"settings.preferredProvider": "Preferred translator",
"settings.save": "Save",
"settings.cancel": "Cancel",
"settings.saved": "Settings saved.",
"settings.saveFailed": "Failed to save settings.",
"settings.providerNotice": "No-key translation endpoints may change or stop working; keep a fallback provider available for production use.",
"providers.edge-web": "Microsoft Edge (No Key)",
"providers.google-web": "Google Web (Experimental / Free)"
})
});
function normalizeLocale(language) {
return String(language || "").toLowerCase().startsWith("zh") ? "zh" : "en";
}
function createI18n2({ navigator = globalThis.navigator } = {}) {
const locale = normalizeLocale(navigator?.language);
return {
locale,
t(key, replacements = null, overrideLocale = locale) {
const dictionary = MESSAGES[normalizeLocale(overrideLocale)] || MESSAGES.en;
let message = dictionary[key] || MESSAGES.en[key] || key;
if (replacements && typeof replacements === "object") {
for (const [name, value] of Object.entries(replacements)) {
message = message.replaceAll(`{${name}}`, String(value));
}
}
return message;
}
};
}
module.exports = { createI18n: createI18n2, normalizeLocale };
}
});
// src/ui/menu-command.js
var require_menu_command = __commonJS({
"src/ui/menu-command.js"(exports, module) {
var MENU_COMMAND_REGISTRY_KEY = "__fastTrMailTampermonkeyMenuCommands__";
var DEFAULT_SCRIPT_KEY = "fasttrmail-tampermonkey";
function getRegistryScope(scope) {
if (scope && (typeof scope === "object" || typeof scope === "function")) {
return scope;
}
return globalThis;
}
function getRegisteredMenuCommands(scope, scriptKey) {
const registryScope = getRegistryScope(scope);
if (!registryScope[MENU_COMMAND_REGISTRY_KEY]) {
registryScope[MENU_COMMAND_REGISTRY_KEY] = /* @__PURE__ */ new Map();
}
const registriesByScript = registryScope[MENU_COMMAND_REGISTRY_KEY];
const resolvedScriptKey = scriptKey || DEFAULT_SCRIPT_KEY;
if (!registriesByScript.has(resolvedScriptKey)) {
registriesByScript.set(resolvedScriptKey, /* @__PURE__ */ new Map());
}
return registriesByScript.get(resolvedScriptKey);
}
function wrapAsyncMenuHandler(handler, { diagnostics, event }) {
return (...args) => {
try {
const result = handler(...args);
if (result && typeof result.then === "function") {
return result.catch((error) => {
diagnostics?.recordError?.(event, error, {});
});
}
return result;
} catch (error) {
diagnostics?.recordError?.(event, error, {});
return void 0;
}
};
}
function registerSettingsMenuCommand2({ tmApi, i18n, onClick, diagnostics }) {
return tmApi.registerMenuCommand(
i18n.t("settings.menu"),
wrapAsyncMenuHandler(onClick, {
diagnostics,
event: "ui.settings.menu.unexpected-error"
})
);
}
function registerDiagnosticsMenuCommand2({ tmApi, i18n, diagnosticsStore, diagnostics }) {
const menuLabel = i18n.t("diagnostics.toggleMenu");
return tmApi.registerMenuCommand(
menuLabel,
wrapAsyncMenuHandler(async () => {
const nextEnabled = await diagnosticsStore.toggleEnabled();
diagnostics?.setEnabled?.(nextEnabled);
}, {
diagnostics,
event: "ui.diagnostics.menu.unexpected-error"
})
);
}
function createMenuCommandRegistry2({ tmApi, scope = globalThis, scriptKey = DEFAULT_SCRIPT_KEY }) {
let activeKey = null;
const registeredMenuCommands = getRegisteredMenuCommands(scope, scriptKey);
function registerMenuCommand(label, handler) {
if (!activeKey) {
return tmApi.registerMenuCommand(label, handler);
}
const existing = registeredMenuCommands.get(activeKey);
if (existing) {
existing.label = label;
existing.handler = handler;
return existing.commandId;
}
const entry = {
label,
handler,
commandId: 0
};
entry.commandId = tmApi.registerMenuCommand(label, (...args) => entry.handler(...args));
registeredMenuCommands.set(activeKey, entry);
return entry.commandId;
}
return {
tmApi: {
...tmApi,
registerMenuCommand
},
ensure(key, register) {
activeKey = key;
try {
return register();
} finally {
activeKey = null;
}
}
};
}
module.exports = {
createMenuCommandRegistry: createMenuCommandRegistry2,
wrapAsyncMenuHandler,
registerSettingsMenuCommand: registerSettingsMenuCommand2,
registerDiagnosticsMenuCommand: registerDiagnosticsMenuCommand2
};
}
});
// src/ui/settings-modal.js
var require_settings_modal = __commonJS({
"src/ui/settings-modal.js"(exports, module) {
function createSettingsModal2({
constants,
i18n,
languages,
providers,
runtimeState,
document = globalThis.document
}) {
let overlay = null;
let restoreFocusTarget = null;
function isElement(node) {
const HTMLElementCtor = document.defaultView?.HTMLElement || globalThis.HTMLElement;
return typeof HTMLElementCtor === "function" && node instanceof HTMLElementCtor;
}
function getFocusableElements(container) {
return Array.from(container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)).filter((node) => isElement(node) && !node.disabled);
}
function close() {
if (!overlay) {
return;
}
const node = overlay;
const focusTarget = restoreFocusTarget;
overlay = null;
restoreFocusTarget = null;
runtimeState.withObserverMuted(() => {
node.remove();
});
if (isElement(focusTarget) && focusTarget.isConnected) {
focusTarget.focus();
}
}
function open({ currentSettings, onSave, currentTarget }) {
close();
let isSaving = false;
restoreFocusTarget = isElement(currentTarget) ? currentTarget : document.activeElement;
overlay = document.createElement("div");
overlay.className = constants.MODAL_OVERLAY_CLASS;
overlay.innerHTML = `
<div class="${constants.MODAL_CLASS}" role="dialog" aria-modal="true" aria-label="${i18n.t("settings.title")}">
<h2>${i18n.t("settings.title")}</h2>
<label for="fmt-target-language">${i18n.t("settings.targetLanguage")}</label>
<select id="fmt-target-language"></select>
<label for="fmt-preferred-provider">${i18n.t("settings.preferredProvider")}</label>
<select id="fmt-preferred-provider"></select>
<p class="fmt-settings-modal-provider-notice">${i18n.t("settings.providerNotice")}</p>
<div class="fmt-settings-modal-error" aria-live="polite"></div>
<footer>
<button type="button" data-variant="secondary">${i18n.t("settings.cancel")}</button>
<button type="button" data-variant="primary">${i18n.t("settings.save")}</button>
</footer>
</div>
`;
const select = overlay.querySelector("#fmt-target-language");
const providerSelect = overlay.querySelector("#fmt-preferred-provider");
const errorNode = overlay.querySelector(".fmt-settings-modal-error");
const cancelButton = overlay.querySelector('button[data-variant="secondary"]');
const saveButton = overlay.querySelector('button[data-variant="primary"]');
const syncPendingState = () => {
select.disabled = isSaving;
providerSelect.disabled = isSaving;
cancelButton.disabled = isSaving;
saveButton.disabled = isSaving;
};
const handleKeyDown = (event) => {
if (event.key === "Escape") {
if (!isSaving) {
event.preventDefault();
close();
}
return;
}
if (event.key !== "Tab") {
return;
}
const focusableElements = getFocusableElements(overlay);
if (focusableElements.length === 0) {
event.preventDefault();
return;
}
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const activeElement = document.activeElement;
if (event.shiftKey && activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
};
for (const language of languages) {
const option = document.createElement("option");
option.value = language.id;
option.textContent = language.label;
option.selected = language.id === currentSettings?.targetLanguage;
select.appendChild(option);
}
for (const provider of providers) {
const option = document.createElement("option");
option.value = provider.id;
option.textContent = provider.label;
option.selected = provider.id === currentSettings?.preferredProvider;
providerSelect.appendChild(option);
}
overlay.addEventListener("click", (event) => {
if (isSaving) {
return;
}
if (event.target === overlay) {
close();
}
});
overlay.addEventListener("keydown", handleKeyDown);
cancelButton.addEventListener("click", () => {
if (isSaving) {
return;
}
close();
});
saveButton.addEventListener("click", async () => {
if (isSaving) {
return;
}
isSaving = true;
syncPendingState();
if (errorNode instanceof globalThis.HTMLElement) {
errorNode.textContent = "";
}
try {
await onSave({
targetLanguage: select.value,
preferredProvider: providerSelect.value
});
close();
} catch (error) {
if (errorNode instanceof globalThis.HTMLElement) {
errorNode.textContent = typeof error?.message === "string" && error.message.trim() ? error.message.trim() : i18n.t("settings.saveFailed");
}
} finally {
isSaving = false;
if (overlay) {
syncPendingState();
}
}
});
runtimeState.withObserverMuted(() => {
document.body.appendChild(overlay);
});
select.focus();
}
return {
close,
open
};
}
module.exports = { createSettingsModal: createSettingsModal2 };
}
});
// src/ui/styles.js
var require_styles = __commonJS({
"src/ui/styles.js"(exports, module) {
function createStyles2(constants) {
return `
.${constants.BUTTON_CLASS}.${constants.BUTTON_ACTIVE_CLASS} {
color: #467dde;
font-weight: 600;
}
.${constants.TITLE_TRANSLATION_CLASS},
.${constants.INLINE_TRANSLATION_CLASS},
.${constants.MESSAGE_STATUS_CLASS} {
margin-top: 8px;
padding: 8px 10px;
border-radius: 8px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.${constants.TITLE_TRANSLATION_CLASS},
.${constants.INLINE_TRANSLATION_CLASS},
.${constants.TITLE_TRANSLATION_CLASS} .fmt-title-translation-content,
.${constants.INLINE_TRANSLATION_CLASS} .fmt-inline-translation-content {
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.${constants.TITLE_TRANSLATION_CLASS}[data-state="done"],
.${constants.INLINE_TRANSLATION_CLASS}[data-state="done"] {
color: #467dde;
background: rgba(70, 125, 222, 0.09);
}
.${constants.TITLE_TRANSLATION_CLASS}[data-state="loading"],
.${constants.INLINE_TRANSLATION_CLASS}[data-state="loading"],
.${constants.MESSAGE_STATUS_CLASS}[data-state="info"] {
color: #76859c;
background: rgba(170, 182, 200, 0.1);
}
.${constants.TITLE_TRANSLATION_CLASS}[data-state="error"],
.${constants.INLINE_TRANSLATION_CLASS}[data-state="error"],
.${constants.MESSAGE_STATUS_CLASS}[data-state="error"] {
color: #c94b5b;
background: rgba(255, 122, 122, 0.08);
}
.${constants.MODAL_OVERLAY_CLASS} {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(17, 24, 39, 0.38);
backdrop-filter: blur(6px);
}
.${constants.MODAL_CLASS} {
width: min(420px, 100%);
padding: 20px;
border-radius: 16px;
background: #ffffff;
color: #1f2937;
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
font-family: "SF Pro Text", "PingFang SC", "Microsoft YaHei", sans-serif;
}
.${constants.MODAL_CLASS} h2 {
margin: 0 0 16px;
font-size: 18px;
line-height: 1.3;
}
.${constants.MODAL_CLASS} label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #4b5563;
}
.${constants.MODAL_CLASS} select {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #d1d5db;
border-radius: 10px;
background: #ffffff;
color: #111827;
font-size: 14px;
}
.${constants.MODAL_CLASS} .fmt-settings-modal-provider-notice {
margin: 8px 0 0;
color: #6b7280;
font-size: 12px;
line-height: 1.45;
}
.${constants.MODAL_CLASS} .fmt-settings-modal-error {
min-height: 20px;
margin-top: 10px;
color: #c94b5b;
font-size: 13px;
line-height: 1.4;
}
.${constants.MODAL_CLASS} footer {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 20px;
}
.${constants.MODAL_CLASS} button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
height: 38px;
padding: 0 14px;
border: 0;
border-radius: 10px;
font-size: 14px;
cursor: pointer;
}
.${constants.MODAL_CLASS} button[data-variant="secondary"] {
background: #e5e7eb;
color: #1f2937;
}
.${constants.MODAL_CLASS} button[data-variant="primary"] {
background: #2563eb;
color: #ffffff;
}
`;
}
module.exports = { createStyles: createStyles2 };
}
});
// src/ui/toolbar-button.js
var require_toolbar_button = __commonJS({
"src/ui/toolbar-button.js"(exports, module) {
function createToolbarButton({ document, constants, i18n }) {
const button = document.createElement("button");
button.type = "button";
button.className = `v-Button v-Button--subtleStandard v-Button--sizeM ${constants.BUTTON_CLASS}`;
button.innerHTML = '<span class="label"></span>';
updateToolbarButtonState(button, false, { constants, i18n });
return button;
}
function normalizeActionLabel(text) {
return String(text || "").replace(/\s+/g, " ").trim().toLowerCase();
}
function isMoreActionLabel(text) {
const normalized = normalizeActionLabel(text);
return normalized === "more" || normalized === "更多";
}
function getButtonLabel(button) {
if (!button) {
return "";
}
if (typeof button.textContent === "string" && button.classList?.contains("label")) {
return button.textContent;
}
if (typeof button.querySelector === "function") {
return button.querySelector(".label")?.textContent || button.textContent || "";
}
return button.textContent || "";
}
function placeToolbarButton(toolbar, button, divider) {
const actionButtons = Array.from(toolbar.querySelectorAll(":scope > button"));
const anchorButton = actionButtons.find((candidate) => {
return candidate !== button && isMoreActionLabel(getButtonLabel(candidate));
});
const spacer = toolbar.querySelector(".v-Toolbar-flex");
if (anchorButton?.nextElementSibling) {
toolbar.insertBefore(divider, anchorButton.nextElementSibling);
toolbar.insertBefore(button, divider.nextElementSibling);
return;
}
if (spacer instanceof HTMLElement) {
toolbar.insertBefore(divider, spacer);
toolbar.insertBefore(button, spacer);
return;
}
toolbar.appendChild(divider);
toolbar.appendChild(button);
}
function updateToolbarButtonState(button, isActive, { constants, i18n }) {
const label = i18n.t(isActive ? "content.restoreOriginal" : "content.translate");
button.setAttribute("title", label);
button.setAttribute("aria-label", label);
button.classList.toggle(constants.BUTTON_ACTIVE_CLASS, isActive);
const labelNode = button.querySelector(".label");
if (labelNode) {
labelNode.textContent = label;
}
}
module.exports = {
createToolbarButton,
getButtonLabel,
isMoreActionLabel,
placeToolbarButton,
updateToolbarButtonState
};
}
});
// src/ui/translation-renderer.js
var require_translation_renderer = __commonJS({
"src/ui/translation-renderer.js"(exports, module) {
function createTranslationRenderer2({
constants,
i18n,
runtimeState,
segmenter,
document = globalThis.document
}) {
function getMessageTranslationScope(descriptor) {
if (descriptor?.contentRoot instanceof globalThis.HTMLElement && descriptor.contentRoot.isConnected) {
return descriptor.contentRoot;
}
if (descriptor?.body instanceof globalThis.HTMLElement && descriptor.body.isConnected) {
return descriptor.body;
}
if (descriptor?.messageNode instanceof globalThis.HTMLElement && descriptor.messageNode.isConnected) {
return descriptor.messageNode;
}
if (descriptor?.card instanceof globalThis.HTMLElement && descriptor.card.isConnected) {
return descriptor.card;
}
return null;
}
function removeInlineTranslations(bodyElement) {
if (!(bodyElement instanceof globalThis.HTMLElement)) {
return;
}
runtimeState.withObserverMuted(() => {
bodyElement.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}, .${constants.SEGMENT_ANCHOR_CLASS}, .${constants.TRANSLATION_WRAPPER_CLASS}`).forEach((node) => node.remove());
});
}
function clearMessageTranslations(descriptor) {
const scope = getMessageTranslationScope(descriptor);
if (!(scope instanceof globalThis.HTMLElement)) {
return;
}
runtimeState.withObserverMuted(() => {
scope.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}, .${constants.SEGMENT_ANCHOR_CLASS}, .${constants.TRANSLATION_WRAPPER_CLASS}`).forEach((node) => node.remove());
});
}
function pruneMessageTranslations(descriptor, currentSegments) {
const scope = getMessageTranslationScope(descriptor);
if (!(scope instanceof globalThis.HTMLElement)) {
return;
}
const currentSegmentIds = new Set(
Array.isArray(currentSegments) ? currentSegments.map((segment) => segment?.id).filter(Boolean) : []
);
if (currentSegmentIds.size === 0) {
clearMessageTranslations(descriptor);
return;
}
runtimeState.withObserverMuted(() => {
scope.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}`).forEach((node) => {
const segmentId = node.getAttribute(constants.SEGMENT_ATTRIBUTE);
if (segmentId && currentSegmentIds.has(segmentId)) {
return;
}
const rowWrapper = node.closest(`tr.${constants.TRANSLATION_WRAPPER_CLASS}`);
if (rowWrapper instanceof globalThis.HTMLElement) {
rowWrapper.remove();
return;
}
const cellWrapper = node.closest(`td.${constants.TRANSLATION_WRAPPER_CLASS}`);
if (cellWrapper instanceof globalThis.HTMLElement) {
cellWrapper.remove();
return;
}
node.remove();
});
scope.querySelectorAll(`.${constants.SEGMENT_ANCHOR_CLASS}`).forEach((node) => {
const translationNode = node.nextElementSibling;
if (translationNode instanceof globalThis.HTMLElement && translationNode.classList.contains(constants.INLINE_TRANSLATION_CLASS) && currentSegmentIds.has(translationNode.getAttribute(constants.SEGMENT_ATTRIBUTE) || "")) {
return;
}
node.remove();
});
});
}
function clearThreadRenderArtifacts(threadRoot) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return;
}
runtimeState.withObserverMuted(() => {
threadRoot.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}, .${constants.SEGMENT_ANCHOR_CLASS}, .${constants.MESSAGE_STATUS_CLASS}, .${constants.TITLE_TRANSLATION_CLASS}, .${constants.TRANSLATION_WRAPPER_CLASS}`).forEach((node) => node.remove());
});
}
function hasThreadRenderArtifacts(threadRoot) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return false;
}
return threadRoot.querySelector(
`.${constants.INLINE_TRANSLATION_CLASS}, .${constants.SEGMENT_ANCHOR_CLASS}, .${constants.MESSAGE_STATUS_CLASS}, .${constants.TITLE_TRANSLATION_CLASS}, .${constants.TRANSLATION_WRAPPER_CLASS}`
) !== null;
}
function getThreadRenderArtifactCounts(threadRoot) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return {
inlineTranslations: 0,
titleTranslations: 0,
messageStatuses: 0,
segmentAnchors: 0,
translationWrappers: 0
};
}
return {
inlineTranslations: threadRoot.querySelectorAll(`.${constants.INLINE_TRANSLATION_CLASS}`).length,
titleTranslations: threadRoot.querySelectorAll(`.${constants.TITLE_TRANSLATION_CLASS}`).length,
messageStatuses: threadRoot.querySelectorAll(`.${constants.MESSAGE_STATUS_CLASS}`).length,
segmentAnchors: threadRoot.querySelectorAll(`.${constants.SEGMENT_ANCHOR_CLASS}`).length,
translationWrappers: threadRoot.querySelectorAll(`.${constants.TRANSLATION_WRAPPER_CLASS}`).length
};
}
function clearTitleTranslation(threadRoot) {
const titleNode = findTitleTranslationNode(threadRoot);
if (!titleNode) {
return;
}
runtimeState.withObserverMuted(() => {
titleNode.remove();
});
}
function renderTitleTranslation(threadRoot, sourceElement, content, state = "done") {
if (!(threadRoot instanceof globalThis.HTMLElement) || !(sourceElement instanceof globalThis.HTMLElement)) {
return false;
}
const normalizedContent = normalizeTranslationText(content);
const existingNode = findTitleTranslationNode(threadRoot);
if (existingNode) {
return setTitleTranslationState(existingNode, { state, content: normalizedContent });
}
const node = document.createElement("div");
node.className = constants.TITLE_TRANSLATION_CLASS;
node.dataset.state = state;
node.innerHTML = '<div class="fmt-title-translation-content"></div>';
if (!setTitleTranslationState(node, { state, content: normalizedContent })) {
return false;
}
runtimeState.withObserverMuted(() => {
sourceElement.insertAdjacentElement("afterend", node);
});
return node.isConnected;
}
function clearMessageStatus(descriptor) {
const statusNode = findMessageStatusNode(descriptor);
if (statusNode) {
runtimeState.withObserverMuted(() => {
statusNode.remove();
});
}
}
function renderMessageStatus(descriptor, message, state = "info") {
clearMessageStatus(descriptor);
const mountTarget = getMessageStatusMountTarget(descriptor);
if (!(mountTarget instanceof globalThis.HTMLElement)) {
return false;
}
const node = document.createElement("div");
node.className = constants.MESSAGE_STATUS_CLASS;
node.dataset.state = state;
node.dataset.messageKey = descriptor.key || "";
node.textContent = normalizeTranslationText(message);
runtimeState.withObserverMuted(() => {
if (mountTarget.matches(".v-Message-body, .u-containSelection, .u-article, article, pre")) {
mountTarget.insertAdjacentElement("afterbegin", node);
} else {
mountTarget.insertAdjacentElement("afterend", node);
}
});
return node.isConnected;
}
function createRenderResult(expectedCount) {
return {
ok: true,
expectedCount,
renderedCount: 0,
failedSegments: []
};
}
function renderLoadingTranslations(segments) {
return renderSegmentNodes(segments, segments.map(() => i18n.t("content.loading")), "loading");
}
function renderTranslatedSegments(segments, translatedSegments) {
return renderSegmentNodes(segments, translatedSegments, "done");
}
function renderSegmentError(segments, message) {
return renderSegmentNodes(segments, segments.map(() => message), "error");
}
function renderSegmentNodes(segments, contents, state) {
const result = createRenderResult(segments.length);
segments.forEach((segment, index) => {
let translationNode = findInlineTranslationNode(segment);
if (!translationNode) {
translationNode = insertTranslationNode(segment, {
state,
content: contents[index]
});
}
if (!translationNode) {
result.ok = false;
result.failedSegments.push({
segmentId: segment.id,
reason: "insert-failed"
});
return;
}
const didUpdate = setInlineTranslationState(translationNode, {
state,
content: contents[index]
});
if (!didUpdate) {
result.ok = false;
result.failedSegments.push({
segmentId: segment.id,
reason: "update-failed"
});
return;
}
result.renderedCount += 1;
});
return result;
}
function insertTranslationNode(segment, { state, content }) {
const node = document.createElement("div");
node.className = constants.INLINE_TRANSLATION_CLASS;
node.dataset.state = state;
node.setAttribute(constants.SEGMENT_ATTRIBUTE, segment.id || "");
node.innerHTML = '<div class="fmt-inline-translation-content"></div>';
setInlineTranslationState(node, { state, content });
const inserted = runtimeState.withObserverMuted(() => insertTranslationAtValidPosition(segment, node));
return inserted ? node : null;
}
function setInlineTranslationState(node, { state, content }) {
if (!(node instanceof globalThis.HTMLElement)) {
return false;
}
const normalizedContent = normalizeTranslationText(content);
const contentNode = node.querySelector(".fmt-inline-translation-content");
if (!(contentNode instanceof globalThis.HTMLElement)) {
return false;
}
const normalizedState = String(state);
if (node.dataset.state === normalizedState && contentNode.textContent === normalizedContent) {
return true;
}
return runtimeState.withObserverMuted(() => {
if (node.dataset.state !== normalizedState) {
node.dataset.state = normalizedState;
}
if (contentNode.textContent !== normalizedContent) {
contentNode.textContent = normalizedContent;
}
return true;
});
}
function setTitleTranslationState(node, { state, content }) {
if (!(node instanceof globalThis.HTMLElement)) {
return false;
}
const normalizedContent = normalizeTranslationText(content);
const contentNode = node.querySelector(".fmt-title-translation-content");
if (!(contentNode instanceof globalThis.HTMLElement)) {
return false;
}
const normalizedState = String(state);
if (node.dataset.state === normalizedState && contentNode.textContent === normalizedContent) {
return true;
}
return runtimeState.withObserverMuted(() => {
if (node.dataset.state !== normalizedState) {
node.dataset.state = normalizedState;
}
if (contentNode.textContent !== normalizedContent) {
contentNode.textContent = normalizedContent;
}
return true;
});
}
function findInlineTranslationNode(segmentOrId) {
const segmentId = typeof segmentOrId === "string" ? segmentOrId : segmentOrId?.id;
if (!segmentId) {
return null;
}
if (segmentOrId && typeof segmentOrId === "object") {
const localNode = findInlineTranslationNodeNearSegment(segmentOrId, segmentId);
if (localNode) {
return localNode;
}
}
return document.querySelector(`.${constants.INLINE_TRANSLATION_CLASS}[${constants.SEGMENT_ATTRIBUTE}="${segmentId}"]`);
}
function findInlineTranslationNodeNearSegment(segment, segmentId) {
if (segment.referenceNode) {
const anchor = segmenter.getExistingSegmentAnchor(segment.referenceNode);
const sibling = anchor?.nextElementSibling || null;
if (matchesInlineTranslationNode(sibling, segmentId)) {
return sibling;
}
}
const element = segment.element;
if (!(element instanceof globalThis.HTMLElement)) {
return null;
}
if (matchesInlineTranslationNode(element.lastElementChild, segmentId)) {
return element.lastElementChild;
}
if (matchesInlineTranslationNode(element.nextElementSibling, segmentId)) {
return element.nextElementSibling;
}
const rowSibling = element.parentElement?.nextElementSibling;
if (rowSibling instanceof globalThis.HTMLElement) {
const rowNode = rowSibling.querySelector(`.${constants.INLINE_TRANSLATION_CLASS}[${constants.SEGMENT_ATTRIBUTE}="${segmentId}"]`);
if (rowNode instanceof globalThis.HTMLElement) {
return rowNode;
}
}
return null;
}
function matchesInlineTranslationNode(node, segmentId) {
return node instanceof globalThis.HTMLElement && node.classList.contains(constants.INLINE_TRANSLATION_CLASS) && node.getAttribute(constants.SEGMENT_ATTRIBUTE) === segmentId;
}
function insertTranslationAtValidPosition(segment, node) {
if (segment.referenceNode) {
const anchor = segmenter.ensureSegmentAnchor(segment.referenceNode);
if (!(anchor instanceof globalThis.HTMLElement) || !anchor.isConnected) {
return false;
}
anchor.insertAdjacentElement("afterend", node);
return node.isConnected;
}
const element = segment.element;
if (!(element instanceof globalThis.HTMLElement)) {
return false;
}
const tagName = element.tagName.toLowerCase();
if (tagName === "td" || tagName === "th") {
element.appendChild(node);
return true;
}
const parentTag = element.parentElement?.tagName?.toLowerCase();
if (parentTag === "tr") {
const wrapper = document.createElement("td");
wrapper.className = constants.TRANSLATION_WRAPPER_CLASS;
const colSpan = element.parentElement.children.length;
if (colSpan > 1) {
wrapper.colSpan = colSpan;
}
wrapper.appendChild(node);
const row = document.createElement("tr");
row.className = constants.TRANSLATION_WRAPPER_CLASS;
row.appendChild(wrapper);
element.parentElement.insertAdjacentElement("afterend", row);
return true;
}
element.insertAdjacentElement("afterend", node);
return true;
}
function getMessageStatusMountTarget(descriptor) {
if (descriptor?.body instanceof globalThis.HTMLElement && descriptor.body.isConnected) {
return descriptor.body;
}
if (descriptor?.messageNode instanceof globalThis.HTMLElement && descriptor.messageNode.isConnected) {
return descriptor.messageNode;
}
if (descriptor?.card instanceof globalThis.HTMLElement && descriptor.card.isConnected) {
return descriptor.card;
}
return null;
}
function findMessageStatusNode(descriptor) {
const key = descriptor?.key;
if (!key) {
return null;
}
return document.querySelector(`.${constants.MESSAGE_STATUS_CLASS}[data-message-key="${key}"]`);
}
function findTitleTranslationNode(threadRoot) {
if (!(threadRoot instanceof globalThis.HTMLElement)) {
return null;
}
return threadRoot.querySelector(`.${constants.TITLE_TRANSLATION_CLASS}`);
}
function normalizeTranslationText(text) {
return String(text || "").replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
}
return {
clearMessageTranslations,
clearMessageStatus,
clearThreadRenderArtifacts,
clearTitleTranslation,
findInlineTranslationNode,
findTitleTranslationNode,
getMessageStatusMountTarget,
getThreadRenderArtifactCounts,
hasThreadRenderArtifacts,
pruneMessageTranslations,
removeInlineTranslations,
renderLoadingTranslations,
renderMessageStatus,
renderSegmentError,
renderTitleTranslation,
renderTranslatedSegments
};
}
module.exports = { createTranslationRenderer: createTranslationRenderer2 };
}
});
// src/entry/main.js
var { DEFAULT_SETTINGS, DOM_CONSTANTS } = require_defaults();
var { LANGUAGE_DEFINITIONS } = require_languages();
var { PROVIDER_DEFINITIONS } = require_providers();
var { createRuntimeState } = require_runtime_state();
var { createApp } = require_app();
var { createLifecycle } = require_lifecycle();
var { createDiagnostics, createNoopDiagnostics } = require_diagnostics();
var { createDiagnosticsStore } = require_store();
var { createSegmenter } = require_segmenter();
var pageLocator = require_page_locator();
var { createThreadDom } = require_thread_dom();
var textPolicies = require_text_policies();
var { createTmApi } = require_tm_api();
var { createXhr } = require_xhr();
var { createSettingsStore } = require_settings_store();
var { createEdgeAuth } = require_edge_auth();
var { createEdgeTranslator } = require_edge_translator();
var { createGoogleWebTranslator } = require_google_translator();
var { createRequestRegistry } = require_request_registry();
var { createTranslateService } = require_translate_service();
var { createI18n } = require_i18n();
var {
createMenuCommandRegistry,
registerDiagnosticsMenuCommand,
registerSettingsMenuCommand
} = require_menu_command();
var { createSettingsModal } = require_settings_modal();
var { createStyles } = require_styles();
var toolbarButtonApi = require_toolbar_button();
var { createTranslationRenderer } = require_translation_renderer();
var BOOTSTRAP_KEY = "__fastTrMailTampermonkeyBootstrap__";
var BOOTSTRAP_RETRY_KEY = "__fastTrMailTampermonkeyBootstrapRetries__";
var MAX_BOOTSTRAP_RETRIES = 3;
function ensureStyles(document, constants) {
if (document.head.querySelector(`style[${constants.STYLES_ATTRIBUTE}]`)) {
return;
}
const style = document.createElement("style");
style.setAttribute(constants.STYLES_ATTRIBUTE, "true");
style.textContent = createStyles(constants);
document.head.appendChild(style);
}
async function bootstrap() {
let diagnostics = createNoopDiagnostics();
if (globalThis[BOOTSTRAP_KEY] === "ready" || globalThis[BOOTSTRAP_KEY] === "booting") {
return;
}
if (!pageLocator.isFastmailUrl(globalThis.location)) {
return;
}
globalThis[BOOTSTRAP_KEY] = "booting";
try {
const tmApi = createTmApi(globalThis);
const menuCommands = createMenuCommandRegistry({ tmApi });
const diagnosticsStore = createDiagnosticsStore({ tmApi });
diagnostics = createDiagnostics({
enabled: await diagnosticsStore.getEnabled()
});
const xhr = createXhr({ tmApi });
const settingsStore = createSettingsStore({
tmApi,
defaults: DEFAULT_SETTINGS,
languages: LANGUAGE_DEFINITIONS,
providers: PROVIDER_DEFINITIONS
});
const edgeAuth = createEdgeAuth({ diagnostics, xhr });
const edgeTranslator = createEdgeTranslator({ diagnostics, edgeAuth, xhr });
const googleTranslator = createGoogleWebTranslator({ diagnostics, xhr });
const translateService = createTranslateService({
diagnostics,
languages: LANGUAGE_DEFINITIONS,
providers: PROVIDER_DEFINITIONS,
translators: {
"edge-web": edgeTranslator,
"google-web": googleTranslator
}
});
const i18n = createI18n();
const runtimeState = createRuntimeState({
getLocationKey: () => pageLocator.getLocationKey(globalThis.location)
});
const requestRegistry = createRequestRegistry();
const segmenter = createSegmenter({
constants: DOM_CONSTANTS,
policies: textPolicies,
runtimeState
});
const threadDom = createThreadDom({
constants: DOM_CONSTANTS,
i18n,
pageLocator,
policies: textPolicies,
runtimeState,
segmenter,
toolbarButtonApi
});
const renderer = createTranslationRenderer({
constants: DOM_CONSTANTS,
i18n,
runtimeState,
segmenter
});
const settingsModal = createSettingsModal({
constants: DOM_CONSTANTS,
i18n,
languages: LANGUAGE_DEFINITIONS,
providers: PROVIDER_DEFINITIONS.map((provider) => ({
id: provider.id,
label: i18n.t(`providers.${provider.id}`)
})),
runtimeState
});
const app = createApp({
defaults: DEFAULT_SETTINGS,
document: globalThis.document,
diagnostics,
i18n,
policies: textPolicies,
renderer,
requestRegistry,
runtimeState,
segmenter,
settingsModal,
settingsStore,
threadDom,
translateService
});
const lifecycle = createLifecycle({
app,
constants: DOM_CONSTANTS,
document: globalThis.document,
runtimeState,
threadDom
});
ensureStyles(globalThis.document, DOM_CONSTANTS);
menuCommands.ensure("settings", () => registerSettingsMenuCommand({
tmApi: menuCommands.tmApi,
i18n,
diagnostics,
onClick: app.openSettings
}));
menuCommands.ensure("diagnostics", () => registerDiagnosticsMenuCommand({
tmApi: menuCommands.tmApi,
i18n,
diagnostics,
diagnosticsStore
}));
await app.ensureTargetLanguageId();
lifecycle.initialize();
diagnostics.record("bootstrap.ready", {
locationKey: pageLocator.getLocationKey(globalThis.location)
});
globalThis[BOOTSTRAP_KEY] = "ready";
globalThis[BOOTSTRAP_RETRY_KEY] = 0;
} catch (error) {
globalThis[BOOTSTRAP_KEY] = "idle";
const retryCount = Number(globalThis[BOOTSTRAP_RETRY_KEY] || 0) + 1;
globalThis[BOOTSTRAP_RETRY_KEY] = retryCount;
diagnostics.recordError("bootstrap.failed", error, {
retryCount
});
console.error("[FastTrMail] bootstrap failed", error);
if (retryCount < MAX_BOOTSTRAP_RETRIES) {
globalThis.setTimeout(() => {
void bootstrap();
}, retryCount * 1e3);
}
}
}
if (globalThis.document?.readyState === "loading") {
globalThis.document.addEventListener("DOMContentLoaded", () => {
void bootstrap();
}, { once: true });
} else {
void bootstrap();
}
})();