// ==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;
}
})();