(function (root) { 'use strict'; var EN = { saveFixRule: 'Fix the error in the current rule before saving.', saved: 'Saved.', autoSaveOn: 'Autosave enabled.', autoSaveOff: 'Autosave disabled.', noRules: 'No rules yet.', hostListLabel: 'host list', untitledRule: 'Untitled rule', ruleAppliedAuto: 'Rule applied and will be saved automatically.', ruleAppliedManual: 'Rule applied. Do not forget to click "Save".', ruleAddedAuto: 'Rule added and will be saved automatically.', ruleAddedManual: 'Rule added. Do not forget to click "Save".', cacheCleared: 'Last working host cache cleared for rule "{name}".', confirmDeleteRule: 'Delete rule "{name}"?', ruleDeletedAuto: 'Rule deleted and will be saved automatically.', ruleDeletedManual: 'Rule deleted. Do not forget to click "Save".', importNoRulesArray: 'No rules array was found in the file.', importRuleError: 'Error in rule #{index} "{name}": {error}', importNoRules: 'The imported file does not contain any rules.', exportCanceled: 'Export canceled: fix the error in the current rule first.', settingsExported: 'Settings exported to a JSON file.', importInvalidObject: 'File must contain a JSON object with a rules field.', importWrongAppConfirm: 'The file does not look like an Auto Mirror Switcher export. Try importing anyway?', importCanceled: 'Import canceled.', importedSavedCount: 'Imported and saved rules: {count}.', importReplaceConfirm: 'Import will replace current rules ({current}) with rules from the file ({incoming}). Continue?', importError: 'Import error: {message}', importReadError: 'Import error: could not read the file.', logEmpty: 'Log is empty.', logUpdated: 'Log updated. Events: {count}.', logExported: 'Log exported. Events: {count}.', confirmClearLog: 'Clear the diagnostic log?', logCleared: 'Diagnostic log cleared.', logCopied: 'Log copied.', logCopyManual: 'Could not copy the log automatically. Select the text manually.', validationHostRegexRequired: 'The hostRegex field cannot be empty.', validationHostRegexInvalid: 'Invalid RegExp in hostRegex: {detail}', validationNumberTemplateMissing: 'For number strategy, the host template must contain {n}.', validationLettersTemplateMissing: 'For letter strategy, the host template must contain {s}.', validationListHostsRequired: 'For list mode, enter at least one host.' }; function browserLanguage() { try { if (root.chrome && chrome.i18n && chrome.i18n.getUILanguage) { return chrome.i18n.getUILanguage(); } } catch (e) { // Fall back to navigator below. } return root.navigator && (navigator.language || navigator.userLanguage) || 'en'; } function detectLanguage() { var language = String(browserLanguage() || 'en').toLowerCase(); return /^ru(?:-|$)/.test(language) ? 'ru' : 'en'; } var language = detectLanguage(); function format(text, vars) { vars = vars || {}; return String(text == null ? '' : text).replace(/\{([a-zA-Z0-9_]+)\}/g, function (match, key) { return Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match; }); } function t(key, fallback, vars) { var text = language === 'en' && EN[key] ? EN[key] : fallback; return format(text == null ? key : text, vars); } function validationError(error, fallback) { if (!error) return null; if (language !== 'en') return fallback || error.message || ''; var key = { hostRegexRequired: 'validationHostRegexRequired', hostRegexInvalid: 'validationHostRegexInvalid', numberTemplateMissing: 'validationNumberTemplateMissing', lettersTemplateMissing: 'validationLettersTemplateMissing', listHostsRequired: 'validationListHostsRequired' }[error.code]; return t(key || '', fallback || error.message || '', { detail: error.detail || '' }); } function $(selector) { return document.querySelector(selector); } function setText(selector, value) { var element = $(selector); if (element) element.textContent = value; } function setHtml(selector, value) { var element = $(selector); if (element) element.innerHTML = value; } function setAttr(selector, attr, value) { var element = $(selector); if (element) element.setAttribute(attr, value); } function setLabelPrefix(controlSelector, value) { var control = $(controlSelector); var label = control && control.closest ? control.closest('label') : null; if (!label) return; Array.prototype.some.call(label.childNodes, function (node) { if (node.nodeType !== Node.TEXT_NODE || !node.nodeValue.trim()) return false; node.nodeValue = node.nodeValue.replace(node.nodeValue.trim(), value); return true; }); } function applyOptionsEnglish() { document.title = 'Auto Mirror Switcher'; setText('.header p', 'Automatically switches failed mirror domains while preserving the full page path.'); setText('.header .switch-card span', 'Extension enabled'); setText('#addRuleBtn', 'Add rule'); setText('#addMultclubBtn', 'Add Multclub preset'); setText('#exportSettingsBtn', 'Export settings'); setText('#importSettingsBtn', 'Import settings'); setText('.autosave-toggle span', 'Autosave'); setText('#saveBtn', 'Save'); setText('#guideSection > summary h2', 'How to use'); setText('#guideSection > summary p', 'Step-by-step rule setup and when the extension switches mirrors.'); setText('.save-reminder', 'Important: click "Save" after changes'); setText('.steps .step:nth-child(1) strong', 'Enable the extension and rule'); setText('.steps .step:nth-child(1) p', 'The global switch at the top enables page handling. Each rule has its own checkbox in the editor; disabled rules are ignored during mirror search.'); setText('.steps .step:nth-child(2) strong', 'Describe source addresses'); setText('.steps .step:nth-child(2) p', 'In the RegExp field, specify which hosts should be handled. Only the host is checked, without protocol, path, query, or hash, for example mirror1.example.com.'); setText('.steps .step:nth-child(3) strong', 'Configure candidates'); setText('.steps .step:nth-child(3) p', 'Choose a strategy: numbers in a template, letter tokens, or a fixed host list. The new address always uses the selected host plus the original page path.'); setText('.steps .step:nth-child(4) strong', 'Save and test'); setText('.steps .step:nth-child(4) p', 'After saving, open a page. The extension reacts to network errors, HTTP 5xx/403/451, or a configured blocked-page marker in the page text.'); setText('#guideSection .section-body > details:nth-of-type(1) > summary', 'Example with a sample domain'); setText('#guideSection .section-body > details:nth-of-type(1) .details-body p:nth-of-type(1)', 'If this page is open:'); setText('#guideSection .section-body > details:nth-of-type(1) .details-body p:nth-of-type(2)', 'and the current host is considered broken, the extension picks the next host by the rule and opens the same path:'); setHtml('#guideSection .section-body > details:nth-of-type(1) .details-body p:nth-of-type(3)', 'The protocol comes from the rule setting: always https, always http, or the same as the source URL. Path, query, and hash are preserved unchanged.'); setText('#guideSection .section-body > details:nth-of-type(2) > summary', 'Example rules for example.com'); setText('#guideSection .guide-columns > div:nth-child(1) h3', 'Numbers in a subdomain'); setText('#guideSection .guide-columns > div:nth-child(2) h3', 'Numbers before the domain'); setText('#guideSection .guide-columns > div:nth-child(3) h3', 'Variant without a number'); setHtml('#guideSection .guide-columns > div:nth-child(3) p', '\\d+ means "one or more digits", and \\d* allows no digit. Enable the no-number variant only if a host like mirror.example.com should also be a candidate.'); setText('#guideSection .guide-columns > div:nth-child(4) h3', 'Letters/tokens'); setText('#guideSection .guide-columns > div:nth-child(5) h3', 'Fixed list'); setText('#guideSection .section-body > details:nth-of-type(3) > summary', 'What the main fields mean'); setText('.field-help dt:nth-of-type(1)', 'Which hosts to handle, RegExp'); setHtml('.field-help dd:nth-of-type(1)', 'Host filter where the extension should activate in template-based modes. Only the host is checked, for example mirror1.example.com, without https://, path, or parameters. In fixed-list mode this field is hidden: the rule activates on hosts from the list.'); setText('.field-help dt:nth-of-type(2)', 'step mode'); setText('.field-help dd:nth-of-type(2)', 'On error, immediately picks the next candidate from the list and opens it in the current tab. Candidates are built from the rule, and already tried hosts are skipped.'); setText('.field-help dt:nth-of-type(3)', 'probe mode'); setText('.field-help dd:nth-of-type(3)', 'Checks candidates with background requests first, then opens the first host that responds with 2xx/3xx and no blocked-page markers in the text. The number of simultaneous checks is set by "Parallel checks".'); setText('.field-help dt:nth-of-type(4)', 'Last working host'); setHtml('.field-help dd:nth-of-type(4)', 'When a page loads without blocked-page markers, the content script sends PAGE_OK, and the extension remembers that host. "Working host cache, hours" = 0 means "do not expire by time": the saved host is used until it itself returns an error or blocked-page marker.'); setText('.field-help dt:nth-of-type(5)', 'Blocked/error page markers'); setHtml('.field-help dd:nth-of-type(5)', 'After load, the content script checks the title and page text several times. If a marker is found, BLOCKED_PAGE_DETECTED is sent, the current host is considered bad, its cache is cleared, and the extension looks for the next option. Add exact phrases from the error page here, one per line.'); setText('.field-help dt:nth-of-type(6)', 'Maximum redirects'); setText('.field-help dd:nth-of-type(6)', 'This protects against an infinite search loop. The code automatically raises the effective limit to the candidate list size plus a reserve, but a manual value is still useful for large lists.'); setText('#guideSection .section-body > details:nth-of-type(4) > summary', 'Export and import settings'); setHtml('#guideSection .section-body > details:nth-of-type(4) .details-body p:nth-of-type(1)', 'Export settings downloads a JSON file with all rules and the global extension switch. Keep this file separately to quickly restore the configuration after reinstalling Chrome or moving the extension.'); setHtml('#guideSection .section-body > details:nth-of-type(4) .details-body p:nth-of-type(2)', 'Import settings loads that JSON file back and replaces the current rules. After a successful import, settings are saved in the extension immediately.'); setText('#guideSection .section-body > details:nth-of-type(4) .details-body p:nth-of-type(3)', 'The last working host cache is not exported: it depends on the current Chrome profile and updates automatically when pages are actually opened.'); setText('#guideSection .section-body > details:nth-of-type(5) > summary', 'If it did not work'); setHtml('.troubleshooting li:nth-child(1)', 'Check that the extension is enabled at the top of this page and on chrome://extensions.'); setHtml('.troubleshooting li:nth-child(2)', 'After any changes, click "Apply to rule", then "Save". If autosave is enabled, the save button is disabled and changes are written automatically.'); setHtml('.troubleshooting li:nth-child(3)', 'For template-based modes, make sure hostRegex matches the host exactly, for example mirror1.example.com. In fixed-list mode, check that the current host is in the list.'); setHtml('.troubleshooting li:nth-child(4)', 'If the working number is outside the range, increase the "To" field. "Max redirects" automatically expands to the range size, but it can still be raised manually for large lists.'); setHtml('.troubleshooting li:nth-child(5)', 'If an error page opens with Host Error, Error 522, Origin is unreachable, Cloudflare Ray ID, or similar text and the extension does not switch, add the exact phrase from that page to "Blocked/error page markers".'); setHtml('.troubleshooting li:nth-child(6)', 'If the extension keeps sending you to an old saved host, click "Clear rule cache" or open the problematic page directly; on DNS/network error, HTTP 5xx/403/451, or a found blocked marker, the cache will be cleared automatically.'); setHtml('.troubleshooting li:nth-child(7)', 'For diagnostics, open "Diagnostic log" in these settings, click "Refresh log", and export JSON. You can also open the extension DevTools at chrome://extensions and watch [Auto Mirror Switcher] logs.'); setText('aside.card h2', 'Rules'); setText('.editor-card h2', 'Editor'); setText('#ruleForm > label.checkline span', 'Rule enabled'); setLabelPrefix('#name', 'Name'); setAttr('#name', 'placeholder', 'Example: Example mirrors'); setLabelPrefix('#hostRegex', 'Which hosts to handle, RegExp'); setHtml('#hostRegex + small', 'Only the host is checked, without protocol and path. Example: mirror10.example.com.'); setLabelPrefix('#protocol', 'Protocol for the new address'); setText('#protocol option[value="same"]', 'same as source URL'); setLabelPrefix('#mode', 'Mode on DNS/network error'); setText('#mode option[value="step"]', 'step: open the next option'); setText('#mode option[value="probe"]', 'probe: check options in the background first'); setLabelPrefix('#strategy', 'Search strategy'); setText('#strategy option[value="number"]', 'Numbers in the domain'); setText('#strategy option[value="letters"]', 'Letters/tokens in the domain'); setText('#strategy option[value="list"]', 'Fixed host list'); setLabelPrefix('#template', 'Host template'); setHtml('#template + small', 'For numbers use {n}; for letters/tokens use {s}. Examples: mirror{n}.example.com, {n}.example.com, node-{s}.example.com.'); setText('#numberBlock legend', 'Number search'); setLabelPrefix('#numberStart', 'From'); setLabelPrefix('#numberEnd', 'To'); setLabelPrefix('#numberStep', 'Step'); setLabelPrefix('#padLength', 'Left zeroes'); setHtml('#includeEmpty + span', 'Try the variant without a number: example.com for template example{n}.com'); setText('#lettersBlock legend', 'Letter/token search'); setLabelPrefix('#letterCharset', 'Character set'); setLabelPrefix('#letterMinLen', 'Min. length'); setLabelPrefix('#letterMaxLen', 'Max. length'); setLabelPrefix('#maxCandidates', 'Candidate limit'); setText('#listBlock legend', 'Host list'); setLabelPrefix('#customHosts', 'Hosts separated by newline, space, or comma'); setText('#ruleForm fieldset:nth-of-type(4) legend', 'Timing and loop protection'); setLabelPrefix('#timeoutMs', 'Probe timeout, ms'); setLabelPrefix('#parallelChecks', 'Parallel checks'); setLabelPrefix('#maxRedirects', 'Max redirects'); setLabelPrefix('#cacheTtlHours', 'Working host cache, hours'); setText('#cacheTtlHours + small', '0 = do not expire by time; cache is reset only if the host fails.'); setHtml('#preferCachedHost + span', 'Immediately open the last working host if it is already known. For example, from example1.com/page.html go directly to example2.com/page.html.'); setLabelPrefix('#blockedMarkers', 'Blocked/error page markers'); setText('#blockedMarkers + small', 'If the loaded page contains one of these strings, the extension treats the mirror as broken and continues switching.'); setText('#applyRuleBtn', 'Apply to rule'); setText('#duplicateRuleBtn', 'Duplicate'); setText('#clearRuleCacheBtn', 'Clear rule cache'); setText('#deleteRuleBtn', 'Delete'); setText('.log-title', 'Diagnostic log'); setText('.log-description', 'If the extension goes to the wrong place, refresh the log and export JSON or copy text from the box.'); setText('#debugLogSection .guide-badge', 'Keeps up to the last 1200 events'); setText('#refreshLogBtn', 'Refresh log'); setText('#copyLogBtn', 'Copy log'); setText('#exportLogBtn', 'Export log'); setText('#clearLogBtn', 'Clear log'); setAttr('#debugLogOutput', 'placeholder', 'Redirect, cache, Cloudflare marker, and candidate selection events will appear here.'); setText('.log-body small', 'The log can contain URLs of pages opened through extension rules. Check it for sensitive data before publishing.'); } function applyStoppedEnglish() { document.title = 'Auto Mirror Switcher - search stopped'; setText('h1', 'Working mirror not found'); setText('.lead', 'The extension stopped searching to avoid an infinite redirect loop.'); setText('.grid > div:nth-child(1) label', 'Rule'); setText('.grid > div:nth-child(2) label', 'Stop reason'); setText('.grid > div:nth-child(3) label', 'Source URL'); setText('.grid > div:nth-child(4) label', 'Page path'); setText('.grid > div:nth-child(5) label', 'Redirects'); setText('.grid > div:nth-child(6) label', 'Range'); setText('section:not(.grid) label', 'Checked hosts'); setText('#copy', 'Copy diagnostics'); setText('#openOptions', 'Open settings'); setHtml('.hint', 'If every host shows Cloudflare 522/523/524, the extension will try them until the end of the range. For these sites, probe mode is more convenient: it checks candidates in the background and opens only the found working host.'); } function applyPage() { if (!document || !document.documentElement) return; document.documentElement.lang = language; if (language !== 'en') return; var path = String(location && location.pathname || ''); if (path.indexOf('options.html') !== -1) { applyOptionsEnglish(); } else if (path.indexOf('stopped.html') !== -1) { applyStoppedEnglish(); } } root.AutoMirrorI18n = { language: language, t: t, validationError: validationError, applyPage: applyPage }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', applyPage); } else { applyPage(); } })(typeof globalThis !== 'undefined' ? globalThis : this);