// ==UserScript==
// @name xivanalysis-zh
// @name:zh xivanalysis 中文补全
// @namespace http://tanimodori.com/
// @version 0.0.3
// @description Fill in the missing Chinese translations for xivanalysis
// @description:zh 为 xivanalysis 填补缺失的中文翻译
// @author Tanimodori
// @match https://xivanalysis.com/*
// @include https://xivanalysis.com/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
"use strict";
const origFetch = window.fetch;
const injectFetch = (injector) => {
window.fetch = async (...args) => {
let response = await origFetch(...args);
try {
const pkg = {
url: args[0].toString(),
response,
json: await response.clone().json()
};
response = await injector(pkg);
} catch (err) {
console.error(err);
}
return response;
};
};
const injectedStyle = "span.alternative {\r\n background-color: rgba(255, 255, 255, 0.1);\r\n cursor: pointer;\r\n text-decoration: underline;\r\n}\r\n\r\nspan.highlight {\r\n color: rgba(255, 123, 26, 1);\r\n}\r\n\r\nspan.highlight-yellow {\r\n color: rgba(255, 255, 102, 1);\r\n}\r\n\r\nspan.highlight-green {\r\n color: rgba(0, 204, 34, 1);\r\n}\r\n";
const incrementAltContainer = (container, increment) => {
const alts = container.querySelectorAll("span.alternative");
const length = alts.length;
let index = parseInt(container.style.getPropertyValue("--display-nth"));
if (isNaN(index)) {
index = increment;
} else {
index = (index + increment) % length;
}
container.style.setProperty("--display-nth", index.toString());
for (let i = 0; i < length; i++) {
const alt = alts[i];
if (alt instanceof HTMLElement) {
alt.style.display = i === index ? "inline" : "none";
}
}
};
const injectWindowElement = (node) => {
const applyToContainers = () => {
const altContainers = node.querySelectorAll("div span.alternative-container");
for (const altContainer of altContainers) {
incrementAltContainer(altContainer, 0);
}
};
applyToContainers();
const observer = new MutationObserver(applyToContainers);
observer.observe(node, { attributes: true, attributeFilter: ["class"], childList: true, subtree: true });
node.addEventListener("click", (event) => {
let target = event.target;
while (target instanceof HTMLElement) {
if (target.tagName.toLowerCase() === "span" && target.classList.contains("alternative")) {
const container = target.parentElement;
incrementAltContainer(container, 1);
break;
}
target = target.parentElement;
}
});
};
const injectWindow = () => {
window.addEventListener("load", () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length === 0) {
return;
}
const node = mutation.addedNodes[0];
if (node.tagName.toLowerCase() === "div" && node.id.toLowerCase() !== "root" && node.style.position.toLowerCase() === "absolute" && node.style.top.toLowerCase() === "0px" && node.style.left.toLowerCase() === "0px") {
injectWindowElement(node);
}
});
});
observer.observe(document.body, { attributes: true, childList: true, subtree: false });
});
};
const injectCss = () => {
const styleSheet = document.createElement("style");
styleSheet.setAttribute("type", "text/css");
styleSheet.innerHTML = injectedStyle;
document.head.appendChild(styleSheet);
};
const injectStyle = () => {
injectCss();
injectWindow();
};
const actionCatagoryPolyfill = {
1: "自动攻击",
2: "魔法",
3: "战技",
4: "能力",
5: "道具",
6: "采集能力",
7: "制作能力",
8: "任务",
9: "极限技",
10: "系统",
11: "系统",
12: "坐骑",
13: "特殊技能",
14: "道具操作",
15: "极限技",
16: "",
17: "弩炮"
};
const addonTextPolyfill = {
699: "即时",
701: "咏唱时间",
702: "复唱时间",
703: "复唱工次",
704: "消耗体力",
705: "消耗魔力",
706: "消耗技力",
707: "消耗制作力",
708: "消耗采集力",
709: "距离",
710: "范围",
711: "习得条件:",
712: "适应职业:"
};
const classJobPolyfill = {
0: ["冒险者", "ADV", ""],
1: ["剑术师", "GLA", "剑"],
2: ["格斗家", "PGL", "格"],
3: ["斧术师", "MRD", "斧"],
4: ["枪术师", "LNC", "枪"],
5: ["弓箭手", "ARC", "弓"],
6: ["幻术师", "CNJ", "幻"],
7: ["咒术师", "THM", "咒"],
8: ["刻木匠", "CRP", "木"],
9: ["锻铁匠", "BSM", "锻"],
10: ["铸甲匠", "ARM", "甲"],
11: ["雕金匠", "GSM", "雕"],
12: ["制革匠", "LTW", "革"],
13: ["裁衣匠", "WVR", "裁"],
14: ["炼金术士", "ALC", "炼"],
15: ["烹调师", "CUL", "烹"],
16: ["采矿工", "MIN", "矿"],
17: ["园艺工", "BTN", "园"],
18: ["捕鱼人", "FSH", "鱼"],
19: ["骑士", "PLD", "骑"],
20: ["武僧", "MNK", "僧"],
21: ["战士", "WAR", "战"],
22: ["龙骑士", "DRG", "龙"],
23: ["吟游诗人", "BRD", "诗"],
24: ["白魔法师", "WHM", "白"],
25: ["黑魔法师", "BLM", "黑"],
26: ["秘术师", "ACN", "秘"],
27: ["召唤师", "SMN", "召"],
28: ["学者", "SCH", "学"],
29: ["双剑师", "ROG", "双"],
30: ["忍者", "NIN", "忍"],
31: ["机工士", "MCH", "机"],
32: ["暗黑骑士", "DRK", "暗"],
33: ["占星术士", "AST", "占"],
34: ["武士", "SAM", "武"],
35: ["赤魔法师", "RDM", "赤"],
36: ["青魔法师", "BLU", "青"],
37: ["绝枪战士", "GNB", "绝"],
38: ["舞者", "DNC", "舞"],
39: ["钐镰客", "RPR", "钐"],
40: ["贤者", "SGE", "贤"],
41: ["蝰蛇剑士", "VPR", "蛇"],
42: ["绘灵法师", "PCT", "绘"]
};
const classJobCategoryPolyfill = {
0: "",
1: "所有职业",
2: "剑术师",
3: "格斗家",
4: "斧术师",
5: "枪术师",
6: "弓箭手",
7: "幻术师",
8: "咒术师",
9: "刻木匠",
10: "锻铁匠",
11: "铸甲匠",
12: "雕金匠",
13: "制革匠",
14: "裁衣匠",
15: "炼金术士",
16: "烹调师",
17: "采矿工",
18: "园艺工",
19: "捕鱼人",
20: "骑士",
21: "武僧",
22: "战士",
23: "龙骑士",
24: "吟游诗人",
25: "白魔法师",
26: "黑魔法师",
27: "秘术师",
28: "召唤师",
29: "学者",
30: "战斗精英",
31: "魔法导师",
32: "大地使者",
33: "能工巧匠",
34: "战斗精英 魔法导师",
35: "能工巧匠 大地使者",
36: "剑术师之外的战斗精英",
37: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 骑士 战士 暗黑骑士",
38: "剑术师 骑士",
39: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 骑士 战士 暗黑骑士",
40: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 武僧 战士 龙骑士 吟游诗人 忍者",
41: "格斗家 武僧",
42: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 武僧 战士 龙骑士 吟游诗人 忍者",
43: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 骑士 武僧 战士 龙骑士 暗黑骑士",
44: "斧术师 战士",
45: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 骑士 武僧 战士 龙骑士 暗黑骑士",
46: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 武僧 龙骑士 吟游诗人 忍者 机工士",
47: "枪术师 龙骑士",
48: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 武僧 龙骑士 吟游诗人 忍者 机工士",
49: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 吟游诗人 黑魔法师 召唤师 机工士",
50: "弓箭手 吟游诗人",
51: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 吟游诗人 机工士",
52: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 骑士 白魔法师 学者 占星术士",
53: "幻术师 白魔法师",
54: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 白魔法师 黑魔法师 召唤师 学者 占星术士",
55: "咒术师 黑魔法师",
56: "剑术师 幻术师 咒术师 骑士 白魔法师 黑魔法师",
57: "剑术师 咒术师 骑士 黑魔法师",
58: "剑术师 幻术师 骑士 白魔法师",
59: "剑术师 斧术师 骑士 战士 暗黑骑士 绝枪战士",
60: "剑术师 斧术师 枪术师 骑士 战士 龙骑士 暗黑骑士 绝枪战士 钐镰客",
61: "幻术师 咒术师 秘术师 白魔法师 学者 占星术士",
62: "幻术师 咒术师 秘术师 白魔法师 黑魔法师 召唤师 学者 占星术士",
63: "咒术师 秘术师 黑魔法师 召唤师 赤魔法师 青魔法师 绘灵法师",
64: "幻术师 白魔法师 学者 占星术士 贤者",
65: "格斗家 武僧 武士",
66: "弓箭手 吟游诗人 机工士 舞者",
67: "剑术师 格斗家 斧术师 枪术师 双剑师 武僧 龙骑士 忍者",
68: "秘术师 召唤师 学者",
69: "秘术师 召唤师",
70: "烹调师之外的能工巧匠",
71: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 白魔法师 黑魔法师 召唤师 学者",
72: "幻术师 咒术师 秘术师 白魔法师 黑魔法师 召唤师 学者",
73: "幻术师 白魔法师 学者 占星术士 贤者",
74: "",
75: "",
76: "枪术师 龙骑士 钐镰客",
77: "",
78: "",
79: "",
80: "",
81: "",
82: "",
83: "",
84: "格斗家 枪术师 武僧 龙骑士 武士 钐镰客",
85: "战斗精英 魔法导师 特职专用",
86: "骑士 战士 暗黑骑士 绝枪战士 武僧 龙骑士 忍者 武士",
87: "吟游诗人 机工士 舞者 黑魔法师 召唤师 赤魔法师 白魔法师 学者 占星术士",
88: "剑术师 斧术师 格斗家 枪术师 弓箭手 双剑师 骑士 武僧 战士 龙骑士 吟游诗人 忍者 暗黑骑士 机工士",
89: "黑魔法师 召唤师 赤魔法师",
90: "弓箭手 幻术师 咒术师 秘术师 白魔法师 吟游诗人 黑魔法师 召唤师 学者 机工士 占星术士",
91: "双剑师",
92: "忍者",
93: "双剑师 忍者",
94: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 忍者",
95: "剑术师 格斗家 斧术师 枪术师 双剑师 忍者",
96: "机工士",
97: "格斗家 枪术师 弓箭手 双剑师 武僧 龙骑士 吟游诗人 忍者 机工士",
98: "暗黑骑士",
99: "占星术士",
100: "弓箭手 双剑师 吟游诗人 忍者 机工士",
101: "格斗家 枪术师 双剑师 武僧 龙骑士 忍者",
102: "格斗家 双剑师 武僧 忍者 武士 蝰蛇剑士",
103: "双剑师 忍者 蝰蛇剑士",
104: "",
105: "弓箭手 双剑师 吟游诗人 忍者 机工士 舞者 蝰蛇剑士",
106: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 吟游诗人",
107: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 暗黑骑士 占星术士",
108: "战斗精英 魔法导师",
109: "",
110: "战斗精英 魔法导师 特职专用",
111: "武士",
112: "赤魔法师",
113: "剑术师 斧术师 骑士 战士 暗黑骑士 绝枪战士",
114: "格斗家 枪术师 武僧 龙骑士 双剑师 忍者 武士 钐镰客 蝰蛇剑士",
115: "弓箭手 吟游诗人 机工士 舞者",
116: "咒术师 黑魔法师 秘术师 召唤师 赤魔法师 青魔法师 绘灵法师",
117: "幻术师 白魔法师 学者 占星术士 贤者",
118: "格斗家 枪术师 弓箭手 武僧 龙骑士 吟游诗人 双剑师 忍者 机工士 武士 舞者 钐镰客 蝰蛇剑士",
119: "格斗家 枪术师 咒术师 武僧 龙骑士 黑魔法师 秘术师 召唤师 双剑师 忍者 武士 赤魔法师 青魔法师 钐镰客 蝰蛇剑士 绘灵法师",
120: "幻术师 咒术师 白魔法师 黑魔法师 秘术师 召唤师 学者 占星术士 赤魔法师 青魔法师 贤者 绘灵法师",
121: "骑士 战士 暗黑骑士 绝枪战士",
122: "武僧 龙骑士 忍者 武士 钐镰客 蝰蛇剑士",
123: "吟游诗人 机工士 舞者",
124: "黑魔法师 召唤师 赤魔法师 青魔法师 绘灵法师",
125: "白魔法师 学者 占星术士 贤者",
126: "武僧 龙骑士 吟游诗人 忍者 机工士 武士 舞者 钐镰客 蝰蛇剑士",
127: "武僧 龙骑士 黑魔法师 召唤师 忍者 武士 赤魔法师 青魔法师 钐镰客 蝰蛇剑士 绘灵法师",
128: "白魔法师 黑魔法师 召唤师 学者 占星术士 赤魔法师 青魔法师 贤者 绘灵法师",
129: "青魔法师",
130: "所有(除设限特职)",
131: "武僧 龙骑士 吟游诗人 黑魔法师 召唤师 忍者 机工士 武士 赤魔法师 舞者 钐镰客 蝰蛇剑士 绘灵法师",
132: "武僧 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 占星术士 武士 赤魔法师 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
133: "白魔法师 学者 占星术士 贤者",
134: "骑士 战士 暗黑骑士 绝枪战士",
135: "骑士 武僧 战士 龙骑士 吟游诗人 黑魔法师 召唤师 忍者 机工士 暗黑骑士 武士 赤魔法师 绝枪战士 舞者 钐镰客 蝰蛇剑士 绘灵法师",
136: "骑士 战士 白魔法师 学者 暗黑骑士 占星术士 绝枪战士 贤者",
137: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
138: "骑士 武僧 战士 龙骑士 忍者 暗黑骑士 武士 绝枪战士 钐镰客 蝰蛇剑士",
139: "吟游诗人 机工士 舞者",
140: "白魔法师 黑魔法师 召唤师 学者 占星术士 赤魔法师 贤者 绘灵法师",
141: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
142: "战斗精英和魔法导师(除设限特职)",
143: "战斗精英(除设限特职)",
144: "魔法导师(除设限特职)",
145: "骑士 武僧 战士 龙骑士 吟游诗人 忍者 机工士 暗黑骑士 武士 绝枪战士 舞者 钐镰客 蝰蛇剑士",
146: "战斗精英 魔法导师 特职专用(除设限特职)",
147: "黑魔法师 召唤师 赤魔法师 绘灵法师",
148: "武僧 龙骑士 忍者 武士 钐镰客 蝰蛇剑士",
149: "绝枪战士",
150: "舞者",
151: "锻铁匠、铸甲匠、雕金匠",
152: "刻木匠、制革匠、裁衣匠",
153: "炼金术士、烹调师",
154: "采矿工、园艺工",
155: "捕鱼人",
156: "防护职业(设限特职除外)",
157: "治疗职业(设限特职除外)",
158: "物理进攻职业(设限特职除外)",
159: "魔法进攻职业(设限特职除外)",
160: "秘术师 学者",
161: "剑术师 格斗家 斧术师 枪术师 弓箭手 骑士 武僧 战士 龙骑士 吟游诗人 双剑师 忍者 机工士 暗黑骑士 武士 绝枪战士 舞者 钐镰客 蝰蛇剑士",
162: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 <hex:02100103>忍者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者",
163: "武僧 龙骑士 吟游诗人 黑魔法师 召唤师 忍者 机工士 武士 赤魔法师 舞者 钐镰客 蝰蛇剑士 绘灵法师",
164: "武僧 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 占星术士 武士 赤魔法师 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
165: "白魔法师 学者 占星术士 贤者",
166: "骑士 战士 暗黑骑士 绝枪战士",
167: "骑士 武僧 战士 龙骑士 吟游诗人 黑魔法师 召唤师 忍者 机工士 暗黑骑士 武士 赤魔法师 绝枪战士 舞者 钐镰客 蝰蛇剑士 绘灵法师",
168: "骑士 战士 白魔法师 学者 暗黑骑士 占星术士 绝枪战士 贤者",
169: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
170: "骑士 武僧 战士 龙骑士 忍者 暗黑骑士 武士 绝枪战士 钐镰客 蝰蛇剑士",
171: "吟游诗人 机工士 舞者",
172: "白魔法师 黑魔法师 召唤师 学者 占星术士 赤魔法师 贤者 绘灵法师",
173: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
174: "骑士 武僧 战士 龙骑士 吟游诗人 忍者 机工士 暗黑骑士 武士 绝枪战士 舞者 钐镰客 蝰蛇剑士",
175: "黑魔法师 召唤师 赤魔法师 绘灵法师",
176: "武僧 龙骑士 忍者 武士 钐镰客 蝰蛇剑士",
177: "武僧 龙骑士 吟游诗人 忍者 机工士 武士 舞者 钐镰客 蝰蛇剑士",
178: "骑士 战士 黑魔法师 召唤师 暗黑骑士 赤魔法师 绝枪战士 绘灵法师",
179: "白魔法师 召唤师 学者 占星术士 赤魔法师 贤者 绘灵法师",
180: "钐镰客",
181: "贤者",
182: "",
183: "",
184: "",
185: "",
186: "防护职业(设限特职除外)",
187: "治疗职业(设限特职除外)",
188: "近战职业(设限特职除外)",
189: "远程物理进攻职业(设限特职除外)",
190: "远程魔法进攻职业(设限特职除外)",
191: "骑士 武僧 战士 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 暗黑骑士 占星术士 武士 赤魔法师 绝枪战士 舞者 钐镰客 贤者",
192: "战斗精英和魔法导师(除设限特职)",
193: "格斗家 枪术师 弓箭手 双剑师 幻术师 咒术师 秘术师 武僧 龙骑士 吟游诗人 白魔法师 黑魔法师 召唤师 学者 忍者 机工士 占星术士 武士 赤魔法师 舞者 钐镰客 贤者 蝰蛇剑士 绘灵法师",
194: "剑术师 斧术师 幻术师 骑士 战士 白魔法师 学者 暗黑骑士 占星术士 绝枪战士 贤者",
195: "剑术师 格斗家 斧术师 枪术师 弓箭手 双剑师 咒术师 秘术师 骑士 武僧 战士 龙骑士 吟游诗人 黑魔法师 召唤师 忍者 机工士 暗黑骑士 武士 赤魔法师 绝枪战士 舞者 钐镰客 蝰蛇剑士 绘灵法师",
196: "蝰蛇剑士",
197: "绘灵法师",
198: "咒术师 黑魔法师 秘术师 召唤师 赤魔法师 绘灵法师",
199: "幻术师 咒术师 白魔法师 黑魔法师 秘术师 召唤师 学者 占星术士 赤魔法师 贤者 绘灵法师"
};
const translateAddon = async (obj) => {
const id = obj.row_id;
obj.fields.Text = addonTextPolyfill[id];
return obj;
};
const useCache = (fetcher) => {
const cache = /* @__PURE__ */ new Map();
const fetch = (params) => {
if (cache.has(params)) {
return cache.get(params);
}
const promise = fetcher(params);
cache.set(params, promise);
return promise;
};
return {
cache,
fetch
};
};
const _fetchAction = async (id) => {
const response = await origFetch(`https://www.garlandtools.cn/db/doc/Action/chs/2/${id}.json`);
const { action } = await response.json();
return action;
};
const { fetch: fetchAction, cache: actionCache } = useCache(_fetchAction);
const translateAction = async (obj) => {
const id = obj.row_id;
const action = await fetchAction(id);
obj.fields.Name = action.name;
return obj;
};
const translateActionRich = async (obj) => {
const id = obj.row_id;
const action = await fetchAction(id);
obj.fields.Name = action.name;
if (obj.fields.ClassJob.value !== -1) {
obj.fields.ClassJob.fields.Abbreviation = classJobPolyfill[obj.fields.ClassJob.row_id][0];
} else {
if (obj.fields.ClassJob.fields === void 0) {
obj.fields.ClassJob.fields = {};
}
obj.fields.ClassJob.fields.Abbreviation = "";
}
obj.fields.ClassJobCategory.fields.Name = classJobCategoryPolyfill[obj.fields.ClassJobCategory.row_id];
obj.fields.ActionCategory.fields.Name = actionCatagoryPolyfill[obj.fields.ActionCategory.row_id];
obj.transient["Description@as(html)"] = action.description;
return obj;
};
const _fetchItem = async (id) => {
const response = await origFetch(`https://www.garlandtools.cn/db/doc/Item/chs/3/${id}.json`);
const { item } = await response.json();
return item;
};
const { fetch: fetchItem, cache: itemCache } = useCache(_fetchItem);
const translateItem = async (obj) => {
const id = obj.row_id;
const item = await fetchItem(id);
obj.fields.Name = item.name;
obj.fields["Description@as(html)"] = item.description;
return obj;
};
const _fetchSearch = async (text) => {
const response = await origFetch(
`https://www.garlandtools.cn/api/search.php?text=${encodeURIComponent(text)}&lang=en`
);
const items = await response.json();
return items;
};
const { fetch: fetchSearch, cache: searchCache } = useCache(_fetchSearch);
const _fetchStatus = async (id) => {
const response = await origFetch(`https://www.garlandtools.cn/db/doc/Status/chs/2/${id}.json`);
const { status } = await response.json();
return status;
};
const { fetch: fetchStatus, cache: statusCache } = useCache(_fetchStatus);
const translateStatus = async (obj) => {
const id = obj.row_id;
const status = await fetchStatus(id);
obj.fields.Name = status.name;
obj.fields["Description@as(html)"] = status.description;
return obj;
};
const _fetchTimeline = async (text) => {
let items = await fetchSearch(text);
items = items.filter((item) => {
if (item.obj.n.toLowerCase() !== text.toLowerCase()) {
return false;
}
if (item.type !== "action" && item.type !== "status" && item.type !== "item") {
return false;
}
return true;
});
const translations = /* @__PURE__ */ new Map();
for (const item of items) {
try {
let translatedText;
if (item.type === "action") {
const result = await fetchAction(item.id);
translatedText = result.name;
} else if (item.type === "status") {
const result = await fetchStatus(item.id);
translatedText = result.name;
} else {
const result = await fetchItem(item.id);
translatedText = result.name;
}
const count = translations.get(translatedText) || 0;
translations.set(translatedText, count + 1);
} catch (e) {
console.error(e);
}
}
let maxCount = 0;
let translation = text;
for (const [key, value] of translations.entries()) {
if (value > maxCount) {
maxCount = value;
translation = key;
}
}
return translation;
};
const { fetch: fetchTimeline, cache: timelineCache } = useCache(_fetchTimeline);
const timelineCacheInitials = [
// originally translated
["资源", "资源"],
["职业量谱", "职业量谱"],
["触发", "触发"],
// terms
["Raid Buffs", "团辅"],
["GCD", "GCD"],
// == AST ==
["Arcanum", "奥秘卡"],
// Neutral Sect/中间学派
// https://garlandtools.cn/db/#status/1892
["Neutral Sect (Healing Potency)", "中间学派(治疗增益)"],
// Neutral Sect/中间学派
// https://garlandtools.cn/db/#status/1921
["Neutral Sect (Barrier)", "中间学派(血盾)"],
// Wheel of Fortune/命运之轮
// https://garlandtools.cn/db/#status/1206
["Wheel Of Fortune", "命运之轮(HoT)"],
// Collective Unconscious (Mitigation)/命运之轮
// https://garlandtools.cn/db/#status/849
["Collective Unconscious (Mitigation)", "命运之轮(减伤)"],
// == WHM ==
// Confession/告解
// https://garlandtools.cn/db/#status/1219
["Confession", "告解"],
// == SCH ==
["Autos", "自动技能"],
["Commands", "手动技能"],
// Expedience/疾风之计
// https://garlandtools.cn/db/#status/2712
["Expedience", "疾风之计"],
// == DRK ==
["Esteem", "英雄的掠影"],
// == DRG ==
// Enhanced Piercing Talon/???(未实装)
// https://www.garlandtools.cn/db/#status/1870
// == SMN ==
// "Energy Drain/Siphon"/"能量吸收/抽取"
// https://garlandtools.cn/db/#action/16508
// https://garlandtools.cn/db/#action/16510
["Energy Drain/Siphon", "能量吸收/抽取"],
// Crimson Strike Ready/???(未实装)
// https://www.garlandtools.org/db/#status/4400
["Pet", "召唤兽"],
["Demi", "亚灵神"],
// == BRD ==
["Songs", "战歌"],
// == BLM ==
["Ley Lines Buffs", "黑魔纹增益"],
// == SAM ==
// Tengentsu/天眼通 (misspelled)
// https://www.garlandtools.cn/db/#status/3853
["Tengetsu", "天眼通"]
];
timelineCacheInitials.forEach(([text, translation]) => {
timelineCache.set(text, Promise.resolve(translation));
});
const translateTimeline = async (text) => {
return fetchTimeline(text);
};
const injectTimeline = () => {
const className = "Timeline-module_content";
const selector = `[class^="${className}"], [class*=" ${className}"]`;
const observer = new MutationObserver((mutations) => {
var _a;
const found = [];
for (const mutation of mutations) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement) {
if (node.matches(selector)) {
found.push(node);
} else {
const children = node.querySelectorAll(selector);
for (const child of children) {
if (child instanceof HTMLElement) {
found.push(child);
}
}
}
}
}
}
}
for (const node of found) {
const gridColumnStart = (_a = node.parentElement) == null ? void 0 : _a.style.gridColumnStart;
if (gridColumnStart === "-3") {
continue;
}
const text = node.textContent;
if (text) {
fetchTimeline(text).then((translation) => {
node.textContent = translation;
});
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
const iconCache = /* @__PURE__ */ new Map();
const fetchIcon = async (url, name) => {
const iconId = parseInt(url.match(/ui\/icon\/\d+\/(\d+)/)[1]);
if (isNaN(iconId)) {
return;
}
if (iconCache.has(iconId)) {
return iconCache.get(iconId);
}
const searchItems = await fetchSearch(name);
for (const searchItem of searchItems) {
if (searchItem.obj.c !== iconId) {
continue;
}
if (searchItem.type === "action") {
const action = await fetchAction(searchItem.id);
iconCache.set(iconId, action.name);
return action.name;
} else if (searchItem.type === "status") {
const status = await fetchStatus(searchItem.id);
iconCache.set(iconId, status.name);
return status.name;
} else {
const item = await fetchItem(searchItem.id);
iconCache.set(iconId, item.name);
return item.name;
}
}
return translateTimeline(name);
};
const translateIcon = async (element) => {
const name = element.alt;
const translated = await fetchIcon(element.src, name);
if (translated) {
element.alt = translated;
element.title = translated;
}
};
const injectIcon = () => {
const className = "Timeline-module_item";
const selector = `[class^="${className}"] img, [class*=" ${className}"] img`;
const observer = new MutationObserver((mutations) => {
const found = [];
for (const mutation of mutations) {
if (mutation.type === "childList") {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement) {
if (node.matches(selector)) {
found.push(node);
} else {
const children = node.querySelectorAll(selector);
for (const child of children) {
found.push(child);
}
}
}
}
}
}
for (const node of found) {
if (node instanceof HTMLImageElement) {
translateIcon(node);
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
const isXIVPackage = (pkg) => {
const url = new URL(pkg.url);
const params = new URLSearchParams(url.search);
const language = params.get("language");
if (url.hostname.endsWith("xivapi.com")) {
if (!url.pathname.startsWith("/api/")) {
return null;
}
if (url.pathname.endsWith("/sheet/Action")) {
if (params.get("transient") === "Description@as(html)") {
const data = pkg.json;
return {
type: "ActionRich",
rows: data.rows,
language
};
} else {
const data = pkg.json;
return {
type: "Action",
rows: data.rows,
language
};
}
} else if (url.pathname.endsWith("/sheet/Item")) {
const data = pkg.json;
return {
type: "Item",
rows: data.rows,
language
};
} else if (url.pathname.endsWith("/sheet/Status")) {
const data = pkg.json;
return {
type: "Status",
rows: data.rows,
language
};
} else if (url.pathname.endsWith("/sheet/Addon")) {
const data = pkg.json;
return {
type: "Addon",
rows: data.rows,
language
};
}
}
return null;
};
const processPackage = async (pkg) => {
const identifier = isXIVPackage(pkg);
if (!identifier) {
return pkg.response;
}
const { type, rows } = identifier;
const safeMap = (source, fn) => {
const safeFn = (obj) => {
try {
return fn(obj);
} catch (e) {
console.error(e);
return Promise.resolve(obj);
}
};
return Promise.all(source.map(safeFn));
};
let newRows;
if (type === "Action") {
newRows = await safeMap(rows, translateAction);
} else if (type === "ActionRich") {
newRows = await safeMap(rows, translateActionRich);
} else if (type === "Addon") {
newRows = await safeMap(rows, translateAddon);
} else if (type === "Item") {
newRows = await safeMap(rows, translateItem);
} else if (type === "Status") {
newRows = await safeMap(rows, translateStatus);
}
const result = {
...pkg.json,
rows: newRows
};
return new Response(JSON.stringify(result));
};
injectFetch(processPackage);
injectStyle();
injectTimeline();
injectIcon();
})();