Google Search Region Switcher — US/JP shortcuts, a full country picker, and Safari-compatible region preferences
| // ==UserScript== | |
| // @name Google Search Region Switcher | |
| // @namespace https://github.com/GalvinGao | |
| // @version 2026.09.14.5 | |
| // @description Fast Google region defaults with US/JP shortcuts, a full region picker, and tab-local choices. | |
| // @author GalvinGao | |
| // @match https://www.google.com/search* | |
| // @match https://www.google.com/ | |
| // @match https://www.google.com/?* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com | |
| // @run-at document-start | |
| // @noframes | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| if (location.pathname !== '/search' && location.pathname !== '/') return; | |
| const KEY = 'google_gl.manual_region.v2'; | |
| const region = value => typeof value === 'string' && /^[a-z]{2}$/i.test(value) ? value.toLowerCase() : null; | |
| // No manager-specific API: Safari Userscripts does not expose GM_getValue. | |
| const defaultRegion = 'us'; | |
| const readChoice = () => { | |
| try { return region(sessionStorage.getItem(KEY)); } catch { return null; } | |
| }; | |
| const saveChoice = value => { | |
| try { | |
| if (value) sessionStorage.setItem(KEY, value); | |
| else sessionStorage.removeItem(KEY); | |
| } catch { /* Explicit gl still works when storage is unavailable. */ } | |
| }; | |
| const currentRegion = () => region(new URL(location.href).searchParams.get('gl')) || readChoice() || defaultRegion; | |
| // An explicit gl is already the user's instruction; no second marker is needed. | |
| const initial = new URL(location.href); | |
| const legacy = region(initial.searchParams.get('force_gl')); | |
| if (legacy) saveChoice(legacy); | |
| if (initial.searchParams.has('force_gl')) { | |
| if (!region(initial.searchParams.get('gl')) && legacy) initial.searchParams.set('gl', legacy); | |
| initial.searchParams.delete('force_gl'); | |
| // If migrating a missing gl, fetch the selected region rather than relabeling old results. | |
| if (location.pathname === '/search' && !region(new URL(location.href).searchParams.get('gl')) && legacy) { | |
| location.replace(initial.href); | |
| return; | |
| } | |
| history.replaceState(history.state, '', initial.href); | |
| } | |
| if (location.pathname === '/search' && !region(initial.searchParams.get('gl'))) { | |
| initial.searchParams.set('gl', readChoice() || defaultRegion); | |
| // Run before any UI work; replace avoids a useless extra Back entry. | |
| location.replace(initial.href); | |
| return; | |
| } | |
| // Carry the region into Google navigation before the request, where possible. | |
| const searchURL = raw => { | |
| try { | |
| const url = new URL(raw, location.href); | |
| return url.origin === location.origin && url.pathname === '/search' ? url : null; | |
| } catch { return null; } | |
| }; | |
| const prepareLink = event => { | |
| const link = event.target instanceof Element ? event.target.closest('a[href]') : null; | |
| if (!link) return; | |
| const url = searchURL(link.href); | |
| if (!url) return; | |
| if (!region(url.searchParams.get('gl'))) url.searchParams.set('gl', region(url.searchParams.get('force_gl')) || currentRegion()); | |
| url.searchParams.delete('force_gl'); | |
| if (link.href !== url.href) link.href = url.href; | |
| }; | |
| for (const type of ['pointerdown', 'click', 'auxclick', 'contextmenu']) { | |
| document.addEventListener(type, prepareLink, true); | |
| } | |
| document.addEventListener('submit', event => { | |
| const form = event.target; | |
| if (!(form instanceof HTMLFormElement)) return; | |
| const submitter = event.submitter; | |
| const method = submitter?.getAttribute('formmethod') || form.method; | |
| if (method.toLowerCase() !== 'get') return; | |
| const action = searchURL(submitter?.getAttribute('formaction') || form.action); | |
| if (!action) return; | |
| const inputs = [...form.querySelectorAll('[name="gl"]')]; | |
| if (inputs.some(input => !input.disabled && region(input.value))) return; | |
| const input = inputs.find(item => item instanceof HTMLInputElement && !item.disabled) || document.createElement('input'); | |
| input.type = 'hidden'; | |
| input.name = 'gl'; | |
| input.value = region(action.searchParams.get('gl')) || currentRegion(); | |
| if (!input.parentNode) form.appendChild(input); | |
| }, true); | |
| function mount() { | |
| if (document.getElementById('google-gl-switch')) return; | |
| const host = document.createElement('div'); | |
| host.id = 'google-gl-switch'; | |
| host.style.cssText = 'all:initial;position:fixed;right:calc(12px + env(safe-area-inset-right,0px));bottom:calc(12px + env(safe-area-inset-bottom,0px) + var(--viewport-bottom,0px));z-index:2147483646;color-scheme:light dark;'; | |
| const root = host.attachShadow({ mode: 'open' }); | |
| root.innerHTML = ` | |
| :host { --surface:#fff; --text:#3c4043; --muted:#70757a; --line:#dadce0; --hover:#f1f3f4; --active:#e8f0fe; --accent:#174ea6; } | |
| * { box-sizing:border-box; } | |
| nav { display:flex;align-items:center;gap:3px;padding:5px;border:1px solid var(--line);border-radius:16px;background:var(--surface);box-shadow:0 4px 20px #00000014,0 1px 3px #0000000a;font:500 13px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--text); } | |
| .label { padding:0 9px;display:flex;align-items:center;gap:7px;color:var(--muted);font-size:11px;letter-spacing:.06em;text-transform:uppercase; } | |
| svg { width:15px;height:15px; } | |
| a { display:grid;place-items:center;min-width:42px;height:36px;padding:0 10px;border-radius:11px;text-decoration:none;color:var(--text);transition:background .12s,color .12s; } | |
| a:active { background:var(--hover); } | |
| a[aria-current="true"] { background:var(--active);color:var(--accent); } | |
| a:focus-visible { outline:2px solid var(--accent);outline-offset:2px; } | |
| select { appearance:auto;min-width:76px;max-width:140px;height:36px;border:0;border-radius:11px;padding:0 8px;background:var(--hover);color:var(--text);font:inherit;cursor:pointer; } | |
| select[data-selected] { background:var(--active);color:var(--accent); } | |
| select:focus-visible { outline:2px solid var(--accent);outline-offset:2px; } | |
| option,optgroup { background:var(--surface);color:var(--text); } | |
| </style> | |
| Region | |
| </span> | |
| US | |
| JP | |
| </nav>`; | |
| const links = [...root.querySelectorAll('a')]; | |
| const picker = root.querySelector('select'); | |
| const countries = [["af","Afghanistan"],["ax","Åland Islands"],["al","Albania"],["dz","Algeria"],["as","American Samoa"],["ad","Andorra"],["ao","Angola"],["ai","Anguilla"],["aq","Antarctica"],["ag","Antigua & Barbuda"],["ar","Argentina"],["am","Armenia"],["aw","Aruba"],["au","Australia"],["at","Austria"],["az","Azerbaijan"],["bs","Bahamas"],["bh","Bahrain"],["bd","Bangladesh"],["bb","Barbados"],["by","Belarus"],["be","Belgium"],["bz","Belize"],["bj","Benin"],["bm","Bermuda"],["bt","Bhutan"],["bo","Bolivia"],["ba","Bosnia & Herzegovina"],["bw","Botswana"],["bv","Bouvet Island"],["br","Brazil"],["io","British Indian Ocean Territory"],["vg","British Virgin Islands"],["bn","Brunei"],["bg","Bulgaria"],["bf","Burkina Faso"],["bi","Burundi"],["kh","Cambodia"],["cm","Cameroon"],["ca","Canada"],["cv","Cape Verde"],["bq","Caribbean Netherlands"],["ky","Cayman Islands"],["cf","Central African Republic"],["td","Chad"],["cl","Chile"],["cn","China"],["cx","Christmas Island"],["cc","Cocos (Keeling) Islands"],["co","Colombia"],["km","Comoros"],["cg","Congo - Brazzaville"],["cd","Congo - Kinshasa"],["ck","Cook Islands"],["cr","Costa Rica"],["ci","Côte d’Ivoire"],["hr","Croatia"],["cu","Cuba"],["cw","Curaçao"],["cy","Cyprus"],["cz","Czechia"],["dk","Denmark"],["dj","Djibouti"],["dm","Dominica"],["do","Dominican Republic"],["ec","Ecuador"],["eg","Egypt"],["sv","El Salvador"],["gq","Equatorial Guinea"],["er","Eritrea"],["ee","Estonia"],["sz","Eswatini"],["et","Ethiopia"],["fk","Falkland Islands"],["fo","Faroe Islands"],["fj","Fiji"],["fi","Finland"],["fr","France"],["gf","French Guiana"],["pf","French Polynesia"],["tf","French Southern Territories"],["ga","Gabon"],["gm","Gambia"],["ge","Georgia"],["de","Germany"],["gh","Ghana"],["gi","Gibraltar"],["gr","Greece"],["gl","Greenland"],["gd","Grenada"],["gp","Guadeloupe"],["gu","Guam"],["gt","Guatemala"],["gg","Guernsey"],["gn","Guinea"],["gw","Guinea-Bissau"],["gy","Guyana"],["ht","Haiti"],["hm","Heard & McDonald Islands"],["hn","Honduras"],["hk","Hong Kong"],["hu","Hungary"],["is","Iceland"],["in","India"],["id","Indonesia"],["ir","Iran"],["iq","Iraq"],["ie","Ireland"],["im","Isle of Man"],["il","Israel"],["it","Italy"],["jm","Jamaica"],["jp","Japan"],["je","Jersey"],["jo","Jordan"],["kz","Kazakhstan"],["ke","Kenya"],["ki","Kiribati"],["xk","Kosovo"],["kw","Kuwait"],["kg","Kyrgyzstan"],["la","Laos"],["lv","Latvia"],["lb","Lebanon"],["ls","Lesotho"],["lr","Liberia"],["ly","Libya"],["li","Liechtenstein"],["lt","Lithuania"],["lu","Luxembourg"],["mo","Macao"],["mg","Madagascar"],["mw","Malawi"],["my","Malaysia"],["mv","Maldives"],["ml","Mali"],["mt","Malta"],["mh","Marshall Islands"],["mq","Martinique"],["mr","Mauritania"],["mu","Mauritius"],["yt","Mayotte"],["mx","Mexico"],["fm","Micronesia"],["md","Moldova"],["mc","Monaco"],["mn","Mongolia"],["me","Montenegro"],["ms","Montserrat"],["ma","Morocco"],["mz","Mozambique"],["mm","Myanmar (Burma)"],["na","Namibia"],["nr","Nauru"],["np","Nepal"],["nl","Netherlands"],["nc","New Caledonia"],["nz","New Zealand"],["ni","Nicaragua"],["ne","Niger"],["ng","Nigeria"],["nu","Niue"],["nf","Norfolk Island"],["kp","North Korea"],["mk","North Macedonia"],["mp","Northern Mariana Islands"],["no","Norway"],["om","Oman"],["pk","Pakistan"],["pw","Palau"],["ps","Palestinian Territories"],["pa","Panama"],["pg","Papua New Guinea"],["py","Paraguay"],["pe","Peru"],["ph","Philippines"],["pn","Pitcairn Islands"],["pl","Poland"],["pt","Portugal"],["pr","Puerto Rico"],["qa","Qatar"],["re","Réunion"],["ro","Romania"],["ru","Russia"],["rw","Rwanda"],["ws","Samoa"],["sm","San Marino"],["st","São Tomé & Príncipe"],["sa","Saudi Arabia"],["sn","Senegal"],["rs","Serbia"],["sc","Seychelles"],["sl","Sierra Leone"],["sg","Singapore"],["sx","Sint Maarten"],["sk","Slovakia"],["si","Slovenia"],["sb","Solomon Islands"],["so","Somalia"],["za","South Africa"],["gs","South Georgia & South Sandwich Islands"],["kr","South Korea"],["ss","South Sudan"],["es","Spain"],["lk","Sri Lanka"],["bl","St. Barthélemy"],["sh","St. Helena"],["kn","St. Kitts & Nevis"],["lc","St. Lucia"],["mf","St. Martin"],["pm","St. Pierre & Miquelon"],["vc","St. Vincent & Grenadines"],["sd","Sudan"],["sr","Suriname"],["sj","Svalbard & Jan Mayen"],["se","Sweden"],["ch","Switzerland"],["sy","Syria"],["tw","Taiwan"],["tj","Tajikistan"],["tz","Tanzania"],["th","Thailand"],["tl","Timor-Leste"],["tg","Togo"],["tk","Tokelau"],["to","Tonga"],["tt","Trinidad & Tobago"],["tn","Tunisia"],["tr","Türkiye"],["tm","Turkmenistan"],["tc","Turks & Caicos Islands"],["tv","Tuvalu"],["um","U.S. Outlying Islands"],["vi","U.S. Virgin Islands"],["ug","Uganda"],["ua","Ukraine"],["ae","United Arab Emirates"],["gb","United Kingdom"],["us","United States"],["uy","Uruguay"],["uz","Uzbekistan"],["vu","Vanuatu"],["va","Vatican City"],["ve","Venezuela"],["vn","Vietnam"],["wf","Wallis & Futuna"],["eh","Western Sahara"],["ye","Yemen"],["zm","Zambia"],["zw","Zimbabwe"]]; | |
| const names = new Map(countries); | |
| const suggested = ['hk', 'mo', 'tw', 'kr', 'sg', 'gb', 'ca']; | |
| const usageKey = 'google_gl.region_usage.v1'; | |
| let usage = {}; | |
| try { | |
| const stored = JSON.parse(localStorage.getItem(usageKey) || '{}'); | |
| if (stored && typeof stored === 'object' && !Array.isArray(stored)) usage = stored; | |
| } catch { /* The picker also works without persistent storage. */ } | |
| function recordUse(code) { | |
| if (!names.has(code)) return; | |
| const count = Number.isSafeInteger(usage[code]) && usage[code] > 0 ? usage[code] : 0; | |
| usage[code] = Math.min(count + 1, 1000000); | |
| try { localStorage.setItem(usageKey, JSON.stringify(usage)); } catch { /* Optional ranking only. */ } | |
| } | |
| function buildPicker(active) { | |
| picker.replaceChildren(); | |
| const placeholder = new Option('More…', ''); | |
| placeholder.disabled = true; | |
| picker.appendChild(placeholder); | |
| const frequent = countries.map(([code]) => code) | |
| .filter(code => code !== 'us' && code !== 'jp' && Number.isSafeInteger(usage[code]) && usage[code] >= 2) | |
| .sort((a, b) => usage[b] - usage[a] || names.get(a).localeCompare(names.get(b), 'en')) | |
| .slice(0, 3); | |
| const featured = [...new Set([...frequent, ...suggested])]; | |
| const top = document.createElement('optgroup'); | |
| top.label = 'Frequently used & suggested'; | |
| top.appendChild(new Option('Auto (US default)', 'auto')); | |
| for (const code of featured) top.appendChild(new Option(names.get(code), code)); | |
| const rest = document.createElement('optgroup'); | |
| rest.label = 'All other countries & regions'; | |
| for (const [code, name] of countries) { | |
| if (!featured.includes(code)) rest.appendChild(new Option(name, code)); | |
| } | |
| if (!names.has(active)) rest.appendChild(new Option(active.toUpperCase(), active)); | |
| picker.append(top, rest); | |
| picker.value = active === 'us' || active === 'jp' ? '' : active; | |
| picker.toggleAttribute('data-selected', picker.value !== ''); | |
| } | |
| picker.addEventListener('change', () => { | |
| const choice = picker.value; | |
| if (!choice) return; | |
| const target = choice === 'auto' ? defaultRegion : choice; | |
| saveChoice(choice === 'auto' ? null : choice); | |
| if (choice !== 'auto') recordUse(choice); | |
| const url = new URL(location.href); | |
| url.searchParams.delete('force_gl'); | |
| if (target !== currentRegion()) url.searchParams.delete('start'); | |
| url.searchParams.set('gl', target); | |
| if (url.href !== location.href) location.assign(url.href); | |
| else update(); | |
| }); | |
| function update() { | |
| const active = currentRegion(); | |
| buildPicker(active); | |
| for (const link of links) { | |
| const choice = link.dataset.region; | |
| const target = choice === 'auto' ? defaultRegion : choice; | |
| const url = new URL(location.href); | |
| url.searchParams.set('gl', target); | |
| url.searchParams.delete('force_gl'); | |
| if (target !== active) url.searchParams.delete('start'); | |
| link.href = url.href; | |
| link.setAttribute('aria-current', String(choice === active)); | |
| link.title = choice === 'auto' ? 'Clear this tab’s choice; use ' + defaultRegion.toUpperCase() : (choice === 'jp' ? 'Japan' : 'United States') + ' — remember for this tab'; | |
| } | |
| } | |
| for (const link of links) { | |
| link.addEventListener('click', event => { | |
| // Modified clicks remain normal links and do not change the source tab. | |
| if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; | |
| event.preventDefault(); | |
| const choice = link.dataset.region; | |
| saveChoice(choice === 'auto' ? null : choice); | |
| update(); | |
| if (link.href !== location.href) location.assign(link.href); | |
| }); | |
| } | |
| update(); | |
| window.addEventListener('pageshow', update); | |
| window.addEventListener('popstate', update); | |
| // Mount outside Google's page wrappers so their transforms cannot trap fixed positioning. | |
| document.documentElement.appendChild(host); | |
| const viewport = window.visualViewport; | |
| function position() { | |
| // Follow the visible viewport when Safari expands its chrome or opens the keyboard. | |
| const inset = viewport ? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) : 0; | |
| host.style.setProperty('--viewport-bottom', inset + 'px'); | |
| } | |
| position(); | |
| viewport?.addEventListener('resize', position, { passive:true }); | |
| viewport?.addEventListener('scroll', position, { passive:true }); | |
| window.addEventListener('resize', position, { passive:true }); | |
| window.addEventListener('pageshow', position); | |
| } | |
| if (document.body) mount(); | |
| else document.addEventListener('DOMContentLoaded', mount, { once: true }); | |
| })(); |
评论
?
参与讨论