Files

287 lines
20 KiB
JavaScript

(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 <code>https</code>, always <code>http</code>, 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', '<code>\\d+</code> means "one or more digits", and <code>\\d*</code> allows no digit. Enable the no-number variant only if a host like <code>mirror.example.com</code> 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 <code>mirror1.example.com</code>, without <code>https://</code>, 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 <code>PAGE_OK</code>, and the extension remembers that host. <strong>"Working host cache, hours" = 0</strong> 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, <code>BLOCKED_PAGE_DETECTED</code> 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)', '<strong>Export settings</strong> 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)', '<strong>Import settings</strong> 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 <code>chrome://extensions</code>.');
setHtml('.troubleshooting li:nth-child(2)', 'After any changes, click <strong>"Apply to rule"</strong>, then <strong>"Save"</strong>. 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 <code>hostRegex</code> matches the host exactly, for example <code>mirror1.example.com</code>. 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 <strong>"To"</strong> field. <strong>"Max redirects"</strong> 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 <code>Host Error</code>, <code>Error 522</code>, <code>Origin is unreachable</code>, <code>Cloudflare Ray ID</code>, or similar text and the extension does not switch, add the exact phrase from that page to <strong>"Blocked/error page markers"</strong>.');
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 <strong>"Diagnostic log"</strong> in these settings, click <strong>"Refresh log"</strong>, and export JSON. You can also open the extension DevTools at <code>chrome://extensions</code> and watch <code>[Auto Mirror Switcher]</code> 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: <code>mirror10.example.com</code>.');
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 <code>{n}</code>; for letters/tokens use <code>{s}</code>. Examples: <code>mirror{n}.example.com</code>, <code>{n}.example.com</code>, <code>node-{s}.example.com</code>.');
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: <code id="includeEmptyHostExample">example.com</code> for template <code id="includeEmptyTemplateExample">example{n}.com</code>');
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 <code id="cachedFromExample">example1.com/page.html</code> go directly to <code id="cachedToExample">example2.com/page.html</code>.');
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, <strong>probe</strong> 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);