Enjoy DeepL Unlimited

Robust and maintainable DeepL unlimited usage script

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Enjoy DeepL Unlimited
// @namespace    http://tampermonkey.net/
// @version      0.5.0
// @description  Robust and maintainable DeepL unlimited usage script
// @author       fleey
// @match        https://*.deepl.com/translator
// @match        https://*.deepl.com/*/translator
// @grant        GM_setClipboard
// @icon         https://www.deepl.com/favicon.ico
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // Global state management
  const AppState = {
    isResetting: false,
    isInitialized: false,
    lastResetTime: 0,
    detectionCount: 0,
    errors: []
  };

  /**
   * Configuration Manager - Centralized configuration with validation
   */
  class ConfigManager {
    constructor() {
      this.config = {
        // Performance settings
        performance: {
          debounceDelay: 300,
          initialScanDelay: 3000, // Increased delay to ensure page is fully loaded
          maxRetries: 3,
          retryDelay: 1000,
          resetCooldown: 10000 // Increased cooldown to prevent frequent resets
        },

        // Detection settings
        detection: {
          keywords: ['300,000', 'unlimited characters'],
          promotionKeywords: [
            '免费试用 DeepL Pro',
            'try DeepL Pro for free',
            'nearing your monthly character limit',
            'Get up to unlimited characters',
            'You\'re nearing your monthly character limit'
          ],
          // More specific keywords that require exact context
          strictKeywords: [
            '免费试用',
            'DeepL Pro'
          ],
          // Minimum text length for detection
          minTextLength: 10,
          // Maximum text length to avoid false positives
          maxTextLength: 500,
          selectors: {
            // Multiple possible selectors for source text area
            sourceText: [
              'div[aria-labelledby="translation-source-heading"] d-textarea',
              'div[aria-labelledby="translation-source-heading"] textarea',
              'd-textarea[aria-labelledby="translation-source-heading"]',
              'textarea[aria-labelledby="translation-source-heading"]',
              '[data-testid="translator-source-input"]',
              '.lmt__source_textarea',
              '#source-dummydiv',
              'div[contenteditable="true"][role="textbox"]'
            ],
            promotionAreas: [
              'div.border-1.-my-4.mx-4.hidden.max-w-md.items-center.justify-start.gap-3.rounded-lg.border-blue-200.bg-blue-50.p-4.text-blue-700.xl\\:flex.min-\\[1440px\\]\\:mx-0',
              'span.__content.with-content-center',
              'div:has(a[href="/pro"])',
              'a.Button.as-link.as-no-padding.as-medium.-font-size-md'
            ],
            // More targeted common areas, excluding generic header and overly broad selectors
            commonAreas: ['.banner', '[class*="promotion"]', '[class*="trial"]', '[class*="limit"]', '[class*="upgrade"]', '[class*="promo"]']
          },
          // Whitelist patterns to ignore
          whitelist: [
            'DeepL翻译器',
            'DeepL Translator',
            'navigation',
            'menu',
            'logo',
            '每天有数百万人使用DeepL翻译',
            'DeepL Write',
            '人工智能写作助手',
            '翻译模式',
            '翻译文本',
            '翻译文件',
            '35 种语言',
            'millions of people translate with DeepL',
            'AI writing assistant',
            'translation modes',
            'translate text',
            'translate files',
            'languages'
          ]
        },

        // Cookie management
        cookies: {
          names: ['dapSid', 'LMTBID', 'dapUid'],
          domain: '.deepl.com'
        },

        // Logging settings
        logging: {
          level: 'INFO', // DEBUG, INFO, WARN, ERROR
          maxLogEntries: 100
        },

        // Debug settings
        debug: {
          enabled: false, // Set to true for detailed debugging
          logDetectionAttempts: false
        }
      };
    }

    get(path) {
      return path.split('.').reduce((obj, key) => obj?.[key], this.config);
    }

    set(path, value) {
      const keys = path.split('.');
      const lastKey = keys.pop();
      const target = keys.reduce((obj, key) => obj[key] = obj[key] || {}, this.config);
      target[lastKey] = value;
    }

    validate() {
      const required = ['detection.keywords', 'detection.selectors.sourceText', 'cookies.names'];
      return required.every(path => this.get(path) !== undefined);
    }
  }

  /**
   * Logger - Unified logging system with levels and performance tracking
   */
  class Logger {
    constructor(config) {
      this.config = config;
      this.logs = [];
      this.levels = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
      this.currentLevel = this.levels[config.get('logging.level')] || this.levels.INFO;
    }

    log(level, message, data = null) {
      if (this.levels[level] < this.currentLevel) return;

      const entry = {
        timestamp: new Date().toISOString(),
        level,
        message,
        data,
        stack: level === 'ERROR' ? new Error().stack : null
      };

      this.logs.push(entry);

      // Limit log entries
      const maxEntries = this.config.get('logging.maxLogEntries');
      if (this.logs.length > maxEntries) {
        this.logs = this.logs.slice(-maxEntries);
      }

      // Console output
      const consoleMethod = level.toLowerCase() === 'error' ? 'error' :
        level.toLowerCase() === 'warn' ? 'warn' : 'log';
      console[consoleMethod](`[DeepL Unlimited] ${message}`, data || '');
    }

    debug(message, data) { this.log('DEBUG', message, data); }
    info(message, data) { this.log('INFO', message, data); }
    warn(message, data) { this.log('WARN', message, data); }
    error(message, data) { this.log('ERROR', message, data); }

    getLogs() { return [...this.logs]; }
    clearLogs() { this.logs = []; }
  }

  /**
   * I18n Manager - Internationalization with fallback support
   */
  class I18nManager {
    constructor() {
      this.translations = {
        en: {
          copied_alert: "DeepL limit detected. Your source text has been copied to the clipboard.",
          confirm_reload: "Cookies have been cleared. Do you want to reload the page now to reset the limit?",
          error_clipboard: "Failed to copy text to clipboard.",
          error_cookie_clear: "Failed to clear cookies.",
          error_detection: "Error during limit detection.",
          info_initialized: "DeepL Unlimited script initialized successfully.",
          info_reset_triggered: "Reset process triggered.",
          warn_cooldown: "Reset is on cooldown. Please wait."
        },
        zh: {
          copied_alert: "检测到DeepL使用限制。您的原文已复制到剪贴板。",
          confirm_reload: "相关Cookie已被清除。是否立即刷新页面以重置额度?",
          error_clipboard: "复制文本到剪贴板失败。",
          error_cookie_clear: "清除Cookie失败。",
          error_detection: "限制检测过程中出错。",
          info_initialized: "DeepL无限制脚本初始化成功。",
          info_reset_triggered: "重置流程已触发。",
          warn_cooldown: "重置功能冷却中,请稍候。"
        }
      };
    }

    getLang() {
      const lang = navigator.language || navigator.userLanguage;
      return lang.startsWith('zh') ? 'zh' : 'en';
    }

    getString(key) {
      const lang = this.getLang();
      return this.translations[lang]?.[key] || this.translations.en[key] || key;
    }
  }

  /**
   * Action Handler - Manages reset operations with error handling and retry logic
   */
  class ActionHandler {
    constructor(config, logger, i18n) {
      this.config = config;
      this.logger = logger;
      this.i18n = i18n;
    }

    async deleteCookies() {
      try {
        const cookieNames = this.config.get('cookies.names');
        const domain = this.config.get('cookies.domain');

        cookieNames.forEach(name => {
          document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain};`;
        });

        this.logger.info('Cookies cleared successfully', { cookies: cookieNames });
        return true;
      } catch (error) {
        this.logger.error('Failed to clear cookies', error);
        return false;
      }
    }

    async copySourceText() {
      try {
        const selectors = this.config.get('detection.selectors.sourceText');
        let sourceTextArea = null;
        let usedSelector = null;

        // Try each selector until we find the source text area
        for (const selector of selectors) {
          sourceTextArea = document.querySelector(selector);
          if (sourceTextArea) {
            usedSelector = selector;
            break;
          }
        }

        if (!sourceTextArea) {
          this.logger.warn('Source text area not found with any selector', { selectors });
          return false;
        }

        this.logger.debug('Found source text area', { selector: usedSelector });

        // Try different methods to extract text
        let textToCopy = '';

        // Method 1: Check for paragraphs (common in contenteditable divs)
        const paragraphs = sourceTextArea.querySelectorAll('p');
        if (paragraphs.length > 0) {
          textToCopy = Array.from(paragraphs).map(p => p.innerText || p.textContent).join('\n');
        }

        // Method 2: Check direct text content
        if (!textToCopy.trim()) {
          textToCopy = sourceTextArea.innerText || sourceTextArea.textContent || sourceTextArea.value || '';
        }

        // Method 3: Check for spans or other text containers
        if (!textToCopy.trim()) {
          const textElements = sourceTextArea.querySelectorAll('span, div');
          textToCopy = Array.from(textElements)
            .map(el => el.innerText || el.textContent)
            .filter(text => text && text.trim())
            .join('\n');
        }

        if (!textToCopy.trim()) {
          this.logger.warn('No text found to copy', {
            element: sourceTextArea.tagName,
            className: sourceTextArea.className
          });
          return false;
        }

        if (typeof GM_setClipboard === 'function') {
          GM_setClipboard(textToCopy);
          this.logger.info('Text copied to clipboard', {
            length: textToCopy.length,
            selector: usedSelector
          });
          return true;
        } else {
          this.logger.error('GM_setClipboard not available');
          return false;
        }
      } catch (error) {
        this.logger.error('Failed to copy source text', error);
        return false;
      }
    }

    async triggerResetProcess() {
      // Check cooldown
      const cooldown = this.config.get('performance.resetCooldown');
      const timeSinceLastReset = Date.now() - AppState.lastResetTime;

      if (timeSinceLastReset < cooldown) {
        this.logger.warn('Reset on cooldown', {
          remaining: cooldown - timeSinceLastReset
        });
        return false;
      }

      if (AppState.isResetting) {
        this.logger.warn('Reset already in progress');
        return false;
      }

      try {
        AppState.isResetting = true;
        AppState.lastResetTime = Date.now();

        this.logger.info(this.i18n.getString('info_reset_triggered'));

        // Execute reset operations
        const cookiesCleared = await this.deleteCookies();
        const textCopied = await this.copySourceText();

        // Show user notification
        if (textCopied) {
          alert(this.i18n.getString('copied_alert'));
        } else {
          alert(this.i18n.getString('error_clipboard'));
        }

        // Ask for reload
        if (cookiesCleared && confirm(this.i18n.getString('confirm_reload'))) {
          window.location.reload();
        }

        return true;
      } catch (error) {
        this.logger.error('Reset process failed', error);
        AppState.errors.push(error);
        return false;
      } finally {
        AppState.isResetting = false;
      }
    }
  }

  /**
   * Limit Detector - Multiple detection strategies with performance optimization
   */
  class LimitDetector {
    constructor(config, logger) {
      this.config = config;
      this.logger = logger;
      this.cache = new Map();
      this.lastScanTime = 0;
    }

    checkForLimit(text) {
      if (!text) return false;
      const keywords = this.config.get('detection.keywords');
      return keywords.every(keyword => text.includes(keyword));
    }

    checkForPromotion(element) {
      if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;

      const text = element.innerText || element.textContent || '';
      if (!text.trim()) return false;

      // Text length validation
      const minLength = this.config.get('detection.minTextLength');
      const maxLength = this.config.get('detection.maxTextLength');
      if (text.length < minLength || text.length > maxLength) {
        return false;
      }

      // Cache check
      const cacheKey = text.substring(0, 100);
      if (this.cache.has(cacheKey)) {
        return this.cache.get(cacheKey);
      }

      // Check whitelist first
      const whitelist = this.config.get('detection.whitelist');
      const isWhitelisted = whitelist.some(pattern =>
        text.toLowerCase().includes(pattern.toLowerCase())
      );

      if (isWhitelisted) {
        this.cache.set(cacheKey, false);
        return false;
      }

      // Check promotion keywords (high confidence)
      const promotionKeywords = this.config.get('detection.promotionKeywords');
      const hasPromotionKeyword = promotionKeywords.some(keyword =>
        text.toLowerCase().includes(keyword.toLowerCase())
      );

      // Check strict keywords (require additional context validation)
      const strictKeywords = this.config.get('detection.strictKeywords');
      const hasStrictKeyword = strictKeywords.some(keyword =>
        text.toLowerCase().includes(keyword.toLowerCase())
      );

      let isPromotion = false;

      if (hasPromotionKeyword) {
        // High confidence detection
        isPromotion = true;
      } else if (hasStrictKeyword) {
        // Strict keyword requires additional validation
        isPromotion = this.validateStrictKeyword(element, text);
      }

      // Cache result
      this.cache.set(cacheKey, isPromotion);

      // Limit cache size
      if (this.cache.size > 100) {
        const firstKey = this.cache.keys().next().value;
        this.cache.delete(firstKey);
      }

      if (isPromotion) {
        this.logger.debug('Promotion detected', {
          text: text.substring(0, 100),
          element: element.tagName,
          className: element.className
        });
        return true;
      }

      return false;
    }

    validateStrictKeyword(element, text) {
      // Additional validation for strict keywords
      const textLower = text.toLowerCase();

      // Must have STRONG promotional indicators for strict keywords
      const strongPromotionalContext = [
        'trial', 'free trial', '免费试用', 'upgrade', 'premium',
        'character limit', 'monthly limit', 'unlimited', 'subscription',
        'nearing', 'limit reached', 'get more'
      ];

      const hasStrongPromotionalContext = strongPromotionalContext.some(context =>
        textLower.includes(context)
      );

      // Check element attributes for promotional indicators
      const elementClasses = element.className || '';
      const elementId = element.id || '';
      const promotionalClasses = ['promo', 'trial', 'upgrade', 'banner', 'cta', 'alert', 'warning'];

      const hasPromotionalClass = promotionalClasses.some(cls =>
        elementClasses.toLowerCase().includes(cls) ||
        elementId.toLowerCase().includes(cls)
      );

      // Check if element has promotional styling (blue background, etc.)
      let hasPromotionalStyling = false;
      try {
        const style = window.getComputedStyle(element);
        hasPromotionalStyling =
          style.backgroundColor.includes('blue') ||
          style.borderColor.includes('blue') ||
          elementClasses.includes('blue') ||
          elementClasses.includes('bg-blue');
      } catch (e) {
        // Ignore styling errors
      }

      // Check if text contains action words (call-to-action)
      const actionWords = ['click', 'try', 'get', 'upgrade', 'subscribe', '点击', '试用', '获取', '升级'];
      const hasActionWords = actionWords.some(word => textLower.includes(word));

      // Require multiple indicators for strict keywords
      const indicators = [hasStrongPromotionalContext, hasPromotionalClass, hasPromotionalStyling, hasActionWords];
      const indicatorCount = indicators.filter(Boolean).length;

      this.logger.debug('Strict keyword validation', {
        text: text.substring(0, 50),
        hasStrongPromotionalContext,
        hasPromotionalClass,
        hasPromotionalStyling,
        hasActionWords,
        indicatorCount,
        required: 2
      });

      // Require at least 2 indicators for strict keywords
      return indicatorCount >= 2;
    }

    checkElementAndChildren(element, depth = 0) {
      // Prevent infinite recursion
      if (depth > 10) return false;

      if (this.checkForPromotion(element)) {
        return true;
      }

      // Check children recursively
      if (element.children) {
        for (const child of element.children) {
          if (this.checkElementAndChildren(child, depth + 1)) {
            return true;
          }
        }
      }

      return false;
    }

    clearCache() {
      this.cache.clear();
    }
  }

  /**
   * DOM Watcher - Efficient DOM monitoring with debouncing and performance optimization
   */
  class DOMWatcher {
    constructor(config, logger, detector, actionHandler) {
      this.config = config;
      this.logger = logger;
      this.detector = detector;
      this.actionHandler = actionHandler;
      this.observer = null;
      this.debounceTimer = null;
      this.isWatching = false;
    }

    debounce(func, delay) {
      clearTimeout(this.debounceTimer);
      this.debounceTimer = setTimeout(func, delay);
    }

    handleMutation = (mutationsList) => {
      if (AppState.isResetting) return;

      const debounceDelay = this.config.get('performance.debounceDelay');
      this.debounce(() => {
        this.processMutations(mutationsList);
      }, debounceDelay);
    }

    processMutations(mutationsList) {
      try {
        for (const mutation of mutationsList) {
          if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
            for (const node of mutation.addedNodes) {
              if (node.nodeType === Node.ELEMENT_NODE) {
                // Check for original limit detection
                if (this.detector.checkForLimit(node.innerText)) {
                  this.logger.info('Limit detected via mutation');
                  this.actionHandler.triggerResetProcess();
                  return;
                }

                // Check for promotion elements
                if (this.detector.checkElementAndChildren(node)) {
                  this.logger.info('Promotion detected via mutation');
                  this.actionHandler.triggerResetProcess();
                  return;
                }
              }
            }
          }
        }
      } catch (error) {
        this.logger.error('Mutation processing error', error);
      }
    }

    startWatching() {
      if (this.isWatching) return;

      try {
        this.observer = new MutationObserver(this.handleMutation);
        this.observer.observe(document.body, {
          childList: true,
          subtree: true,
          attributes: false,
          characterData: false
        });
        this.isWatching = true;
        this.logger.info('DOM watching started');
      } catch (error) {
        this.logger.error('Failed to start DOM watching', error);
      }
    }

    stopWatching() {
      if (this.observer) {
        this.observer.disconnect();
        this.observer = null;
      }
      if (this.debounceTimer) {
        clearTimeout(this.debounceTimer);
        this.debounceTimer = null;
      }
      this.isWatching = false;
      this.logger.info('DOM watching stopped');
    }

    isLikelyPromotionalArea(element, selector) {
      // Check element classes and IDs for promotional indicators
      const elementClasses = element.className || '';
      const elementId = element.id || '';
      const promotionalIndicators = [
        'banner', 'promo', 'trial', 'upgrade', 'cta', 'alert', 'warning',
        'notification', 'popup', 'modal', 'overlay', 'toast'
      ];

      const hasPromotionalClass = promotionalIndicators.some(indicator =>
        elementClasses.toLowerCase().includes(indicator) ||
        elementId.toLowerCase().includes(indicator)
      );

      // Check if element has promotional styling
      let hasPromotionalStyling = false;
      try {
        const style = window.getComputedStyle(element);
        const bgColor = style.backgroundColor;
        const borderColor = style.borderColor;

        // Look for typical promotional colors (blue, orange, red, yellow)
        hasPromotionalStyling =
          bgColor.includes('blue') || bgColor.includes('orange') ||
          bgColor.includes('red') || bgColor.includes('yellow') ||
          borderColor.includes('blue') || borderColor.includes('orange') ||
          elementClasses.includes('blue') || elementClasses.includes('bg-blue');
      } catch (e) {
        // Ignore styling errors
      }

      // Check text content for promotional language
      const text = element.innerText || element.textContent || '';
      const promotionalPhrases = [
        'trial', 'free', 'upgrade', 'premium', 'pro', 'limit', 'unlimited',
        'subscribe', 'get more', 'try now', '试用', '免费', '升级', '限制'
      ];

      const hasPromotionalText = promotionalPhrases.some(phrase =>
        text.toLowerCase().includes(phrase)
      );

      // Check position (promotional content is often at top or in sidebars)
      const rect = element.getBoundingClientRect();
      const isAtTop = rect.top < 200;
      const isSmallHeight = rect.height < 150;

      // Combine indicators
      const indicators = [hasPromotionalClass, hasPromotionalStyling, hasPromotionalText];
      const indicatorCount = indicators.filter(Boolean).length;

      this.logger.debug('Promotional area check', {
        selector,
        hasPromotionalClass,
        hasPromotionalStyling,
        hasPromotionalText,
        isAtTop,
        isSmallHeight,
        indicatorCount,
        text: text.substring(0, 50)
      });

      // Require at least one strong indicator
      return indicatorCount >= 1 && (isAtTop || isSmallHeight);
    }

    performInitialScan() {
      try {
        this.logger.info('Performing initial scan');

        // Check if page is ready for scanning
        const selectors = this.config.get('detection.selectors.sourceText');
        let sourceTextArea = null;

        for (const selector of selectors) {
          sourceTextArea = document.querySelector(selector);
          if (sourceTextArea) break;
        }

        if (!sourceTextArea) {
          this.logger.info('Page not ready for scanning - source text area not found, will retry');
          // Retry after a short delay
          setTimeout(() => this.performInitialScan(), 2000);
          return false;
        }

        this.logger.debug('Page ready for scanning - source text area found');

        // Check promotion areas first (most specific)
        const promotionSelectors = this.config.get('detection.selectors.promotionAreas');
        for (const selector of promotionSelectors) {
          try {
            const elements = document.querySelectorAll(selector);
            for (const element of elements) {
              if (this.detector.checkForPromotion(element)) {
                this.logger.info('Promotion found during initial scan', {
                  selector,
                  text: element.innerText?.substring(0, 100)
                });
                this.actionHandler.triggerResetProcess();
                return true;
              }
            }
          } catch (e) {
            this.logger.debug('Selector error during initial scan', { selector, error: e.message });
          }
        }

        // Check common areas with more careful validation
        const commonAreas = this.config.get('detection.selectors.commonAreas');
        for (const areaSelector of commonAreas) {
          try {
            const areas = document.querySelectorAll(areaSelector);
            for (const area of areas) {
              // Skip if area is too large (likely not a promotion)
              if (area.children && area.children.length > 20) {
                this.logger.debug('Skipping large area', { selector: areaSelector, childCount: area.children.length });
                continue;
              }

              // Skip if area contains too much text (likely main content)
              const areaText = area.innerText || area.textContent || '';
              if (areaText.length > 300) {
                this.logger.debug('Skipping large text area', {
                  selector: areaSelector,
                  textLength: areaText.length,
                  preview: areaText.substring(0, 50)
                });
                continue;
              }

              // Only check areas that look like promotional containers
              const isLikelyPromotional = this.isLikelyPromotionalArea(area, areaSelector);
              if (!isLikelyPromotional) {
                this.logger.debug('Skipping non-promotional area', {
                  selector: areaSelector,
                  preview: areaText.substring(0, 50)
                });
                continue;
              }

              if (this.detector.checkElementAndChildren(area)) {
                this.logger.info('Promotion found in common area during initial scan', {
                  selector: areaSelector,
                  text: area.innerText?.substring(0, 100)
                });
                this.actionHandler.triggerResetProcess();
                return true;
              }
            }
          } catch (e) {
            this.logger.debug('Common area selector error during initial scan', { selector: areaSelector, error: e.message });
          }
        }

        this.logger.info('Initial scan completed - no promotions found');
        return false;
      } catch (error) {
        this.logger.error('Initial scan error', error);
        return false;
      }
    }
  }

  /**
   * Main Application Class - Orchestrates all components
   */
  class DeepLUnlimitedApp {
    constructor() {
      this.config = null;
      this.logger = null;
      this.i18n = null;
      this.detector = null;
      this.actionHandler = null;
      this.domWatcher = null;
      this.initialized = false;
    }

    async initialize() {
      try {
        // Initialize core components
        this.config = new ConfigManager();

        if (!this.config.validate()) {
          throw new Error('Configuration validation failed');
        }

        this.logger = new Logger(this.config);
        this.i18n = new I18nManager();
        this.detector = new LimitDetector(this.config, this.logger);
        this.actionHandler = new ActionHandler(this.config, this.logger, this.i18n);
        this.domWatcher = new DOMWatcher(this.config, this.logger, this.detector, this.actionHandler);

        // Set global error handler
        window.addEventListener('error', (event) => {
          this.logger.error('Global error caught', {
            message: event.message,
            filename: event.filename,
            lineno: event.lineno,
            colno: event.colno
          });
        });

        this.initialized = true;
        this.logger.info(this.i18n.getString('info_initialized'));

        return true;
      } catch (error) {
        console.error('[DeepL Unlimited] Initialization failed: - b.js:859', error);
        AppState.errors.push(error);
        return false;
      }
    }

    async start() {
      if (!this.initialized) {
        const initSuccess = await this.initialize();
        if (!initSuccess) {
          return false;
        }
      }

      try {
        AppState.isInitialized = true;

        // Perform initial scan
        const initialScanDelay = this.config.get('performance.initialScanDelay');
        setTimeout(() => {
          const foundPromotion = this.domWatcher.performInitialScan();

          // Start watching if no promotion found
          if (!foundPromotion) {
            this.domWatcher.startWatching();
          }
        }, initialScanDelay);

        this.logger.info('Application started successfully');
        return true;
      } catch (error) {
        this.logger.error('Failed to start application', error);
        return false;
      }
    }

    stop() {
      try {
        if (this.domWatcher) {
          this.domWatcher.stopWatching();
        }

        if (this.detector) {
          this.detector.clearCache();
        }

        AppState.isInitialized = false;
        this.logger.info('Application stopped');
      } catch (error) {
        this.logger.error('Error during application stop', error);
      }
    }

    // Public API for debugging
    getStatus() {
      return {
        initialized: this.initialized,
        appState: { ...AppState },
        logs: this.logger ? this.logger.getLogs() : [],
        config: this.config ? this.config.config : null
      };
    }

    // Manual trigger for testing
    triggerReset() {
      if (this.actionHandler) {
        return this.actionHandler.triggerResetProcess();
      }
      return false;
    }

    // Debug helper to find source text area
    findSourceTextArea() {
      const selectors = this.config.get('detection.selectors.sourceText');
      const results = [];

      for (const selector of selectors) {
        const elements = document.querySelectorAll(selector);
        results.push({
          selector,
          found: elements.length,
          elements: Array.from(elements).map(el => ({
            tagName: el.tagName,
            className: el.className,
            id: el.id,
            hasText: !!(el.innerText || el.textContent || el.value),
            textLength: (el.innerText || el.textContent || el.value || '').length
          }))
        });
      }

      console.log('Source text area search results: - b.js:950', results);
      return results;
    }

    // Debug helper to scan for all potential text areas
    scanAllTextAreas() {
      const textAreas = document.querySelectorAll('textarea, [contenteditable="true"], d-textarea, [role="textbox"]');
      const results = Array.from(textAreas).map(el => ({
        tagName: el.tagName,
        className: el.className,
        id: el.id,
        ariaLabel: el.getAttribute('aria-label'),
        ariaLabelledBy: el.getAttribute('aria-labelledby'),
        role: el.getAttribute('role'),
        hasText: !!(el.innerText || el.textContent || el.value),
        textLength: (el.innerText || el.textContent || el.value || '').length
      }));

      console.log('All text areas found: - b.js:968', results);
      return results;
    }
  }

  // Initialize and start the application
  const app = new DeepLUnlimitedApp();

  // Start when DOM is ready
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => app.start());
  } else {
    app.start();
  }

  // Expose app instance for debugging (only in development)
  if (typeof window !== 'undefined') {
    window.DeepLUnlimited = app;
  }

})();