// ==UserScript==
// @name unfaird IPA Decrypt Service — 简体中文 i18n
// @namespace https://github.com/luestr/userscripts
// @version 1.1.1
// @description 将 decrypt.34306.lol 界面翻译为简体中文,并将 App Store 区域默认选为「中国」
// @author 可莉
// @homepageURL https://t.me/ibilibili
// @updateURL https://raw.githubusercontent.com/luestr/userscripts/main/i18n/decrypt.34306.lol-zh-CN.js
// @downloadURL https://raw.githubusercontent.com/luestr/userscripts/main/i18n/decrypt.34306.lol-zh-CN.js
// @match https://decrypt.34306.lol/*
// @run-at document-start
// @grant none
// @noframes
// ==/UserScript==
(function () {
'use strict';
/* ------------------------------------------------------------------
* 目标:让页面直接以简体中文呈现。
* 站点是纯静态 SPA,所有文案都通过 innerHTML / textContent 动态写入,
* 且每个字符串都带越南语特有字符(ă â đ ê ô ơ ư 及各种声调符号)。
* 因此这里采用「全量字符串映射 + 文本节点翻译」的方式:
* 1. 用 MutationObserver 监听 DOM 增删,节点一出现就翻译;
* 2. 用 Object.defineProperty 接管 placeholder / title / value 等属性;
* 3. 拦截 alert(),让错误提示也是中文;
* 4. 不提供任何语言切换界面,也不保留原文回退入口。
*
* 另含一项功能增强:App Store 区域默认选中「中国」(站点原本默认越南)。
* ---------------------------------------------------------------- */
// ========== 越南语 → 简体中文 映射表 ==========
const DICT = [
// ---- 页面标题 / 品牌 ----
['unfaird', 'unfaird'],
['IPA Decrypt Service', 'IPA 解密服务'],
['IPA DECRYPT SERVICE', 'IPA 解密服务'],
// ---- 登录 / 注册弹窗 ----
['Đăng nhập để tải app mới. App có sẵn thì tải không cần đăng nhập.',
'登录后才能下载新应用;已缓存的应用无需登录即可下载。'],
['Đăng nhập', '登录'],
['Đăng ký', '注册'],
['Tên của bạn', '你的昵称'],
['Mật khẩu', '密码'],
['Chưa có tài khoản? Đăng ký', '还没有账号?立即注册'],
['Đã có tài khoản? Đăng nhập', '已有账号?去登录'],
['Đăng ký xong, đăng nhập nhé', '注册成功,请登录'],
['Về trang chủ', '返回首页'],
// ---- 账户栏 ----
['🔓 Mở · tải không giới hạn', '🔓 开放 · 不限下载'],
['🔓 Mở · không giới hạn', '🔓 开放 · 无限制'],
['chờ duyệt', '待审核'],
['Thoát', '退出登录'],
['Admin', '管理'],
// ---- 搜索区 ----
['App Name', '应用名称'],
['Bundle ID', '应用包名'],
['Search iOS apps...', '搜索 iOS 应用…'],
['Search Apple TV (tvOS) apps...', '搜索 Apple TV (tvOS) 应用…'],
['Search Vision Pro (visionOS) apps...', '搜索 Vision Pro (visionOS) 应用…'],
['com.example.appname', 'com.example.appname'],
['Search', '搜索'],
['Decrypt', '解密'],
['App Store region', 'App Store 区域'],
['Platform', '平台'],
['Vietnam', '越南'],
['China', '中国'],
['Japan', '日本'],
['USA', '美国'],
['iPhone / iPad', 'iPhone / iPad'],
['Apple TV · tvOS', 'Apple TV · tvOS'],
['Vision Pro · visionOS', 'Vision Pro · visionOS'],
// ---- 列表 / 卡片状态 ----
['Recently Decrypted', '最近解密'],
['Top Free Apps', '热门免费应用'],
['Purchased', '已购买'],
['cached:', '已缓存:'],
['Download', '下载'],
['Processing', '处理中'],
['Versions', '版本'],
['Cached', '缓存'],
['▾ Versions', '▾ 版本'],
['▲ Versions', '▲ 版本'],
['▾ Cached', '▾ 缓存'],
['▲ Cached', '▲ 缓存'],
['↻ Latest v', '↻ 最新 v'],
['Searching...', '搜索中…'],
['Search failed', '搜索失败'],
['No free apps found', '未找到免费应用'],
['Looking up ', '正在查询 '],
['Decrypt bản mới nhất', '解密商店最新版本'],
['Tải + decrypt bản mới nhất trên store', '下载并解密商店最新版本'],
['Decrypted Mach-O binaries only', '仅解密后的 Mach-O 二进制文件'],
['Decrypt bản mới nhất', '解密最新版本'],
['Unknown', '未知'],
// ---- 包名输入校验提示 ----
['Đây là một đường link', '这看起来是一个链接'],
['Đây có vẻ là tên app', '这似乎是应用名称'],
['Sai định dạng Bundle ID', 'Bundle ID 格式错误'],
['Ô này chỉ nhận', '该输入框只接受'],
['(vd:', '(例如:'],
['Muốn tìm theo', '想按'],
['tên app', '应用名称'],
['→ bấm tab', '→ 请点击上方'],
['ở trên.', '标签页。'],
['Bundle ID', 'Bundle ID'],
['App Name', '应用名称'],
// ---- 任务面板 ----
['Completed', '已完成'],
['Failed', '失败'],
['downloading', '下载中'],
['purchasing', '购买中'],
['decrypting', '解密中'],
['waiting_decrypt', '等待解密'],
['waiting decrypt', '等待解密'],
['Waiting', '等待中'],
['queued', '排队中'],
['pending', '等待中'],
['ready', '就绪'],
['Processing', '处理中'],
// ---- 队列 ----
['Queue', '队列'],
['workers', '个线程'],
['waiting decrypt', '等待解密'],
// ---- 版本选择面板 ----
['version trên store · chọn bản để decrypt', '个商店版本 · 选择要解密的版本'],
['version đã decrypt · tải ngay, không decrypt lại', '个已解密版本 · 可直接下载,无需重新解密'],
['Đang lấy danh sách version…', '正在获取版本列表…'],
['Đang tải cache…', '正在加载缓存…'],
['Không lấy được version cho app này (cần đăng nhập account vùng này / app không có trên store).',
'无法获取该应用的版本列表(需要登录该区域账号,或该应用不在此商店)。'],
['Chưa có version nào của app này trong cache.', '该应用暂无已缓存的版本。'],
['Xem thêm version cũ hơn', '查看更多历史版本'],
['Đang tải…', '加载中…'],
['latest', '最新'],
['đã có', '已缓存'],
// ---- 暂停横幅 ----
['⏸ Đang tạm dừng nhận app mới (bảo trì/nâng cấp). Các app đang trong hàng đợi vẫn chạy.',
'⏸ 已暂停接收新的应用请求(维护/升级中)。队列中已有的应用仍会继续处理。'],
// ---- 捐赠栏 ----
['yeah i maintain this web for free, feel free to', '本站由我免费维护,欢迎'],
['buy me some token for claude code/codex', '请我喝杯咖啡(Claude Code / Codex 额度)'],
['☕', '☕'],
];
// ---- 精确匹配用(整串相等才翻译,避免误伤 app 名称/包名) ----
const EXACT = new Map();
// ---- 片段替换用(多处出现、可安全内联替换的词) ----
const FRAGMENT = new Map();
for (const [vi, zh] of DICT) {
// 含越南语字符的短词一律走精确匹配;长句同样精确匹配。
EXACT.set(vi, zh);
}
// 片段级替换:仅限不会与 app 名称/包名冲突的 UI 词。
// 顺序敏感:长片段务必排在短片段之前。
const FRAGMENTS = [
// ---- 包名校验提示(整块 HTML 是一个文本节点) ----
['Đây là một đường link', '这看起来是一个链接'],
['Đây có vẻ là tên app', '这似乎是应用名称'],
['Sai định dạng Bundle ID', 'Bundle ID 格式错误'],
['Ô này chỉ nhận Bundle ID (vd: ', '该输入框只接受 Bundle ID(例如:'],
[').
Muốn tìm theo tên app → bấm tab App Name ở trên.',
')。
想按 应用名称 搜索 → 请点击上方的 应用名称 标签页。'],
// ---- 账户栏剩余额度:còn {n}/{m} app hôm nay ----
['còn ', '剩余 '],
[' app hôm nay', ' 个应用额度(今日)'],
// ---- 队列标题:Queue (N workers) ----
['Queue (', '队列('],
[' workers)', ' 个线程)'],
// ---- 版本 / 缓存面板 ----
[' version trên store · chọn bản để decrypt', ' 个商店版本 · 选择要解密的版本'],
[' version đã decrypt · tải ngay, không decrypt lại', ' 个已解密版本 · 可直接下载,无需重新解密'],
['Đang lấy danh sách version…', '正在获取版本列表…'],
['Đang tải cache…', '正在加载缓存…'],
['Chưa có version nào của app này trong cache.', '该应用暂无已缓存的版本。'],
['Không lấy được version cho app này (cần đăng nhập account vùng này / app không có trên store).',
'无法获取该应用的版本列表(需要登录该区域账号,或该应用不在此商店)。'],
['Xem thêm version cũ hơn', '查看更多历史版本'],
['Đang tải…', '加载中…'],
];
// ---------- 词条查表 ----------
function translate(text) {
if (!text) return text;
const raw = text;
const trimmed = raw.trim();
if (!trimmed) return raw;
// 1) 整串精确匹配(保留首尾空白)
if (EXACT.has(trimmed)) {
const lead = raw.slice(0, raw.indexOf(trimmed));
const tail = raw.slice(raw.indexOf(trimmed) + trimmed.length);
return lead + EXACT.get(trimmed) + tail;
}
let out = raw;
let changed = false;
// 1.5) 前置 emoji / 旗帜符号:如 "🇻🇳 Vietnam"、"📱 iPhone / iPad"
// 拆出前缀符号,仅翻译其后的实体文本。
const m = trimmed.match(/^([\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}\u{200D}\u{1F1E6}-\u{1F1FF}]+\s*)(.+)$/u);
if (m && EXACT.has(m[2].trim())) {
const rest = m[2].trim();
const newRest = EXACT.get(rest);
return out.replace(rest, newRest);
}
// 2) 片段替换
for (const [vi, zh] of FRAGMENTS) {
if (out.indexOf(vi) !== -1) {
out = out.split(vi).join(zh);
changed = true;
}
}
if (changed) return out;
// 3) 前缀型(如 "Looking up com.xxx..." / "↻ Latest v1.2.3")
if (trimmed.startsWith('Looking up ')) {
return raw.replace('Looking up ', '正在查询 ');
}
if (/^[▾▴↻]\s*Latest v/.test(trimmed)) {
return raw.replace('Latest v', '最新 v');
}
return raw;
}
// ---------- 判定节点是否属于用户可控内容(不翻译) ----------
function skipNode(node) {
const p = node.parentNode;
if (!p || !p.tagName) return false;
const tag = p.tagName.toUpperCase();
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'CODE' || tag === 'PRE') return true;
// 包名 / 应用名 / 版本号等动态内容不要动:
// 交由 translate() 的精确匹配保证,不在这里粗暴跳过。
return false;
}
// ---------- 翻译单个文本节点 ----------
function translateTextNode(node) {
if (skipNode(node)) return;
const data = node.nodeValue;
if (!data) return;
const next = translate(data);
if (next !== data) node.nodeValue = next;
}
// ---------- 翻译元素属性 ----------
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'value'];
function translateElementAttrs(el) {
if (!el || el.nodeType !== 1) return;
for (const a of ATTRS) {
if (!el.hasAttribute || !el.hasAttribute(a)) continue;
const cur = el.getAttribute(a);
const next = translate(cur);
if (next !== cur) el.setAttribute(a, next);
}
}
// ---------- 遍历子树 ----------
function walk(root) {
if (!root) return;
if (root.nodeType === 3) { // 文本节点
translateTextNode(root);
return;
}
if (root.nodeType !== 1 && root.nodeType !== 9 && root.nodeType !== 11) return;
if (root.nodeType === 1) translateElementAttrs(root);
// 用 TreeWalker 但避开 script/style
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {
acceptNode(n) {
if (n.nodeType === 1) {
const t = n.tagName.toUpperCase();
if (t === 'SCRIPT' || t === 'STYLE' || t === 'CODE' || t === 'PRE') {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
let n;
while ((n = walker.nextNode())) {
if (n.nodeType === 3) translateTextNode(n);
else translateElementAttrs(n);
}
// 根节点自身文本(TreeWalker 从根的子节点开始)
if (root.nodeType === 1 && root.firstChild === null) translateTextNode(root);
}
// ---------- alert 拦截 ----------
const nativeAlert = window.alert;
window.alert = function (msg) {
return nativeAlert.call(window, translate(String(msg == null ? '' : msg)));
};
// ---------- MutationObserver ----------
function startObserver() {
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'characterData') {
translateTextNode(m.target);
continue;
}
for (const node of m.addedNodes) {
if (node.nodeType === 3) translateTextNode(node);
else if (node.nodeType === 1) walk(node);
}
if (m.type === 'attributes' && m.target && m.target.nodeType === 1) {
translateElementAttrs(m.target);
}
}
});
obs.observe(document.documentElement || document, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ATTRS,
});
return obs;
}
// ========== 新增功能:App Store 区域默认选中「中国」 ==========
// 站点内联脚本里写死 `let region='vn'`(默认越南),HTML 中 #region 也默认选中越南,
// 且区域不做任何持久化,因此每次加载都会回到越南。
// 这里在站点脚本就绪后把 #region 切到中国,并派发 change 事件,
// 由站点自身的 onRegionChange() 把内部 region 变量同步为 'cn'。
const DEFAULT_REGION = 'cn'; // 目标默认区域:中国
const SITE_DEFAULT_REGION = 'vn'; // 站点原始默认区域:越南
function applyDefaultRegion() {
const sel = document.getElementById('region');
if (!sel) return false;
if (sel.dataset.zhRegionApplied === '1') return true; // 只处理一次
sel.dataset.zhRegionApplied = '1';
// 仅当仍处于站点默认值时才切换,避免覆盖用户手动选择过的区域
if (sel.value !== SITE_DEFAULT_REGION) return true;
sel.value = DEFAULT_REGION;
// 触发站点 onchange="onRegionChange()",同步其内部 region 变量
try { sel.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
// onRegionChange() 内部会调用 _clearView() 清空列表,这里补回一次首屏列表
if (typeof window.loadFeatured === 'function') {
try { window.loadFeatured(); } catch (e) {}
}
return true;
}
// 站点内联脚本位于 body 末尾,需等 onRegionChange 定义后(脚本执行完)再切换,
// 这样派发的 change 事件才能被站点正确处理。
function bootDefaultRegion() {
let tries = 0;
const timer = setInterval(() => {
const ready = document.getElementById('region') &&
typeof window.onRegionChange === 'function';
if (ready) {
applyDefaultRegion();
clearInterval(timer);
} else if (++tries >= 300) { // 约 3s 兜底
applyDefaultRegion();
clearInterval(timer);
}
}, 10);
}
// ---------- 首屏处理 ----------
function boot() {
const root = document.documentElement || document;
walk(root);
startObserver();
bootDefaultRegion(); // 新增:把 App Store 区域默认切到「中国」
// 站点会在极短时间内多次重绘,补几次全量扫描兜底
let ticks = 0;
const t = setInterval(() => {
walk(document.documentElement || document);
if (++ticks >= 20) clearInterval(t);
}, 150);
}
if (document.documentElement) boot();
else document.addEventListener('DOMContentLoaded', boot, { once: true });
window.addEventListener('load', () => walk(document.documentElement || document));
})();