From 7f7b7698406589e50c70ec49046c7ed8ed6ac0b3 Mon Sep 17 00:00:00 2001
From: troytt <47798984@qq.com>
Date: Sat, 19 Sep 2026 12:05:24 +0000
Subject: [PATCH] =?UTF-8?q?feat(i18n):=20=E8=A3=85=E5=A4=87=E4=B8=8E?=
=?UTF-8?q?=E8=AF=8D=E6=9D=A1=E4=B8=AD=E8=8B=B1=E6=96=87=E5=A4=9A=E8=AF=AD?=
=?UTF-8?q?=E8=A8=80=E5=88=87=E6=8D=A2=E6=94=AF=E6=8C=81=20EN/ZH/=E5=8F=8C?=
=?UTF-8?q?=E8=AF=AD=20(fixes=20#122)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
boss.html | 73 +
src/boss.ts | 70 +-
src/game/item-tooltip.ts | 188 +-
src/game/tooltip-i18n.ts | 3116 +++++++++++++++++++++++++++
tests/item-tooltip-i18n-122.test.ts | 208 ++
5 files changed, 3593 insertions(+), 62 deletions(-)
create mode 100644 src/game/tooltip-i18n.ts
create mode 100644 tests/item-tooltip-i18n-122.test.ts
diff --git a/boss.html b/boss.html
index 5b1b2e9..9223fea 100644
--- a/boss.html
+++ b/boss.html
@@ -526,6 +526,65 @@
gap: 16px;
}
+ /* Showcase Toolbar & Language Switcher (Issue #122) */
+ .showcase-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #0d0906;
+ border: 1px solid #2a1c11;
+ border-radius: 6px;
+ padding: 8px 14px;
+ margin-bottom: 4px;
+ }
+ .toolbar-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .toolbar-icon {
+ font-size: 14px;
+ }
+ .toolbar-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--gold-bright);
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ }
+ .lang-switch-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+ .lang-label {
+ font-size: 11px;
+ color: var(--text-dim);
+ margin-right: 4px;
+ }
+ .lang-pill {
+ background: #18110b;
+ border: 1px solid #3d2a1b;
+ color: #aa9c8d;
+ padding: 3px 10px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ }
+ .lang-pill:hover {
+ background: #24170e;
+ border-color: var(--border-gold);
+ color: var(--gold-bright);
+ }
+ .lang-pill.active {
+ background: linear-gradient(180deg, #44260d 0%, #2e1806 100%);
+ border-color: #ff9800;
+ color: #ffe082;
+ box-shadow: 0 0 8px rgba(255, 152, 0, 0.35);
+ }
+
/* Diablo II Authentic Tooltip / Item Card */
.d2-item-card {
background: #080604;
@@ -822,6 +881,20 @@
+
+
diff --git a/src/boss.ts b/src/boss.ts
index 03a5a8c..72b9f18 100644
--- a/src/boss.ts
+++ b/src/boss.ts
@@ -13,6 +13,7 @@
import { getEmbeddedDropTables } from './game/embedded-drop-tables.ts'
import { executeDropPipeline, type DropTables } from './game/drop-pipeline.ts'
import { formatItemTooltip, type FormattedItemTooltip } from './game/item-tooltip.ts'
+import { getTooltipLocale, setTooltipLocale, type TooltipLocale } from './game/tooltip-i18n.ts'
import type { Item } from './game/items.ts'
import { computeScaledNoDrop, computeEffectivePlayers } from './game/treasure-engine.ts'
@@ -148,6 +149,7 @@ interface DropSessionRecord {
}
const sessionHistory: DropSessionRecord[] = []
+let latestDroppedItems: Item[] = []
let totalKills = 0
let totalItemsDropped = 0
let totalUniques = 0
@@ -171,6 +173,7 @@ export function initBossApp(): void {
bindDifficultySelector()
bindSettings()
bindActionButtons()
+ bindLanguageSelector()
bindKeyboardShortcuts()
// 3. Initial UI update
@@ -369,11 +372,25 @@ function bindActionButtons(): void {
if (clearBtn) {
clearBtn.addEventListener('click', () => {
+ latestDroppedItems = []
renderLootShowcase([])
})
}
}
+function bindLanguageSelector(): void {
+ const pills = document.querySelectorAll
('.lang-pill')
+ pills.forEach(pill => {
+ pill.addEventListener('click', () => {
+ const lang = pill.dataset.lang as TooltipLocale | undefined
+ if (!lang) return
+ setTooltipLocale(lang)
+ pills.forEach(p => p.classList.toggle('active', p === pill))
+ renderLootShowcase(latestDroppedItems)
+ })
+ })
+}
+
function bindKeyboardShortcuts(): void {
window.addEventListener('keydown', (e: KeyboardEvent) => {
// Don't trigger if typing in an input
@@ -494,15 +511,19 @@ function renderKillBanner(times: number, dropCount: number, durationMs: string):
}
function renderLootShowcase(items: Item[]): void {
+ latestDroppedItems = items
const container = document.getElementById('loot-items-grid')
if (!container) return
+ const locale = getTooltipLocale()
+
if (items.length === 0) {
+ const isZh = locale === 'zh' || locale === 'bilingual'
container.innerHTML = `
⚔️
-
尚未击杀首领
-
点击左侧「模拟击杀 Boss」或按下 [空格键] 体验 1.13c 真实掉落与词缀属性生成
+
${isZh ? '尚未击杀首领' : 'No Boss Slain Yet'}
+
${isZh ? '点击左侧「模拟击杀 Boss」或按下 [空格键] 体验 1.13c 真实掉落与词缀属性生成' : 'Click "Kill Boss" on the left sidebar or press [Space] to simulate authentic 1.13c drops & affixes.'}
`
return
@@ -510,69 +531,80 @@ function renderLootShowcase(items: Item[]): void {
// Render Item Cards
container.innerHTML = items.map((item, idx) => {
- const tooltip = formatItemTooltip(item)
- return renderSingleItemCard(tooltip, idx + 1)
+ const tooltip = formatItemTooltip(item, locale)
+ return renderSingleItemCard(tooltip, idx + 1, locale)
}).join('')
}
-function renderSingleItemCard(t: FormattedItemTooltip, index: number): string {
+function renderSingleItemCard(t: FormattedItemTooltip, index: number, locale: TooltipLocale = getTooltipLocale()): string {
const linesHtml = t.lines.map(l => {
const colorClass = `line-${l.color || 'blue'}`
return `${escapeHtml(l.text)}
`
}).join('')
+ const isZh = locale === 'zh' || locale === 'bilingual'
+ const lblDefense = isZh ? '防御: ' : 'Defense: '
+ const lblOneHand = isZh ? '单手伤害: ' : 'One-Hand Damage: '
+ const lblTwoHand = isZh ? '双手伤害: ' : 'Two-Hand Damage: '
+ const lblDurability = isZh ? '耐久度: ' : 'Durability: '
+ const lblReqLevel = isZh ? '需要等级: ' : 'Required Level: '
+ const lblReqStr = isZh ? '需要力量: ' : 'Required Strength: '
+ const lblReqDex = isZh ? '需要敏捷: ' : 'Required Dexterity: '
+ const wordOf = isZh ? ' / ' : ' of '
+ const wordTo = isZh ? ' 至 ' : ' to '
+
const baseStatsHtml: string[] = []
if (t.defense !== undefined) {
const defText =
t.baseDefense !== undefined && t.baseDefense !== t.defense
? `${t.defense} (${t.baseDefense})`
: `${t.defense}`
- baseStatsHtml.push(`Defense: ${defText}
`)
+ baseStatsHtml.push(`${lblDefense}${defText}
`)
}
if (t.oneHandDamage) {
const d = t.oneHandDamage
const dmgText =
d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
- ? `${d.min} to ${d.max} (${d.baseMin} to ${d.baseMax})`
- : `${d.min} to ${d.max}`
- baseStatsHtml.push(`One-Hand Damage: ${dmgText}
`)
+ ? `${d.min}${wordTo}${d.max} (${d.baseMin}${wordTo}${d.baseMax})`
+ : `${d.min}${wordTo}${d.max}`
+ baseStatsHtml.push(`${lblOneHand}${dmgText}
`)
}
if (t.twoHandDamage) {
const d = t.twoHandDamage
const dmgText =
d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
- ? `${d.min} to ${d.max} (${d.baseMin} to ${d.baseMax})`
- : `${d.min} to ${d.max}`
- baseStatsHtml.push(`Two-Hand Damage: ${dmgText}
`)
+ ? `${d.min}${wordTo}${d.max} (${d.baseMin}${wordTo}${d.baseMax})`
+ : `${d.min}${wordTo}${d.max}`
+ baseStatsHtml.push(`${lblTwoHand}${dmgText}
`)
}
if (t.durability) {
const dur = t.durability
const durText =
dur.isEnhanced && dur.baseMax !== undefined
- ? `${dur.current} of ${dur.max} (${dur.baseMax})`
- : `${dur.current} of ${dur.max}`
- baseStatsHtml.push(`Durability: ${durText}
`)
+ ? `${dur.current}${wordOf}${dur.max} (${dur.baseMax})`
+ : `${dur.current}${wordOf}${dur.max}`
+ baseStatsHtml.push(`${lblDurability}${durText}
`)
}
if (t.reqLevel !== undefined) {
const lvlText =
t.baseReqLevel !== undefined && t.baseReqLevel !== t.reqLevel
? `${t.reqLevel} (${t.baseReqLevel})`
: `${t.reqLevel}`
- baseStatsHtml.push(`Required Level: ${lvlText}
`)
+ baseStatsHtml.push(`${lblReqLevel}${lvlText}
`)
}
if (t.reqStr !== undefined) {
const strText =
t.baseReqStr !== undefined && t.baseReqStr !== t.reqStr
? `${t.reqStr} (${t.baseReqStr})`
: `${t.reqStr}`
- baseStatsHtml.push(`Required Strength: ${strText}
`)
+ baseStatsHtml.push(`${lblReqStr}${strText}
`)
}
if (t.reqDex !== undefined) {
const dexText =
t.baseReqDex !== undefined && t.baseReqDex !== t.reqDex
? `${t.reqDex} (${t.baseReqDex})`
: `${t.reqDex}`
- baseStatsHtml.push(`Required Dexterity: ${dexText}
`)
+ baseStatsHtml.push(`${lblReqDex}${dexText}
`)
}
return `
@@ -581,7 +613,7 @@ function renderSingleItemCard(t: FormattedItemTooltip, index: number): string {
${escapeHtml(t.title)}
${t.subTitle ? `${escapeHtml(t.subTitle)}
` : ''}
- ${t.quality.toUpperCase()}
+ ${escapeHtml(t.quality.toUpperCase())}
ilvl ${t.ilvl}
diff --git a/src/game/item-tooltip.ts b/src/game/item-tooltip.ts
index ac8e609..88168c1 100644
--- a/src/game/item-tooltip.ts
+++ b/src/game/item-tooltip.ts
@@ -29,6 +29,23 @@ import {
type ItemStatCostTable,
type ItemStatCostRecord,
} from './properties.ts'
+import {
+ getTooltipLocale,
+ setTooltipLocale,
+ translateItemName,
+ translateMagicName,
+ translateRareName,
+ formatPropertyCodeI18n,
+ RUNE_INFO_ZH,
+ type TooltipLocale,
+} from './tooltip-i18n.ts'
+
+export {
+ getTooltipLocale,
+ setTooltipLocale,
+ translateItemName,
+ type TooltipLocale,
+} from './tooltip-i18n.ts'
export interface FormattedStatLine {
text: string
@@ -439,7 +456,11 @@ export function formatPropertyCode(
minOrMax?: number,
maxOrPar?: number | string,
maybePar?: string | number,
+ locale?: TooltipLocale,
): string {
+ if (locale && locale !== 'en') {
+ return formatPropertyCodeI18n(code, valueOrMin, minOrMax, maxOrPar, maybePar, locale)
+ }
let value: number
let min: number
let max: number
@@ -1965,7 +1986,7 @@ function getAffixEffectiveLevelReq(aff: RolledAffix, itemClass: string): number
/**
* Generates formatted tooltip structure for any dropped Diablo II Item.
*/
-export function formatItemTooltip(item: Item): FormattedItemTooltip {
+export function formatItemTooltip(item: Item, locale: TooltipLocale = getTooltipLocale()): FormattedItemTooltip {
const base = item.base
const quality = String(item.rarity || item.quality || 'normal').toLowerCase()
const ilvl = item.level ?? item.ilvl ?? 1
@@ -1982,18 +2003,35 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const runeData = isRune && runeKey ? RUNE_INFO[runeKey] : undefined
if (runeData) {
- title = runeData.nameEn
- subTitle = `Rune (${runeData.num})`
- qualityColor = '#ff8c00'
- lines.push({ text: 'Can be Inserted into Socketed Items', color: 'white' })
- lines.push({ text: `Weapons: ${runeData.weapon}`, color: 'blue' })
- lines.push({ text: `Armor/Helms/Shields: ${runeData.armor}`, color: 'blue' })
+ const rZh = runeKey ? RUNE_INFO_ZH[runeKey] : undefined
+ if (locale === 'zh') {
+ title = (rZh ? rZh.nameZh : runeData.nameEn) + '符文'
+ subTitle = `符文 (${runeData.num})`
+ qualityColor = '#ff8c00'
+ lines.push({ text: '可镶嵌进有凹槽的装备', color: 'white' })
+ lines.push({ text: `武器: ${rZh ? rZh.weaponZh : runeData.weapon}`, color: 'blue' })
+ lines.push({ text: `防具/头盔/盾牌: ${rZh ? rZh.armorZh : runeData.armor}`, color: 'blue' })
+ } else if (locale === 'bilingual') {
+ title = `${runeData.nameEn} (${rZh ? rZh.nameZh : ''})`
+ subTitle = `Rune (${runeData.num})`
+ qualityColor = '#ff8c00'
+ lines.push({ text: '可镶嵌进有凹槽的装备', color: 'white' })
+ lines.push({ text: `武器: ${rZh ? rZh.weaponZh : runeData.weapon}`, color: 'blue' })
+ lines.push({ text: `防具/头盔/盾牌: ${rZh ? rZh.armorZh : runeData.armor}`, color: 'blue' })
+ } else {
+ title = runeData.nameEn
+ subTitle = `Rune (${runeData.num})`
+ qualityColor = '#ff8c00'
+ lines.push({ text: 'Can be Inserted into Socketed Items', color: 'white' })
+ lines.push({ text: `Weapons: ${runeData.weapon}`, color: 'blue' })
+ lines.push({ text: `Armor/Helms/Shields: ${runeData.armor}`, color: 'blue' })
+ }
return {
title,
subTitle,
- quality: 'Rune',
+ quality: locale === 'zh' ? '符文' : 'Rune',
qualityColor,
- baseType: 'Rune',
+ baseType: locale === 'zh' ? '符文' : 'Rune',
ilvl,
reqLevel: runeData.lvl > 0 ? runeData.lvl : undefined,
lines,
@@ -2003,13 +2041,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
// Check if this is Gold
if (base.id === 'gld' || (base.kind !== 'weapon' && base.kind !== 'armor' && base.name.trim().toLowerCase() === 'gold')) {
const amount = (item.stats.amount ?? item.value ?? 100)
- title = `${amount.toLocaleString()} Gold`
+ title = locale === 'zh' ? `${amount.toLocaleString()} 金币` : `${amount.toLocaleString()} Gold`
qualityColor = '#ffd700'
return {
title,
- quality: 'Normal',
+ quality: locale === 'zh' ? '普通' : 'Normal',
qualityColor,
- baseType: 'Gold',
+ baseType: locale === 'zh' ? '金币' : 'Gold',
ilvl: 1,
lines: [],
}
@@ -2028,8 +2066,18 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
qualityColor = '#c8a15a'
const uniqDef = item.uniqueItemDef as UniqueItem | undefined
if (uniqDef) {
- title = uniqDef.index
- subTitle = base.name
+ if (locale === 'zh') {
+ title = translateItemName(uniqDef.index)
+ subTitle = translateItemName(base.name)
+ } else if (locale === 'bilingual') {
+ const uZh = translateItemName(uniqDef.index)
+ const bZh = translateItemName(base.name)
+ title = `${uniqDef.index} (${uZh})`
+ subTitle = `${base.name} (${bZh})`
+ } else {
+ title = uniqDef.index
+ subTitle = base.name
+ }
if (uniqDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, uniqDef.levelreq)
if (item.rolledProps && item.rolledProps.length > 0) {
rawProps.push(...item.rolledProps)
@@ -2047,14 +2095,25 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
}
} else {
- subTitle = base.name
+ subTitle = locale === 'zh' ? translateItemName(base.name) : locale === 'bilingual' ? `${base.name} (${translateItemName(base.name)})` : base.name
}
} else if (quality === 'set') {
qualityColor = '#00e600'
const setDef = item.setItemDef as SetItem | undefined
if (setDef) {
- title = setDef.index
- subTitle = `${base.name} · [${setDef.set}]`
+ if (locale === 'zh') {
+ title = translateItemName(setDef.index)
+ subTitle = `${translateItemName(base.name)} · [${translateItemName(setDef.set)}]`
+ } else if (locale === 'bilingual') {
+ const sZh = translateItemName(setDef.index)
+ const bZh = translateItemName(base.name)
+ const setZh = translateItemName(setDef.set)
+ title = `${setDef.index} (${sZh})`
+ subTitle = `${base.name} (${bZh}) · [${setDef.set} (${setZh})]`
+ } else {
+ title = setDef.index
+ subTitle = `${base.name} · [${setDef.set}]`
+ }
if (setDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, setDef.levelreq)
if (item.rolledProps && item.rolledProps.length > 0) {
rawProps.push(...item.rolledProps)
@@ -2072,19 +2131,28 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
}
} else {
- subTitle = base.name
+ subTitle = locale === 'zh' ? translateItemName(base.name) : locale === 'bilingual' ? `${base.name} (${translateItemName(base.name)})` : base.name
}
} else if (quality === 'rare') {
qualityColor = '#ffff60'
- subTitle = base.name
const rareDef = item.rolledRareAffixes as GeneratedRareAffixes | undefined
- if (rareDef) {
- if (rareDef.name) title = rareDef.name
- if (rareDef.affixes) {
- for (const aff of rareDef.affixes) {
- const effReq = getAffixEffectiveLevelReq(aff, itemClass)
- if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
- }
+ const enName = rareDef?.name || item.name
+ if (locale === 'zh') {
+ title = translateRareName(enName)
+ subTitle = translateItemName(base.name)
+ } else if (locale === 'bilingual') {
+ const rZh = translateRareName(enName)
+ const bZh = translateItemName(base.name)
+ title = `${enName} (${rZh})`
+ subTitle = `${base.name} (${bZh})`
+ } else {
+ title = enName
+ subTitle = base.name
+ }
+ if (rareDef && rareDef.affixes) {
+ for (const aff of rareDef.affixes) {
+ const effReq = getAffixEffectiveLevelReq(aff, itemClass)
+ if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
}
}
if (item.rolledProps && item.rolledProps.length > 0) {
@@ -2114,10 +2182,21 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
} else if (quality === 'magic') {
qualityColor = '#4880ff'
- subTitle = base.name
const magicDef = item.rolledMagicAffixes as GeneratedAffixes | undefined
+ const enName = magicDef?.name || item.name
+ if (locale === 'zh') {
+ title = translateMagicName(enName, base.name)
+ subTitle = translateItemName(base.name)
+ } else if (locale === 'bilingual') {
+ const mZh = translateMagicName(enName, base.name)
+ const bZh = translateItemName(base.name)
+ title = `${enName} (${mZh})`
+ subTitle = `${base.name} (${bZh})`
+ } else {
+ title = enName
+ subTitle = base.name
+ }
if (magicDef) {
- if (magicDef.name) title = magicDef.name
if (magicDef.prefix) {
const effReq = getAffixEffectiveLevelReq(magicDef.prefix, itemClass)
if (effReq > 0) affixReqLevel = Math.max(affixReqLevel, effReq)
@@ -2172,7 +2251,14 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
} else if (quality === 'superior') {
qualityColor = '#ffffff'
- title = item.name && item.name.startsWith('Superior ') ? item.name : `Superior ${base.name}`
+ const enName = item.name && item.name.startsWith('Superior ') ? item.name : `Superior ${base.name}`
+ if (locale === 'zh') {
+ title = translateItemName(enName)
+ } else if (locale === 'bilingual') {
+ title = `${enName} (${translateItemName(enName)})`
+ } else {
+ title = enName
+ }
if (item.rolledProps && item.rolledProps.length > 0) {
rawProps.push(...item.rolledProps)
} else if (base.kind === 'weapon') {
@@ -2182,13 +2268,27 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
} else if (quality === 'low') {
qualityColor = '#a0a0a0'
- title = item.name && item.name !== base.name ? item.name : `Cracked ${base.name}`
+ const enName = item.name && item.name !== base.name ? item.name : `Cracked ${base.name}`
+ if (locale === 'zh') {
+ title = translateItemName(enName)
+ } else if (locale === 'bilingual') {
+ title = `${enName} (${translateItemName(enName)})`
+ } else {
+ title = enName
+ }
if (item.rolledProps && item.rolledProps.length > 0) {
rawProps.push(...item.rolledProps)
}
} else {
qualityColor = '#e0d5c1'
- title = base.name
+ const enName = base.name
+ if (locale === 'zh') {
+ title = translateItemName(enName)
+ } else if (locale === 'bilingual') {
+ title = `${enName} (${translateItemName(enName)})`
+ } else {
+ title = enName
+ }
if (item.rolledProps && item.rolledProps.length > 0) {
rawProps.push(...item.rolledProps)
}
@@ -2198,7 +2298,7 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const mergedProps = aggregateAndSortProperties(rawProps)
for (const prop of mergedProps) {
lines.push({
- text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param),
+ text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param, locale),
color: 'blue',
isMagic: true,
})
@@ -2243,11 +2343,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const explicitSockets = Number((item as any).sockets || item.totalSockets || rolledSockets || 0)
- if (isEthereal && !lines.some(l => l.text.includes('Ethereal (Cannot be Repaired)'))) {
- lines.push({ text: 'Ethereal (Cannot be Repaired)', color: 'blue', isMagic: true })
+ if (isEthereal && !lines.some(l => l.text.includes('Ethereal') || l.text.includes('无形'))) {
+ const ethText = locale === 'zh' || locale === 'bilingual' ? '无形 (无法修复)' : 'Ethereal (Cannot be Repaired)'
+ lines.push({ text: ethText, color: 'blue', isMagic: true })
}
- if (explicitSockets > 0 && !lines.some(l => l.text.includes('Socketed ('))) {
- lines.push({ text: `Socketed (${explicitSockets})`, color: 'blue', isMagic: true })
+ if (explicitSockets > 0 && !lines.some(l => l.text.includes('Socketed') || l.text.includes('凹槽'))) {
+ const sockText = locale === 'zh' || locale === 'bilingual' ? `凹槽 (${explicitSockets})` : `Socketed (${explicitSockets})`
+ lines.push({ text: sockText, color: 'blue', isMagic: true })
}
// 1. Defense (Enhanced / Low Quality / Ethereal vs Base)
@@ -2365,13 +2467,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
const sockets = explicitSockets > 0 ? explicitSockets : (base as any).gemsockets > 0 ? (base as any).gemsockets : undefined
const displayQuality =
- quality === 'unique' ? 'Unique' :
- quality === 'set' ? 'Set' :
- quality === 'rare' ? 'Rare' :
- quality === 'magic' ? 'Magic' :
- quality === 'superior' ? 'Superior' :
- quality === 'low' ? 'Low Quality' :
- isRune ? 'Rune' : 'Normal'
+ quality === 'unique' ? (locale === 'zh' ? '暗金' : 'Unique') :
+ quality === 'set' ? (locale === 'zh' ? '套装' : 'Set') :
+ quality === 'rare' ? (locale === 'zh' ? '亮金' : 'Rare') :
+ quality === 'magic' ? (locale === 'zh' ? '魔法' : 'Magic') :
+ quality === 'superior' ? (locale === 'zh' ? '超强' : 'Superior') :
+ quality === 'low' ? (locale === 'zh' ? '劣质' : 'Low Quality') :
+ isRune ? (locale === 'zh' ? '符文' : 'Rune') : (locale === 'zh' ? '普通' : 'Normal')
return {
title,
diff --git a/src/game/tooltip-i18n.ts b/src/game/tooltip-i18n.ts
new file mode 100644
index 0000000..1d1181b
--- /dev/null
+++ b/src/game/tooltip-i18n.ts
@@ -0,0 +1,3116 @@
+/**
+ * Diablo II Tooltip Internationalization & Localization (Issue #122).
+ *
+ * Supports three display modes:
+ * - 'en': 100% 1.13c English original (default, 0 regression to existing tests)
+ * - 'zh': 100% Simplified Chinese localized
+ * - 'bilingual': Bilingual title (English + Chinese) with Chinese property lines
+ */
+
+export type TooltipLocale = 'en' | 'zh' | 'bilingual'
+
+let currentTooltipLocale: TooltipLocale = 'en'
+
+export function getTooltipLocale(): TooltipLocale {
+ return currentTooltipLocale
+}
+
+export function setTooltipLocale(locale: TooltipLocale): void {
+ currentTooltipLocale = locale
+}
+
+/**
+ * Item Base Names, Uniques, Sets, and Set Items (1,340+ entries).
+ */
+export const ITEM_NAMES_ZH: Record