Google 搜尋語言過濾器

根據可配置語言,自動為 Google 搜尋結果套用 lr=lang_* 語言過濾。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Google Search Language Filter
// @name:zh-CN   Google 搜索语言过滤器
// @name:zh-TW   Google 搜尋語言過濾器
// @name:ja      Google 検索言語フィルター
// @name:es      Filtro de idioma de búsqueda de Google
// @name:fa      فیلتر زبان جستجوی گوگل
// @name:ru      Фильтр языка поиска Google
// @namespace    google-search-lang-script
// @version      0.1.1
// @description  Automatically apply lr=lang_* language filters to Google search results based on configurable languages.
// @description:zh-CN 根据可配置语言,自动为 Google 搜索结果应用 lr=lang_* 语言过滤。
// @description:zh-TW 根據可配置語言,自動為 Google 搜尋結果套用 lr=lang_* 語言過濾。
// @description:ja    設定した言語に基づき、Google 検索結果に lr=lang_* の言語フィルターを自動適用します。
// @description:es    Aplica automáticamente filtros de idioma lr=lang_* a los resultados de búsqueda de Google según los idiomas configurados。
// @description:fa    بر اساس زبان‌های تنظیم‌شده، فیلترهای زبانی lr=lang_* را به‌طور خودکار روی نتایج جستجوی گوگل اعمال می‌کند.
// @description:ru    Автоматически применяет языковые фильтры lr=lang_* к результатам поиска Google на основе выбранных языков.
// @homepageURL  https://github.com/tiaot33/google-search-lang-script
// @supportURL   https://github.com/tiaot33/google-search-lang-script/issues
// @run-at       document-start
// @noframes
// @author       tiaot33
// @match              *://*.google.com/search*
// @match              *://*.google.ad/search*
// @match              *://*.google.ae/search*
// @match              *://*.google.com.af/search*
// @match              *://*.google.com.ag/search*
// @match              *://*.google.com.ai/search*
// @match              *://*.google.al/search*
// @match              *://*.google.am/search*
// @match              *://*.google.co.ao/search*
// @match              *://*.google.com.ar/search*
// @match              *://*.google.as/search*
// @match              *://*.google.at/search*
// @match              *://*.google.com.au/search*
// @match              *://*.google.az/search*
// @match              *://*.google.ba/search*
// @match              *://*.google.com.bd/search*
// @match              *://*.google.be/search*
// @match              *://*.google.bf/search*
// @match              *://*.google.bg/search*
// @match              *://*.google.com.bh/search*
// @match              *://*.google.bi/search*
// @match              *://*.google.bj/search*
// @match              *://*.google.com.bn/search*
// @match              *://*.google.com.bo/search*
// @match              *://*.google.com.br/search*
// @match              *://*.google.bs/search*
// @match              *://*.google.bt/search*
// @match              *://*.google.co.bw/search*
// @match              *://*.google.by/search*
// @match              *://*.google.com.bz/search*
// @match              *://*.google.ca/search*
// @match              *://*.google.cd/search*
// @match              *://*.google.cf/search*
// @match              *://*.google.cg/search*
// @match              *://*.google.ch/search*
// @match              *://*.google.ci/search*
// @match              *://*.google.co.ck/search*
// @match              *://*.google.cl/search*
// @match              *://*.google.cm/search*
// @match              *://*.google.cn/search*
// @match              *://*.google.com.co/search*
// @match              *://*.google.co.cr/search*
// @match              *://*.google.com.cu/search*
// @match              *://*.google.cv/search*
// @match              *://*.google.com.cy/search*
// @match              *://*.google.cz/search*
// @match              *://*.google.de/search*
// @match              *://*.google.dj/search*
// @match              *://*.google.dk/search*
// @match              *://*.google.dm/search*
// @match              *://*.google.com.do/search*
// @match              *://*.google.dz/search*
// @match              *://*.google.com.ec/search*
// @match              *://*.google.ee/search*
// @match              *://*.google.com.eg/search*
// @match              *://*.google.es/search*
// @match              *://*.google.com.et/search*
// @match              *://*.google.fi/search*
// @match              *://*.google.com.fj/search*
// @match              *://*.google.fm/search*
// @match              *://*.google.fr/search*
// @match              *://*.google.ga/search*
// @match              *://*.google.ge/search*
// @match              *://*.google.gg/search*
// @match              *://*.google.com.gh/search*
// @match              *://*.google.com.gi/search*
// @match              *://*.google.gl/search*
// @match              *://*.google.gm/search*
// @match              *://*.google.gr/search*
// @match              *://*.google.com.gt/search*
// @match              *://*.google.gy/search*
// @match              *://*.google.hk/search*
// @match              *://*.google.com.hk/search*
// @match              *://*.google.hn/search*
// @match              *://*.google.hr/search*
// @match              *://*.google.ht/search*
// @match              *://*.google.hu/search*
// @match              *://*.google.co.id/search*
// @match              *://*.google.ie/search*
// @match              *://*.google.co.il/search*
// @match              *://*.google.im/search*
// @match              *://*.google.co.in/search*
// @match              *://*.google.iq/search*
// @match              *://*.google.is/search*
// @match              *://*.google.it/search*
// @match              *://*.google.je/search*
// @match              *://*.google.com.jm/search*
// @match              *://*.google.jo/search*
// @match              *://*.google.jp/search*
// @match              *://*.google.co.jp/search*
// @match              *://*.google.co.ke/search*
// @match              *://*.google.com.kh/search*
// @match              *://*.google.ki/search*
// @match              *://*.google.kg/search*
// @match              *://*.google.co.kr/search*
// @match              *://*.google.com.kw/search*
// @match              *://*.google.kz/search*
// @match              *://*.google.la/search*
// @match              *://*.google.com.lb/search*
// @match              *://*.google.li/search*
// @match              *://*.google.lk/search*
// @match              *://*.google.co.ls/search*
// @match              *://*.google.lt/search*
// @match              *://*.google.lu/search*
// @match              *://*.google.lv/search*
// @match              *://*.google.com.ly/search*
// @match              *://*.google.co.ma/search*
// @match              *://*.google.md/search*
// @match              *://*.google.me/search*
// @match              *://*.google.mg/search*
// @match              *://*.google.mk/search*
// @match              *://*.google.ml/search*
// @match              *://*.google.com.mm/search*
// @match              *://*.google.mn/search*
// @match              *://*.google.ms/search*
// @match              *://*.google.com.mt/search*
// @match              *://*.google.mu/search*
// @match              *://*.google.mv/search*
// @match              *://*.google.mw/search*
// @match              *://*.google.com.mx/search*
// @match              *://*.google.com.my/search*
// @match              *://*.google.co.mz/search*
// @match              *://*.google.com.na/search*
// @match              *://*.google.com.ng/search*
// @match              *://*.google.com.ni/search*
// @match              *://*.google.ne/search*
// @match              *://*.google.nl/search*
// @match              *://*.google.no/search*
// @match              *://*.google.com.np/search*
// @match              *://*.google.nr/search*
// @match              *://*.google.nu/search*
// @match              *://*.google.co.nz/search*
// @match              *://*.google.com.om/search*
// @match              *://*.google.com.pa/search*
// @match              *://*.google.com.pe/search*
// @match              *://*.google.com.pg/search*
// @match              *://*.google.com.ph/search*
// @match              *://*.google.com.pk/search*
// @match              *://*.google.pl/search*
// @match              *://*.google.pn/search*
// @match              *://*.google.com.pr/search*
// @match              *://*.google.ps/search*
// @match              *://*.google.pt/search*
// @match              *://*.google.com.py/search*
// @match              *://*.google.com.qa/search*
// @match              *://*.google.ro/search*
// @match              *://*.google.ru/search*
// @match              *://*.google.rw/search*
// @match              *://*.google.com.sa/search*
// @match              *://*.google.com.sb/search*
// @match              *://*.google.sc/search*
// @match              *://*.google.se/search*
// @match              *://*.google.com.sg/search*
// @match              *://*.google.sh/search*
// @match              *://*.google.si/search*
// @match              *://*.google.sk/search*
// @match              *://*.google.com.sl/search*
// @match              *://*.google.sn/search*
// @match              *://*.google.so/search*
// @match              *://*.google.sm/search*
// @match              *://*.google.sr/search*
// @match              *://*.google.st/search*
// @match              *://*.google.com.sv/search*
// @match              *://*.google.td/search*
// @match              *://*.google.tg/search*
// @match              *://*.google.co.th/search*
// @match              *://*.google.com.tj/search*
// @match              *://*.google.tl/search*
// @match              *://*.google.tm/search*
// @match              *://*.google.tn/search*
// @match              *://*.google.to/search*
// @match              *://*.google.com.tr/search*
// @match              *://*.google.tt/search*
// @match              *://*.google.com.tw/search*
// @match              *://*.google.co.tz/search*
// @match              *://*.google.com.ua/search*
// @match              *://*.google.co.ug/search*
// @match              *://*.google.co.uk/search*
// @match              *://*.google.com.uy/search*
// @match              *://*.google.co.uz/search*
// @match              *://*.google.com.vc/search*
// @match              *://*.google.co.ve/search*
// @match              *://*.google.vg/search*
// @match              *://*.google.co.vi/search*
// @match              *://*.google.com.vn/search*
// @match              *://*.google.vu/search*
// @match              *://*.google.ws/search*
// @match              *://*.google.rs/search*
// @match              *://*.google.co.za/search*
// @match              *://*.google.co.zm/search*
// @match              *://*.google.co.zw/search*
// @match              *://*.google.cat/search*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = 'googleSearchLangConfig';
  const CONFIG_VERSION = 1;
  const DEFAULT_LANGUAGES = Object.freeze(['lang_en']);

  const AVAILABLE_LANGUAGES = [
    { code: 'lang_ar', label: 'Arabic' },
    { code: 'lang_bn', label: 'Bengali' },
    { code: 'lang_bg', label: 'Bulgarian' },
    { code: 'lang_ca', label: 'Catalan' },
    { code: 'lang_zh-CN', label: 'Chinese (simplified)' },
    { code: 'lang_zh-TW', label: 'Chinese (traditional)' },
    { code: 'lang_hr', label: 'Croatian' },
    { code: 'lang_cs', label: 'Czech' },
    { code: 'lang_da', label: 'Danish' },
    { code: 'lang_nl', label: 'Dutch' },
    { code: 'lang_en', label: 'English' },
    { code: 'lang_et', label: 'Estonian' },
    { code: 'lang_tl', label: 'Filipino' },
    { code: 'lang_fi', label: 'Finnish' },
    { code: 'lang_fr', label: 'French' },
    { code: 'lang_de', label: 'German' },
    { code: 'lang_el', label: 'Greek' },
    { code: 'lang_gu', label: 'Gujarati' },
    { code: 'lang_iw', label: 'Hebrew' },
    { code: 'lang_hi', label: 'Hindi' },
    { code: 'lang_hu', label: 'Hungarian' },
    { code: 'lang_is', label: 'Icelandic' },
    { code: 'lang_id', label: 'Indonesian' },
    { code: 'lang_it', label: 'Italian' },
    { code: 'lang_ja', label: 'Japanese' },
    { code: 'lang_kn', label: 'Kannada' },
    { code: 'lang_ko', label: 'Korean' },
    { code: 'lang_lv', label: 'Latvian' },
    { code: 'lang_lt', label: 'Lithuanian' },
    { code: 'lang_ms', label: 'Malay' },
    { code: 'lang_ml', label: 'Malayalam' },
    { code: 'lang_mr', label: 'Marathi' },
    { code: 'lang_no', label: 'Norwegian' },
    { code: 'lang_fa', label: 'Persian' },
    { code: 'lang_pl', label: 'Polish' },
    { code: 'lang_pt', label: 'Portuguese' },
    { code: 'lang_pa', label: 'Punjabi' },
    { code: 'lang_ro', label: 'Romanian' },
    { code: 'lang_ru', label: 'Russian' },
    { code: 'lang_sr', label: 'Serbian' },
    { code: 'lang_sk', label: 'Slovak' },
    { code: 'lang_sl', label: 'Slovenian' },
    { code: 'lang_es', label: 'Spanish' },
    { code: 'lang_sv', label: 'Swedish' },
    { code: 'lang_ta', label: 'Tamil' },
    { code: 'lang_te', label: 'Telugu' },
    { code: 'lang_th', label: 'Thai' },
    { code: 'lang_tr', label: 'Turkish' },
    { code: 'lang_uk', label: 'Ukrainian' },
    { code: 'lang_ur', label: 'Urdu' },
    { code: 'lang_vi', label: 'Vietnamese' }
  ];

  const AVAILABLE_LANGUAGE_CODES = new Set(
    AVAILABLE_LANGUAGES.map((lang) => lang.code)
  );

  let cachedConfig = null;
  let hasCachedConfig = false;

  const gmApi = typeof GM === 'object' && GM !== null ? GM : null;
  const gmAsyncGet = gmApi && typeof gmApi.getValue === 'function'
    ? gmApi.getValue.bind(gmApi)
    : null;
  const gmAsyncSet = gmApi && typeof gmApi.setValue === 'function'
    ? gmApi.setValue.bind(gmApi)
    : null;

  function sanitizeLanguages(languages) {
    if (!Array.isArray(languages)) {
      return [];
    }

    const seen = new Set();
    const result = [];

    languages.forEach((code) => {
      if (typeof code !== 'string') {
        return;
      }
      const trimmed = code.trim();
      if (!trimmed || !AVAILABLE_LANGUAGE_CODES.has(trimmed)) {
        return;
      }
      if (seen.has(trimmed)) {
        return;
      }
      seen.add(trimmed);
      result.push(trimmed);
    });

    return result;
  }

  function normalizeConfig(rawConfig) {
    const hasLanguages =
      rawConfig && Object.prototype.hasOwnProperty.call(rawConfig, 'languages');
    const rawLanguages = hasLanguages ? rawConfig.languages : undefined;
    const sanitizedLanguages = sanitizeLanguages(rawLanguages);

    const shouldKeepEmptySelection =
      Array.isArray(rawLanguages) && rawLanguages.length === 0;

    const languages =
      sanitizedLanguages.length || shouldKeepEmptySelection
        ? sanitizedLanguages
        : Array.from(DEFAULT_LANGUAGES);

    return {
      version: CONFIG_VERSION,
      languages
    };
  }

  function parseStoredValue(value) {
    if (value == null) {
      return null;
    }
    if (typeof value === 'string') {
      if (!value) {
        return null;
      }
      try {
        return JSON.parse(value);
      } catch (err) {
        return null;
      }
    }
    if (typeof value === 'object') {
      return value;
    }
    return null;
  }

  async function readStoredConfig() {
    if (gmAsyncGet) {
      try {
        const value = await gmAsyncGet(STORAGE_KEY, null);
        return parseStoredValue(value);
      } catch (err) {
        return null;
      }
    }

    if (typeof GM_getValue === 'function') {
      try {
        const value = GM_getValue(STORAGE_KEY, null);
        return parseStoredValue(value);
      } catch (err) {
        return null;
      }
    }

    return null;
  }

  async function writeStoredConfig(config) {
    const serialized = JSON.stringify(config);

    if (gmAsyncSet) {
      try {
        await gmAsyncSet(STORAGE_KEY, serialized);
        return { success: true };
      } catch (err) {
        return {
          success: false,
          error: err && err.message ? err.message : 'Unknown error'
        };
      }
    }

    if (typeof GM_setValue === 'function') {
      try {
        GM_setValue(STORAGE_KEY, serialized);
        return { success: true };
      } catch (err) {
        return {
          success: false,
          error: err && err.message ? err.message : 'Unknown error'
        };
      }
    }

    return { success: false, error: 'GM_setValue unavailable' };
  }

  async function loadConfig() {
    if (hasCachedConfig) {
      return cachedConfig;
    }

    const stored = await readStoredConfig();
    cachedConfig = normalizeConfig(stored);
    hasCachedConfig = true;
    return cachedConfig;
  }

  async function saveConfig(config) {
    const normalized = normalizeConfig({
      ...config,
      version: CONFIG_VERSION
    });

    const writeResult = await writeStoredConfig(normalized);
    cachedConfig = normalized;
    hasCachedConfig = true;

    if (!writeResult.success) {
      return writeResult;
    }

    return { success: true, config: normalized };
  }

  function buildLrValue(languages) {
    return (languages || []).filter(Boolean).join('|');
  }

  function isGoogleSearchPage() {
    if (!/^www\.google\./.test(window.location.hostname)) {
      return false;
    }
    return window.location.pathname === '/search';
  }

  async function applyLanguageFilterIfNeeded(config) {
    if (!isGoogleSearchPage()) {
      return;
    }

    const activeConfig = config || (await loadConfig());

    const params = new URLSearchParams(window.location.search);
    const query = (params.get('q') || '').trim();
    if (!query) {
      return;
    }

    const lr = buildLrValue(activeConfig.languages);
    if (!lr) {
      return;
    }

    const currentUrl = window.location.href;

    if (params.get('lr') === lr) {
      return;
    }

    params.set('lr', lr);

    const newSearch = params.toString();
    const newUrl =
      window.location.origin +
      window.location.pathname +
      (newSearch ? `?${newSearch}` : '');

    if (newUrl === currentUrl) {
      return;
    }

    window.location.replace(newUrl);
  }

  function openConfigDialog(initialConfig) {
    if (!document.body) {
      window.addEventListener(
        'DOMContentLoaded',
        () => {
          openConfigDialog(initialConfig);
        },
        { once: true }
      );
      return;
    }

    const existing = document.getElementById('gs-lang-config-overlay');
    if (existing) {
      existing.remove();
    }

    const overlay = document.createElement('div');
    overlay.id = 'gs-lang-config-overlay';
    overlay.style.position = 'fixed';
    overlay.style.top = '0';
    overlay.style.left = '0';
    overlay.style.width = '100%';
    overlay.style.height = '100%';
    overlay.style.background = 'rgba(0, 0, 0, 0.4)';
    overlay.style.zIndex = '2147483647';
    overlay.style.display = 'flex';
    overlay.style.alignItems = 'center';
    overlay.style.justifyContent = 'center';
    overlay.setAttribute('role', 'dialog');
    overlay.setAttribute('aria-modal', 'true');
    overlay.setAttribute('aria-labelledby', 'gs-lang-config-title');

    const panel = document.createElement('div');
    panel.style.background = '#fff';
    panel.style.color = '#000';
    panel.style.padding = '16px 20px';
    panel.style.minWidth = '320px';
    panel.style.maxWidth = '420px';
    panel.style.borderRadius = '8px';
    panel.style.boxShadow = '0 2px 10px rgba(0,0,0,0.3)';
    panel.style.fontFamily = 'Arial, sans-serif';
    panel.style.fontSize = '13px';

    const title = document.createElement('div');
    title.id = 'gs-lang-config-title';
    title.setAttribute('role', 'heading');
    title.setAttribute('aria-level', '1');
    title.textContent = 'Google 搜索语言过滤配置';
    title.style.fontSize = '15px';
    title.style.fontWeight = 'bold';
    title.style.marginBottom = '8px';

    const hint = document.createElement('div');
    hint.textContent = '请选择要应用到搜索结果的语言(勾选一项或多项,可按名称搜索)。';
    hint.style.marginBottom = '6px';

    const searchWrapper = document.createElement('div');
    searchWrapper.style.marginBottom = '6px';

    const searchInput = document.createElement('input');
    searchInput.type = 'text';
    searchInput.placeholder = '按语言名称或代码过滤,例如 "english" 或 "en"';
    searchInput.style.width = '100%';
    searchInput.style.boxSizing = 'border-box';
    searchInput.style.padding = '4px 6px';
    searchInput.style.border = '1px solid #ddd';
    searchInput.style.borderRadius = '4px';
    searchInput.style.fontSize = '12px';

    searchWrapper.appendChild(searchInput);

    const list = document.createElement('div');
    list.style.maxHeight = '260px';
    list.style.overflowY = 'auto';
    list.style.border = '1px solid #ddd';
    list.style.padding = '8px 10px';
    list.style.marginTop = '4px';

    const initialLanguages =
      initialConfig && Array.isArray(initialConfig.languages)
        ? initialConfig.languages
        : Array.from(DEFAULT_LANGUAGES);
    const selectedSet = new Set(initialLanguages);

    function renderList(filterText) {
      const normalized = (filterText || '').trim().toLowerCase();
      list.innerHTML = '';

      AVAILABLE_LANGUAGES.forEach((lang) => {
        const labelText = `${lang.label} (${lang.code})`;
        if (
          normalized &&
          !labelText.toLowerCase().includes(normalized)
        ) {
          return;
        }

        const item = document.createElement('label');
        item.style.display = 'flex';
        item.style.alignItems = 'center';
        item.style.marginBottom = '4px';

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.value = lang.code;
        checkbox.checked = selectedSet.has(lang.code);
        checkbox.style.marginRight = '6px';

        checkbox.addEventListener('change', () => {
          if (checkbox.checked) {
            selectedSet.add(lang.code);
          } else {
            selectedSet.delete(lang.code);
          }
        });

        const text = document.createElement('span');
        text.textContent = labelText;

        item.appendChild(checkbox);
        item.appendChild(text);
        list.appendChild(item);
      });
    }

    renderList('');

    searchInput.addEventListener('input', () => {
      renderList(searchInput.value);
    });

    const buttons = document.createElement('div');
    buttons.style.marginTop = '12px';
    buttons.style.textAlign = 'right';

    const cancelBtn = document.createElement('button');
    cancelBtn.textContent = '取消';
    cancelBtn.style.marginRight = '8px';

    const saveBtn = document.createElement('button');
    const saveDefaultLabel = '保存';
    saveBtn.textContent = saveDefaultLabel;
    saveBtn.style.background = '#1a73e8';
    saveBtn.style.color = '#fff';
    saveBtn.style.border = 'none';
    saveBtn.style.padding = '4px 10px';
    saveBtn.style.borderRadius = '4px';
    saveBtn.style.cursor = 'pointer';

    const statusText = document.createElement('div');
    statusText.style.marginTop = '6px';
    statusText.style.minHeight = '1em';
    statusText.style.fontSize = '12px';
    statusText.style.textAlign = 'right';
    statusText.setAttribute('role', 'status');
    statusText.setAttribute('aria-live', 'polite');

    const handleKeydown = (event) => {
      if (event.key === 'Escape') {
        closeOverlay();
      }
    };

    function closeOverlay() {
      document.removeEventListener('keydown', handleKeydown);
      if (overlay.parentNode) {
        overlay.parentNode.removeChild(overlay);
      }
    }

    document.addEventListener('keydown', handleKeydown);

    cancelBtn.addEventListener('click', () => {
      closeOverlay();
    });

    saveBtn.addEventListener('click', async () => {
      saveBtn.disabled = true;
      saveBtn.textContent = '保存中...';
      statusText.textContent = '';
      const config = { languages: Array.from(selectedSet) };
      try {
        const result = await saveConfig(config);

        if (result.success) {
          statusText.textContent = '配置已保存';
          setTimeout(() => {
            closeOverlay();
          }, 400);
          return;
        }

        statusText.textContent = result.error
          ? `保存失败: ${result.error}`
          : '保存失败';
      } catch (err) {
        statusText.textContent = '保存失败: 未知错误';
      }

      saveBtn.disabled = false;
      saveBtn.textContent = saveDefaultLabel;
    });

    buttons.appendChild(cancelBtn);
    buttons.appendChild(saveBtn);

    panel.appendChild(title);
    panel.appendChild(hint);
    panel.appendChild(searchWrapper);
    panel.appendChild(list);
    panel.appendChild(buttons);
    panel.appendChild(statusText);

    overlay.appendChild(panel);

    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        closeOverlay();
      }
    });

    document.body.appendChild(overlay);
  }

  loadConfig()
    .then((config) => {
      return applyLanguageFilterIfNeeded(config);
    })
    .catch(() => {
      // Ignore storage errors during initial load; defaults will be used on next invocation.
    });

  if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('配置 Google 搜索语言过滤', async () => {
      try {
        const config = await loadConfig();
        openConfigDialog(config);
      } catch (err) {
        openConfigDialog(normalizeConfig(null));
      }
    });
  }
})();