287 lines
20 KiB
JavaScript
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);
|