NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Microsoft Store Locale Redirect
// @namespace https://apps.microsoft.com/
// @version 2.3.3
// @description Sends Microsoft Store pages to the language and country you pick from 21 curated locales — a path segment on microsoft.com, hl/gl on apps.microsoft.com — keeping the choice in a cookie so both subdomains share it, redirecting without adding history entries, and clearing an invalid value instead of looping on it. On your wishlist it adds sort and filters with remembered settings, a shareable link and a 'Learn more' panel.
// @author g31w0fw0rld
// @license MIT
// @match https://apps.microsoft.com/detail/*
// @match https://www.microsoft.com/*/store/wishlist*
// @downloadURL https://github.com/g31w0fw0rld/microsoft-store-locale-redirect/raw/main/microsoft-store-locale-redirect.user.js
// @updateURL https://github.com/g31w0fw0rld/microsoft-store-locale-redirect/raw/main/microsoft-store-locale-redirect.user.js
// @grant none
// ==/UserScript==
(function () {
'use strict';
// =============================================
// DETECCIÓN DE RUTA
// =============================================
function isWishlist() {
return location.hostname === 'www.microsoft.com' && /\/store\/wishlist/i.test(location.pathname);
}
// =============================================
// IDIOMA (auto-detect: si la página/navegador está en español -> es, si no -> en)
// =============================================
// Prioriza el lang del documento (idioma con que Microsoft sirve la página) y
// cae al del navegador. Solo distingue español vs. resto (inglés por defecto).
function detectLang() {
const docLang = (document.documentElement.getAttribute('lang') || '').toLowerCase();
const navLang = (navigator.language || navigator.languages?.[0] || '').toLowerCase();
return (docLang || navLang).startsWith('es') ? 'es' : 'en';
}
const LANG = detectLang();
const I18N = {
es: {
sortLabel: 'Ordenar:', added: 'Agregado', name: 'Nombre', price: 'Precio', discount: 'Descuento',
onlyDiscount: 'Solo con descuento', remember: 'Recordar',
copy: '🔗 Copiar enlace', copied: '✔ Copiado', copyPrompt: 'Copia este enlace:',
about: 'ℹ️ Saber más', close: 'Cerrar',
regionLabel: 'Redirección:',
applyLabel: '✔ Aplicar', applyTip: 'Guarda el locale elegido y aplica la redirección ahora (recarga esta página, incluida la lista de deseos, en ese idioma/país). Con "Auto" no redirige.',
sortTip: 'Ordena tu lista de deseos por fecha de agregado, nombre, precio o porcentaje de descuento.',
dirTip: 'Alterna entre orden ascendente (↑) y descendente (↓).',
onlyDiscountTip: 'Oculta los juegos que no están en oferta; muestra solo los que tienen descuento.',
rememberTip: 'Guarda tu orden y filtros y los reaplica al volver a la lista de deseos.',
copyTip: 'Copia un enlace que reproduce tu orden y filtros actuales al abrirlo.',
regionTip: 'Elige el idioma/país (locale) al que redirigir las páginas de Microsoft Store, incluida esta lista de deseos. Con "Auto" no redirige. Pulsa "Aplicar" para guardar y redirigir ahora.',
aboutTip: 'Ver qué hace este script en su totalidad.',
aboutTitle: '¿Qué hace este script?',
aboutBody: [
'Este script mejora Microsoft Store en dos frentes:',
'• Redirección de región: lleva las páginas de Microsoft Store —apps.microsoft.com y tu lista de deseos— al idioma/país (locale) que elijas en el selector. Con "Auto" no redirige.',
'– El selector ofrece 21 locales curados, solo combinaciones que la tienda soporta de verdad.',
'– Redirige de dos formas según el sitio: en microsoft.com el locale es un segmento de la ruta (/es-mx/) y en apps.microsoft.com es una query (hl y gl).',
'– Usa un reemplazo en vez de una navegación nueva, así que no deja entrada extra en el historial y el botón Atrás se comporta con normalidad.',
'– Un locale guardado inválido se borra en vez de usarse, para no entrar en bucles de redirección.',
'– "Aplicar" guarda tu elección y redirige al momento, lista de deseos incluida.',
'• Herramientas en tu lista de deseos:',
'– Ordenar: por fecha de agregado, nombre, precio o descuento, con un botón ↑/↓ para ascendente o descendente.',
'– Solo con descuento: muestra únicamente las apps/juegos en oferta.',
'– Recordar: guarda tu orden y filtros y los reaplica al volver.',
'– Copiar enlace: genera una URL que reproduce tu orden y filtros. Si el navegador bloquea el portapapeles, la muestra en un diálogo para copiarla a mano.',
'La preferencia de país/idioma se guarda en una cookie de microsoft.com, porque la lista de deseos y las fichas de app están en subdominios distintos que no comparten localStorage; el resto sí va en localStorage. No se envían datos a ningún servidor.'
]
},
en: {
sortLabel: 'Sort:', added: 'Added', name: 'Name', price: 'Price', discount: 'Discount',
onlyDiscount: 'Only discounted', remember: 'Remember',
copy: '🔗 Copy link', copied: '✔ Copied', copyPrompt: 'Copy this link:',
about: 'ℹ️ Learn more', close: 'Close',
regionLabel: 'Redirect:',
applyLabel: '✔ Apply', applyTip: 'Saves the chosen locale and applies the redirect now (reloads this page, wishlist included, in that language/country). With "Auto" it does not redirect.',
sortTip: 'Sorts your wishlist by date added, name, price or discount percentage.',
dirTip: 'Toggles ascending (↑) and descending (↓) order.',
onlyDiscountTip: 'Hides items that are not on sale; shows only discounted ones.',
rememberTip: 'Saves your sort and filters and reapplies them when you return to the wishlist.',
copyTip: 'Copies a link that reproduces your current sort and filters when opened.',
regionTip: 'Choose the language/country (locale) to redirect Microsoft Store pages to, including this wishlist. With "Auto" it does not redirect. Click "Apply" to save and redirect now.',
aboutTip: 'See everything this script does.',
aboutTitle: 'What does this script do?',
aboutBody: [
'This script improves Microsoft Store in two ways:',
'• Region redirect: takes Microsoft Store pages —apps.microsoft.com and your wishlist— to the language/country (locale) you pick in the selector. With "Auto" it does not redirect.',
'– The selector offers 21 curated locales, only combinations the store actually supports.',
'– It redirects two different ways depending on the site: on microsoft.com the locale is a path segment (/en-us/) and on apps.microsoft.com it is a query (hl and gl).',
'– It uses a replace rather than a new navigation, so it leaves no extra history entry and the Back button behaves normally.',
'– An invalid saved locale is cleared instead of used, so a bad value cannot cause a redirect loop.',
'– "Apply" saves your choice and redirects right away, wishlist included.',
'• Wishlist tools:',
'– Sort: by date added, name, price or discount, with an ↑/↓ button for ascending or descending.',
'– Only discounted: shows only items on sale.',
'– Remember: saves your sort and filters and reapplies them on return.',
'– Copy link: builds a URL that reproduces your sort and filters. If the browser blocks clipboard access, it shows the URL in a dialog so you can copy it by hand.',
'The country/language preference is stored in a microsoft.com cookie, because the wishlist and the app pages sit on different subdomains that do not share localStorage; the rest does go in localStorage. No data is sent to any server.'
]
}
};
const t = I18N[LANG];
// Lista curada de LOCALES válidos (combinación idioma-país). Un solo selector:
// así solo se ofrecen combinaciones que Microsoft Store realmente soporta. El
// código vacío ('') significa "Auto": no forzar redirección.
const LOCALES = [
{ code: '', es: 'Auto (no redirigir)', en: 'Auto (no redirect)' },
{ code: 'es-MX', es: 'Español – México (es-MX)', en: 'Spanish – Mexico (es-MX)' },
{ code: 'es-ES', es: 'Español – España (es-ES)', en: 'Spanish – Spain (es-ES)' },
{ code: 'es-AR', es: 'Español – Argentina (es-AR)', en: 'Spanish – Argentina (es-AR)' },
{ code: 'es-CO', es: 'Español – Colombia (es-CO)', en: 'Spanish – Colombia (es-CO)' },
{ code: 'es-CL', es: 'Español – Chile (es-CL)', en: 'Spanish – Chile (es-CL)' },
{ code: 'en-US', es: 'Inglés – EE. UU. (en-US)', en: 'English – United States (en-US)' },
{ code: 'en-GB', es: 'Inglés – Reino Unido (en-GB)', en: 'English – United Kingdom (en-GB)' },
{ code: 'en-CA', es: 'Inglés – Canadá (en-CA)', en: 'English – Canada (en-CA)' },
{ code: 'en-AU', es: 'Inglés – Australia (en-AU)', en: 'English – Australia (en-AU)' },
{ code: 'pt-BR', es: 'Portugués – Brasil (pt-BR)', en: 'Portuguese – Brazil (pt-BR)' },
{ code: 'fr-FR', es: 'Francés – Francia (fr-FR)', en: 'French – France (fr-FR)' },
{ code: 'fr-CA', es: 'Francés – Canadá (fr-CA)', en: 'French – Canada (fr-CA)' },
{ code: 'de-DE', es: 'Alemán – Alemania (de-DE)', en: 'German – Germany (de-DE)' },
{ code: 'it-IT', es: 'Italiano – Italia (it-IT)', en: 'Italian – Italy (it-IT)' },
{ code: 'ja-JP', es: 'Japonés – Japón (ja-JP)', en: 'Japanese – Japan (ja-JP)' },
{ code: 'ko-KR', es: 'Coreano – Corea del Sur (ko-KR)', en: 'Korean – South Korea (ko-KR)' },
{ code: 'zh-CN', es: 'Chino – China (zh-CN)', en: 'Chinese – China (zh-CN)' },
{ code: 'ru-RU', es: 'Ruso – Rusia (ru-RU)', en: 'Russian – Russia (ru-RU)' },
{ code: 'pl-PL', es: 'Polaco – Polonia (pl-PL)', en: 'Polish – Poland (pl-PL)' },
{ code: 'nl-NL', es: 'Neerlandés – Países Bajos (nl-NL)', en: 'Dutch – Netherlands (nl-NL)' },
{ code: 'tr-TR', es: 'Turco – Turquía (tr-TR)', en: 'Turkish – Turkey (tr-TR)' }
];
// =============================================
// LOCALE REDIRECT (apps.microsoft.com + wishlist en www.microsoft.com)
// =============================================
// Segmento de locale en la ruta de www.microsoft.com (ej. /es-mx/store/...).
const LOCALE_PATH_REGEX = /\/([a-z]{2}-[a-z]{2})\//i;
// Preferencia de locale. Cookie con domain=.microsoft.com para que se comparta
// entre el wishlist (www.microsoft.com) y las páginas de app (apps.microsoft.com),
// subdominios distintos que NO comparten localStorage. La cookie sí cruza.
const LOCALE_COOKIE = 'mswl-locale';
// ¿Es un locale válido de la lista curada? Evita valores viejos/parciales
// (p. ej. "en-" guardado por versiones anteriores) que provocaban
// redirecciones inválidas en bucle.
function isValidLocale(code) {
return !!code && LOCALES.some((l) => l.code && l.code.toLowerCase() === code.toLowerCase());
}
// Lee el locale guardado (ej. "es-MX"); '' = Auto (no redirigir). Sanea
// valores inválidos borrándolos, para no entrar en bucles de redirección.
function readLocalePref() {
try {
const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + LOCALE_COOKIE + '=([^;]+)'));
const v = m ? decodeURIComponent(m[1]) : '';
if (v && !isValidLocale(v)) { saveLocalePref(''); return ''; }
return v;
} catch (e) { return ''; }
}
// Guarda el locale elegido ('' = Auto). Cookie a 1 año.
function saveLocalePref(code) {
try {
document.cookie = `${LOCALE_COOKIE}=${encodeURIComponent(code || '')}; domain=.microsoft.com; path=/; max-age=${60 * 60 * 24 * 365}`;
} catch (e) { console.error('(mswl): saveLocalePref error:', e); }
}
// Locale destino canónico (ej. "es-MX") o '' si Auto/sin preferencia.
function desiredLocale() { return readLocalePref(); }
// Construye la URL destino aplicando el locale según el host:
// - www.microsoft.com: segmento de ruta en minúsculas (/es-mx/).
// - apps.microsoft.com: query hl (es-mx) + gl (MX).
// Devuelve null si no hay que redirigir (Auto, inválido o ya correcto).
function buildLocaleUrl(code) {
if (!isValidLocale(code)) return null;
const country = (code.split('-')[1] || '').toUpperCase();
if (location.hostname === 'www.microsoft.com') {
const cur = window.location.href;
const m = cur.match(LOCALE_PATH_REGEX);
if (!m) return null;
if (m[1].toLowerCase() === code.toLowerCase()) return null;
return cur.replace(LOCALE_PATH_REGEX, `/${code.toLowerCase()}/`);
}
// apps.microsoft.com (u otros): query hl/gl.
const url = new URL(window.location.href);
const curHl = (url.searchParams.get('hl') || '').toLowerCase();
const curGl = (url.searchParams.get('gl') || '').toUpperCase();
if (curHl === code.toLowerCase() && (!country || curGl === country)) return null;
url.searchParams.set('hl', code.toLowerCase());
if (country) url.searchParams.set('gl', country);
return url.toString();
}
/**
* Si hay preferencia explícita y el locale actual difiere, redirige (sin
* historial). Con Auto ('') no fuerza nada. Aplica en apps.microsoft.com y en
* la lista de deseos de www.microsoft.com.
*/
function redirectIfNeeded() {
const target = buildLocaleUrl(desiredLocale());
if (target && target !== window.location.href) window.location.replace(target);
}
// =============================================
// WISHLIST — ordenar y filtrar
// =============================================
// Microsoft Store renderiza el wishlist con clases estables (no hasheadas),
// así que los selectores son directos y sobreviven a los rebuilds.
const LIST_SELECTOR = 'ul.wishlist-list';
const ITEM_SELECTOR = 'li.product-wishlist-item';
const TITLE_SELECTOR = '.wishlist-item-title';
const PRICE_BOX_SELECTOR = '[id^="wishlist-price-"]';
const CUR_PRICE_SELECTOR = '.font-weight-semibold'; // precio vigente
const ORIG_PRICE_SELECTOR = '.text-line-through'; // precio original (tachado)
const DISCOUNT_BADGE_SELECTOR = '.badge.bg-yellow'; // "Ahorra $X"
const ORD_ATTR = 'data-mswl-ord';
const TOOLBAR_ID = 'mswl-toolbar';
const STYLES_ID = 'mswl-styles';
const SCRIPT_VERSION = '2.3.3'; // sincronizar con @version
const SETTINGS_KEY = 'mswl-settings';
const SORTS = ['added', 'name', 'price', 'discount'];
const SORT_LABELS = { added: t.added, name: t.name, price: t.price, discount: t.discount };
let settings = loadSettings();
let applying = false; // silencia el observer al reordenar
let listObserver = null;
let observerDebounce = null;
// --- Persistencia -----------------------------------------------------------
function loadSettings() {
const def = { remember: true, sort: 'added', dir: 'asc', onlyDiscount: false };
try {
const raw = localStorage.getItem(SETTINGS_KEY);
const parsed = raw ? JSON.parse(raw) : null;
if (parsed && typeof parsed === 'object') {
return Object.assign(def, parsed, {
sort: SORTS.includes(parsed.sort) ? parsed.sort : 'added',
dir: parsed.dir === 'desc' ? 'desc' : 'asc',
onlyDiscount: !!parsed.onlyDiscount,
remember: parsed.remember !== false
});
}
} catch (e) { console.error('(mswl): loadSettings error:', e); }
return def;
}
function saveSettings() {
try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); }
catch (e) { console.error('(mswl): saveSettings error:', e); }
}
function persistIfRemember() { if (settings.remember !== false) saveSettings(); }
// --- URL compartible (parámetros legibles) ----------------------------------
function readUrlView() {
const p = new URLSearchParams(location.search);
if (!p.has('wlsort') && !p.has('wldir') && !p.has('wldisc')) return null;
return {
sort: SORTS.includes(p.get('wlsort')) ? p.get('wlsort') : 'added',
dir: p.get('wldir') === 'desc' ? 'desc' : 'asc',
onlyDiscount: p.get('wldisc') === '1'
};
}
function buildShareUrl() {
const p = new URLSearchParams(location.search);
// Conservar params ajenos (p. ej. ?id=... de una lista compartida).
p.delete('wlsort'); p.delete('wldir'); p.delete('wldisc');
if (settings.sort && settings.sort !== 'added') p.set('wlsort', settings.sort);
if (settings.dir && settings.dir !== 'asc') p.set('wldir', settings.dir);
if (settings.onlyDiscount) p.set('wldisc', '1');
const qs = p.toString();
return location.origin + location.pathname + (qs ? ('?' + qs) : '');
}
// --- Extracción -------------------------------------------------------------
function parsePrice(txt) {
if (!txt) return null;
const m = txt.replace(/\s/g, '').match(/[\d.]+/);
if (!m) return null;
let s = m[0];
const lastDot = s.lastIndexOf('.'), lastComma = s.lastIndexOf(',');
if (lastDot >= 0 && lastComma >= 0) {
if (lastDot > lastComma) s = s.replace(/,/g, '');
else s = s.replace(/\./g, '').replace(',', '.');
} else if (lastComma >= 0) {
s = (s.length - 1 - lastComma === 3) ? s.replace(/,/g, '') : s.replace(',', '.');
}
const n = parseFloat(s);
return isNaN(n) ? null : n;
}
function extract(el) {
const name = (el.querySelector(TITLE_SELECTOR)?.textContent || '').trim();
const box = el.querySelector(PRICE_BOX_SELECTOR) || el;
const price = parsePrice(box.querySelector(CUR_PRICE_SELECTOR)?.textContent);
const original = parsePrice(box.querySelector(ORIG_PRICE_SELECTOR)?.textContent);
const hasBadge = !!el.querySelector(DISCOUNT_BADGE_SELECTOR);
const discounted = (original != null && price != null && original > price) || hasBadge;
const disc = (discounted && original && price) ? (original - price) / original : 0;
const ord = parseInt(el.getAttribute(ORD_ATTR), 10);
return { name, price, original, discounted, disc, ord: isNaN(ord) ? 0 : ord };
}
// --- Ordenar / filtrar ------------------------------------------------------
function getListEl() { return document.querySelector(LIST_SELECTOR); }
function getItems(list) { return Array.from((list || document).querySelectorAll(ITEM_SELECTOR)); }
function tagOriginalOrder(items) {
items.forEach((el, i) => { if (el.getAttribute(ORD_ATTR) == null) el.setAttribute(ORD_ATTR, String(i)); });
}
function priceCmp(a, b) { const x = a == null ? Infinity : a, y = b == null ? Infinity : b; return x - y; }
function apply() {
const list = getListEl();
if (!list) return;
const items = getItems(list);
if (!items.length) return;
tagOriginalOrder(items);
// Desconectar el observer mientras reordenamos: appendChild dispara
// mutaciones de childList que, como el callback corre en microtask (tras
// resetear el flag), reentrarían en apply() en bucle. Reconectar al final
// descarta esas mutaciones propias y deja escuchando cambios externos.
applying = true;
if (listObserver) listObserver.disconnect();
try {
const mul = settings.dir === 'desc' ? -1 : 1;
const rows = items.map((el) => ({ el, d: extract(el) }));
rows.sort((a, b) => {
let c = 0;
if (settings.sort === 'name') c = a.d.name.localeCompare(b.d.name, undefined, { sensitivity: 'base' });
else if (settings.sort === 'price') c = priceCmp(a.d.price, b.d.price);
else if (settings.sort === 'discount') c = a.d.disc - b.d.disc;
else c = a.d.ord - b.d.ord;
if (c === 0) c = a.d.ord - b.d.ord;
return c * mul;
});
rows.forEach(({ el, d }) => {
el.style.display = (settings.onlyDiscount && !d.discounted) ? 'none' : '';
list.appendChild(el);
});
} finally {
applying = false;
if (listObserver) listObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
}
}
// --- UI ---------------------------------------------------------------------
function injectStyles() {
if (document.getElementById(STYLES_ID)) return;
const style = document.createElement('style');
style.id = STYLES_ID;
style.textContent = `
#${TOOLBAR_ID} {
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
margin: 0 0 16px; padding: 10px 12px; border-radius: 8px;
background: rgba(127,127,127,.12); font-size: 14px; color: inherit;
}
#${TOOLBAR_ID} label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
#${TOOLBAR_ID} select, #${TOOLBAR_ID} button {
font-size: 14px; padding: 4px 8px; border-radius: 6px;
border: 1px solid rgba(127,127,127,.4); background: inherit; color: inherit; cursor: pointer;
}
#${TOOLBAR_ID} .mswl-dir { min-width: 2.2em; text-align: center; font-weight: 600; }
#${TOOLBAR_ID} .mswl-share { background: #107c10; color: #fff; border: none; }
#${TOOLBAR_ID} .mswl-region { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; }
#${TOOLBAR_ID} .mswl-apply { background: #107c10; color: #fff; border: none; font-weight: 600; }
`;
(document.head || document.documentElement).appendChild(style);
}
// --- Modal "Saber más" (autocontenido) --------------------------------------
function showAboutModal() {
if (document.getElementById('mswl-about-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'mswl-about-overlay';
Object.assign(overlay.style, {
position: 'fixed', inset: '0', width: '100%', height: '100%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.6)', zIndex: '2147483647',
transition: 'opacity 180ms ease', opacity: '0'
});
const box = document.createElement('div');
Object.assign(box.style, {
background: '#12181f', color: '#f2f5f7', borderRadius: '14px',
padding: '26px 30px', minWidth: '320px', maxWidth: '560px',
maxHeight: '80vh', overflowY: 'auto', boxSizing: 'border-box',
boxShadow: '0 8px 32px rgba(0,0,0,0.5)', border: '1px solid #0067b8',
fontFamily: 'Segoe UI, system-ui, sans-serif', fontSize: '14px', lineHeight: '1.5',
transform: 'translateY(8px) scale(0.98)', opacity: '0',
transition: 'transform 180ms ease, opacity 180ms ease'
});
const title = document.createElement('div');
title.textContent = t.aboutTitle;
title.style.cssText = 'font-weight:bold;font-size:17px;margin-bottom:14px;color:#4ca3e0;';
box.appendChild(title);
(t.aboutBody || []).forEach((p) => {
const row = document.createElement('div');
const trimmed = String(p).replace(/^\s+/, '');
row.textContent = trimmed;
row.style.marginBottom = '8px';
if (trimmed.startsWith('–')) row.style.paddingLeft = '22px';
else if (trimmed.startsWith('•')) row.style.paddingLeft = '10px';
box.appendChild(row);
});
const gh = document.createElement('a');
gh.href = 'https://github.com/g31w0fw0rld/microsoft-store-locale-redirect';
gh.target = '_blank'; gh.rel = 'noopener';
gh.textContent = 'github.com/g31w0fw0rld/microsoft-store-locale-redirect';
gh.style.cssText = 'display:inline-block;margin-top:6px;color:#4ca3e0;text-decoration:underline;font-size:12px;';
box.appendChild(gh);
const kofi = document.createElement('a');
kofi.href = 'https://ko-fi.com/g31w0fw0rld';
kofi.target = '_blank'; kofi.rel = 'noopener';
kofi.textContent = '☕ Apóyame en Ko-fi / Support me on Ko-fi';
kofi.style.cssText = 'display:block;margin-top:8px;color:#4ca3e0;text-decoration:underline;font-size:12px;';
box.appendChild(kofi);
const foot = document.createElement('div');
foot.textContent = 'v' + SCRIPT_VERSION + ' · g31w0fw0rld';
foot.style.cssText = 'margin-top:2px;font-size:12px;opacity:0.7;';
box.appendChild(foot);
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.textContent = t.close;
closeBtn.style.cssText = 'display:block;margin-top:16px;padding:8px 14px;background:#0067b8;color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:bold;font-size:13px;';
box.appendChild(closeBtn);
const closeIt = () => {
overlay.style.opacity = '0'; box.style.opacity = '0';
box.style.transform = 'translateY(8px) scale(0.98)';
document.removeEventListener('keydown', onKey);
setTimeout(() => overlay.remove(), 180);
};
const onKey = (e) => { if (e.key === 'Escape') closeIt(); };
closeBtn.addEventListener('click', closeIt);
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeIt(); });
document.addEventListener('keydown', onKey);
overlay.appendChild(box);
document.body.appendChild(overlay);
setTimeout(() => {
overlay.style.opacity = '1';
box.style.transform = 'translateY(0) scale(1)';
box.style.opacity = '1';
}, 10);
}
// Construye un <select> a partir de una lista curada [{code,es,en}].
function buildLocaleSelect(current) {
const sel = document.createElement('select');
LOCALES.forEach((it) => {
const o = document.createElement('option');
o.value = it.code;
o.textContent = it[LANG];
if (it.code.toLowerCase() === (current || '').toLowerCase()) o.selected = true;
sel.appendChild(o);
});
return sel;
}
function buildToolbar() {
injectStyles();
const bar = document.createElement('div');
bar.id = TOOLBAR_ID;
const sortLabel = document.createElement('label');
sortLabel.title = t.sortTip;
sortLabel.appendChild(document.createTextNode(t.sortLabel));
const sortSel = document.createElement('select');
SORTS.forEach((s) => {
const o = document.createElement('option');
o.value = s; o.textContent = SORT_LABELS[s];
if (s === settings.sort) o.selected = true;
sortSel.appendChild(o);
});
sortSel.addEventListener('change', () => {
settings.sort = sortSel.value;
settings.dir = (settings.sort === 'discount') ? 'desc' : 'asc';
dirBtn.textContent = settings.dir === 'desc' ? '↓' : '↑';
persistIfRemember(); apply();
});
sortLabel.appendChild(sortSel);
const dirBtn = document.createElement('button');
dirBtn.type = 'button';
dirBtn.className = 'mswl-dir';
dirBtn.title = t.dirTip;
dirBtn.textContent = settings.dir === 'desc' ? '↓' : '↑';
dirBtn.addEventListener('click', () => {
settings.dir = settings.dir === 'desc' ? 'asc' : 'desc';
dirBtn.textContent = settings.dir === 'desc' ? '↓' : '↑';
persistIfRemember(); apply();
});
const discLabel = document.createElement('label');
discLabel.title = t.onlyDiscountTip;
const discChk = document.createElement('input');
discChk.type = 'checkbox';
discChk.checked = !!settings.onlyDiscount;
discChk.addEventListener('change', () => { settings.onlyDiscount = discChk.checked; persistIfRemember(); apply(); });
discLabel.appendChild(discChk);
discLabel.appendChild(document.createTextNode(t.onlyDiscount));
const remLabel = document.createElement('label');
remLabel.title = t.rememberTip;
const remChk = document.createElement('input');
remChk.type = 'checkbox';
remChk.checked = settings.remember !== false;
remChk.addEventListener('change', () => { settings.remember = remChk.checked; saveSettings(); });
remLabel.appendChild(remChk);
remLabel.appendChild(document.createTextNode(t.remember));
const shareBtn = document.createElement('button');
shareBtn.type = 'button';
shareBtn.className = 'mswl-share';
shareBtn.title = t.copyTip;
shareBtn.textContent = t.copy;
shareBtn.addEventListener('click', async () => {
const url = buildShareUrl();
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(url);
shareBtn.textContent = t.copied;
setTimeout(() => { shareBtn.textContent = t.copy; }, 2000);
} else { window.prompt(t.copyPrompt, url); }
} catch (e) { window.prompt(t.copyPrompt, url); }
});
// Selector único de redirección (locale = idioma-país), guardado en cookie
// de .microsoft.com. Cada opción explica la combinación en su label.
const localeSel = buildLocaleSelect(readLocalePref());
const regionText = document.createElement('span');
regionText.textContent = t.regionLabel;
regionText.title = t.regionTip;
regionText.style.fontWeight = '600';
const localeWrap = document.createElement('label');
localeWrap.title = t.regionTip;
localeWrap.appendChild(localeSel);
// Botón "Aplicar": guarda el locale elegido y redirige la página actual
// (incluida la lista de deseos) al instante. Con "Auto" solo recarga.
const applyBtn = document.createElement('button');
applyBtn.type = 'button';
applyBtn.className = 'mswl-apply';
applyBtn.textContent = t.applyLabel;
applyBtn.title = t.applyTip;
applyBtn.addEventListener('click', () => {
const code = localeSel.value;
saveLocalePref(code);
const target = buildLocaleUrl(code);
if (target && target !== window.location.href) window.location.assign(target);
else window.location.reload();
});
// Grupo de región: "Redirección: [locale ▾] [Aplicar]" viaja junto.
const regionGroup = document.createElement('span');
regionGroup.className = 'mswl-region';
regionGroup.appendChild(regionText);
regionGroup.appendChild(localeWrap);
regionGroup.appendChild(applyBtn);
// Botón "Saber más"
const aboutBtn = document.createElement('button');
aboutBtn.type = 'button';
aboutBtn.className = 'mswl-about';
aboutBtn.title = t.aboutTip;
aboutBtn.textContent = t.about;
aboutBtn.addEventListener('click', showAboutModal);
bar.appendChild(sortLabel);
bar.appendChild(dirBtn);
bar.appendChild(discLabel);
bar.appendChild(remLabel);
bar.appendChild(shareBtn);
bar.appendChild(regionGroup);
bar.appendChild(aboutBtn);
return bar;
}
function ensureToolbar() {
if (document.getElementById(TOOLBAR_ID)) return;
const list = getListEl();
if (!list) return;
list.parentNode.insertBefore(buildToolbar(), list);
}
// --- Observer + init --------------------------------------------------------
function startObserver() {
if (listObserver) return;
listObserver = new MutationObserver(() => {
if (applying) return;
if (observerDebounce) return;
observerDebounce = setTimeout(() => {
observerDebounce = null;
ensureToolbar();
apply();
}, 250);
});
listObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
}
function waitForList(timeoutMs) {
return new Promise((resolve) => {
if (getListEl()) return resolve(getListEl());
const deadline = Date.now() + (timeoutMs || 15000);
const iv = setInterval(() => {
if (getListEl()) { clearInterval(iv); resolve(getListEl()); }
else if (Date.now() > deadline) { clearInterval(iv); resolve(null); }
}, 200);
});
}
async function initWishlist() {
const list = await waitForList(20000);
if (!list) return;
const fromUrl = readUrlView();
if (fromUrl) {
settings.sort = fromUrl.sort;
settings.dir = fromUrl.dir;
settings.onlyDiscount = fromUrl.onlyDiscount;
if (settings.remember !== false) saveSettings();
}
ensureToolbar();
apply();
startObserver();
console.log('(mswl): Microsoft Store wishlist tools activos');
}
// =============================================
// INICIALIZACIÓN (por ruta)
// =============================================
// Microsoft Store es una SPA: si se navega sin recargar, se reintenta.
// La redirección se evalúa SIEMPRE (también en la lista de deseos); si ya
// estamos en el locale correcto no hace nada y se cargan las herramientas.
function route() {
try {
redirectIfNeeded();
if (isWishlist()) initWishlist();
} catch (e) {
console.error('(microsoft-store-locale-redirect): Error:', e);
}
}
(function watchSpaNav() {
const fire = () => setTimeout(route, 300);
const p = history.pushState, r = history.replaceState;
history.pushState = function () { p.apply(this, arguments); fire(); };
history.replaceState = function () { r.apply(this, arguments); fire(); };
window.addEventListener('popstate', fire);
})();
route();
})();