根據可配置語言,自動為 Google 搜尋結果套用 lr=lang_* 語言過濾。
// ==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));
}
});
}
})();