Calculate steam review positive rate for other languages.
// ==UserScript==
// @name Steam Review Other lang Calculation
// @name:zh-CN Steam评测其他语言好评率计算
// @namespace https://controlnet.space/
// @version 2026-03-31
// @description Calculate steam review positive rate for other languages.
// @description:zh-CN 计算Steam消费者评测中其他语言的好评率。
// @author ControlNet
// @match https://store.steampowered.com/app/*
// @grant none
// @license AGPL-3.0
// ==/UserScript==
(function () {
'use strict';
const RESULT_ID = 'tm_other_lang_review_rate';
const STYLE_ID = 'tm_other_lang_review_rate_style';
const LANG_MAP = {
'zh-cn': 'schinese',
'zh-tw': 'tchinese',
'en': 'english',
'ja': 'japanese',
'ko': 'koreana',
'fr': 'french',
'de': 'german',
'es': 'spanish',
'ru': 'russian',
'pt-br': 'brazilian',
'pt': 'portuguese',
'it': 'italian',
'pl': 'polish',
'tr': 'turkish',
'th': 'thai',
'vi': 'vietnamese',
'uk': 'ukrainian'
};
let cachedResult = null;
let observer = null;
let renderScheduled = false;
let dataLoaded = false;
function log(...args) {
console.log('[Steam Other Lang Review]', ...args);
}
function getAppId() {
const match = location.pathname.match(/\/app\/(\d+)/);
return match ? match[1] : null;
}
function getSteamLanguage() {
const htmlLang = (document.documentElement.lang || '').toLowerCase();
if (LANG_MAP[htmlLang]) return LANG_MAP[htmlLang];
const url = new URL(location.href);
const l = (url.searchParams.get('l') || '').toLowerCase();
if (l) return l;
const config = document.querySelector('#application_config');
const dataConfig = config?.getAttribute('data-config') || '';
if (dataConfig.includes('"LANGUAGE":"schinese"')) return 'schinese';
return 'schinese';
}
function isSimplifiedChinesePage() {
const htmlLang = (document.documentElement.lang || '').toLowerCase();
const url = new URL(location.href);
const l = (url.searchParams.get('l') || '').toLowerCase();
return htmlLang === 'zh-cn' || l === 'schinese' || l === '';
}
async function fetchReviewSummary(appId, language) {
const url = new URL(`https://store.steampowered.com/appreviews/${appId}`);
url.searchParams.set('json', '1');
url.searchParams.set('language', language);
url.searchParams.set('filter', 'all');
url.searchParams.set('day_range', '365');
url.searchParams.set('review_type', 'all');
url.searchParams.set('purchase_type', 'all');
url.searchParams.set('num_per_page', '1');
url.searchParams.set('cursor', '*');
const res = await fetch(url.toString(), { credentials: 'same-origin' });
if (!res.ok) {
throw new Error(`HTTP ${res.status} when fetching ${language}`);
}
const data = await res.json();
if (!data || data.success !== 1 || !data.query_summary) {
throw new Error(`Invalid appreviews response for ${language}`);
}
const qs = data.query_summary;
return {
totalReviews: Number(qs.total_reviews || 0),
totalPositive: Number(qs.total_positive || 0),
totalNegative: Number(qs.total_negative || 0),
scoreDesc: qs.review_score_desc || ''
};
}
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${RESULT_ID} {
display: block;
margin: 10px 0 14px 0;
padding: 12px 14px;
border-left: 3px solid #66c0f4;
background: linear-gradient(90deg, rgba(102,192,244,0.16), rgba(102,192,244,0.08));
color: #c7d5e0;
font-size: 14px;
line-height: 1.55;
border-radius: 3px;
box-sizing: border-box;
width: 100%;
clear: both;
}
#${RESULT_ID} .tm-title {
display: block;
font-weight: 700;
color: #ffffff;
}
#${RESULT_ID} .tm-sub {
display: block;
margin-top: 4px;
color: #8f98a0;
font-size: 12px;
}
`;
document.head.appendChild(style);
}
function ensureBox() {
ensureStyle();
let box = document.getElementById(RESULT_ID);
if (!box) {
box = document.createElement('div');
box.id = RESULT_ID;
}
return box;
}
function findStableMountTarget() {
// 目标位置:插到“筛选条件/您的语言”这一块后面
const activeFilters = document.querySelector('.reviews_info_ctn #reviews_active_filters');
if (activeFilters && activeFilters.isConnected) {
return { type: 'after', node: activeFilters };
}
// 备选:插到整个 reviews_info_ctn 末尾最前面
const reviewsInfo = document.querySelector('.reviews_info_ctn');
if (reviewsInfo && reviewsInfo.isConnected) {
return { type: 'prepend', node: reviewsInfo };
}
// 再备选:插到评测筛选栏前面
const filterOptions = document.querySelector('#reviews_filter_options.user_reviews_filter_options');
if (filterOptions && filterOptions.isConnected) {
return { type: 'before', node: filterOptions };
}
// 最后兜底:整个顾客评测区
const reviewSection =
document.querySelector('#reviewSettingsPopupCtn')?.parentElement ||
document.querySelector('.user_reviews');
if (reviewSection && reviewSection.isConnected) {
return { type: 'prepend', node: reviewSection };
}
return null;
}
function isCorrectlyMounted(box, mount) {
if (!box.isConnected || !mount?.node?.isConnected) return false;
if (mount.type === 'after') {
return box.previousElementSibling === mount.node;
}
if (mount.type === 'before') {
return box.nextElementSibling === mount.node;
}
if (mount.type === 'prepend') {
return box.parentElement === mount.node && mount.node.firstElementChild === box;
}
return false;
}
function renderCachedResult() {
if (!cachedResult) return false;
const mount = findStableMountTarget();
if (!mount) {
log('Stable mount target not found yet.');
return false;
}
const box = ensureBox();
box.innerHTML = `
<span class="tm-title">${cachedResult.message}</span>
<span class="tm-sub">${cachedResult.subtext}</span>
`;
if (!isCorrectlyMounted(box, mount)) {
box.remove();
if (mount.type === 'after') {
mount.node.insertAdjacentElement('afterend', box);
} else if (mount.type === 'before') {
mount.node.insertAdjacentElement('beforebegin', box);
} else if (mount.type === 'prepend') {
mount.node.insertAdjacentElement('afterbegin', box);
}
log('Mounted box at stable target:', mount);
}
return true;
}
function scheduleRender() {
if (renderScheduled) return;
renderScheduled = true;
requestAnimationFrame(() => {
renderScheduled = false;
try {
renderCachedResult();
} catch (err) {
console.error('[Steam Other Lang Review]', err);
}
});
}
function startObserver() {
if (observer || !document.body) return;
observer = new MutationObserver(() => {
if (dataLoaded) {
scheduleRender();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
async function computeResult() {
const appId = getAppId();
if (!appId) {
throw new Error('App ID not found');
}
const currentLanguage = getSteamLanguage();
const [allSummary, currentSummary] = await Promise.all([
fetchReviewSummary(appId, 'all'),
fetchReviewSummary(appId, currentLanguage)
]);
const otherReviews = allSummary.totalReviews - currentSummary.totalReviews;
const otherPositive = allSummary.totalPositive - currentSummary.totalPositive;
if (otherReviews <= 0) {
return {
message: '没有可用于计算的“其他语言”评测。',
subtext: `全部评测 ${allSummary.totalReviews.toLocaleString('zh-CN')},当前语言评测 ${currentSummary.totalReviews.toLocaleString('zh-CN')}`
};
}
const otherPercent = Math.round((otherPositive / otherReviews) * 100);
return {
message: `其他语言的 ${otherReviews.toLocaleString('zh-CN')} 篇用户评测中约有 ${otherPercent}% 为好评。`,
subtext: `当前语言:${currentSummary.totalReviews.toLocaleString('zh-CN')} 篇;所有语言:${allSummary.totalReviews.toLocaleString('zh-CN')} 篇`
};
}
async function main() {
if (!isSimplifiedChinesePage()) {
log('Not a simplified Chinese page, skip.');
return;
}
try {
startObserver();
cachedResult = await computeResult();
dataLoaded = true;
log('Computed result:', cachedResult);
scheduleRender();
let tries = 0;
const timer = setInterval(() => {
tries += 1;
const ok = renderCachedResult();
if (ok || tries >= 20) {
clearInterval(timer);
if (!ok) {
log('Render retries exhausted.');
}
}
}, 500);
} catch (err) {
console.error('[Steam Other Lang Review]', err);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main, { once: true });
} else {
main();
}
window.addEventListener('load', () => {
if (dataLoaded) scheduleRender();
}, { once: true });
})();