// 日曆事件繁體中文化:規則對照優先,剩餘英文再用 MyMemory 免費 API(結果快取) import { getCachedJSON, putCachedJSON } from './db.js'; const SOURCE_ZH = { 'Federal Reserve': '美國聯準會', BLS: '美國勞動統計局', BEA: '美國經濟分析局', 'Nasdaq earnings': 'Nasdaq 財報', 'Market calendar': '市場日曆', 'FRED releases': 'FRED 發布日程', 'Global markets': '全球市場', ECB: '歐洲央行', BOJ: '日本央行', BOE: '英格蘭銀行', }; const TITLE_RULES = [ [/Consumer Price Index for All Urban Consumers/i, 'CPI 消費者物價(通膨)'], [/Consumer Price Index|CPI/i, 'CPI 通膨'], [/Employment Situation|Nonfarm Payrolls|Nonfarm/i, '非農就業 / 失業率'], [/Producer Price Index|PPI/i, 'PPI 生產者物價'], [/Job Openings and Labor Turnover|Job Openings|JOLTS/i, 'JOLTS 職缺'], [/Import and Export Price/i, '進出口物價'], [/Real Earnings/i, '實質薪資'], [/Gross Domestic Product|GDP/i, 'GDP 國內生產毛額'], [/Personal Income and Outlays|Personal Income|Personal Outlays|PCE/i, 'PCE / 個人收入支出'], [/International Trade in Goods and Services/i, '國際貿易'], [/Corporate Profits/i, '企業利潤'], [/FOMC Meeting Minutes|FOMC minutes/i, 'FOMC 會議紀要'], [/FOMC.*SEP|SEP.*FOMC|dot plot/i, 'FOMC 利率決議 + 點陣圖'], [/FOMC|Federal Open Market Committee/i, 'FOMC 利率決議'], [/Retail Sales/i, '零售銷售'], [/Industrial Production/i, '工業生產'], [/Housing Starts|Building Permits/i, '住宅相關數據'], [/Jackson Hole/i, 'Jackson Hole 全球央行年會'], [/ECB 利率決議/i, 'ECB 利率決議'], [/日本央行 利率決議/i, '日本央行 利率決議'], [/英央行 MPC/i, '英央行 MPC 利率決議'], [/初領失業|Jobless Claims/i, '初領失業救濟金'], [/ADP/i, 'ADP 私部門就業'], [/密西根|Surveys of Consumers/i, '密西根消費者信心'], [/成屋銷售|Existing Home/i, '成屋銷售'], [/新屋開工|New Residential Construction/i, '新屋開工 / 營建許可'], [/耐久財|Manufacturer's Shipments/i, '耐久財 / 工廠接單出貨'], [/工業生產|Industrial Production/i, '工業生產 / 產能利用率'], [/零售銷售|Retail and Food Services/i, '零售銷售'], [/月選擇權結算/i, '月選擇權結算'], [/四巫日/i, '四巫日(衍生品結算)'], [/美股休市/i, '美股休市'], [/Employment Cost Index|ECI/i, '就業成本指數 ECI'], [/Productivity and Costs/i, '生產力 / 單位成本'], ]; const PHRASE_RULES = [ [/Summary of Economic Projections/i, '經濟預測摘要'], [/dot plot/i, '點陣圖'], [/policy statement and interest rate decision/i, '政策聲明與利率決議'], [/News Release/i, '新聞發布'], [/U\.S\./gi, '美國'], [/EPS forecast/i, 'EPS 預估'], [/fiscal quarter ending/i, '財季截止'], [/FOMC minutes/i, 'FOMC 會議紀要'], [/Federal Reserve/i, '美國聯準會'], [/Interest Rate Decision/i, '利率決議'], [/All Urban Consumers/i, '所有城市消費者'], [/seasonally adjusted/i, '季調'], ]; function hasLatin(text) { return /[A-Za-z]{3,}/.test(String(text || '')); } function applyRules(text, rules) { let out = String(text || '').trim(); if (!out) return ''; for (const [re, rep] of rules) out = out.replace(re, rep); out = out .replace(/\bET\b/g, '美東') .replace(/\bEST\b/g, '美東') .replace(/\bAM\b/g, '上午') .replace(/\bPM\b/g, '下午') .replace(/\s+/g, ' ') .trim(); return out; } function localizeTitle(title) { const t = String(title || ''); for (const [re, rep] of TITLE_RULES) { if (re.test(t)) return rep; } return applyRules(t.replace(/^U\.S\.\s*/i, '').replace(/\s+News Release.*$/i, ''), TITLE_RULES); } function localizeNote(note) { return applyRules(note, PHRASE_RULES); } function localizeTime(time) { if (!time) return ''; return String(time) .replace(/\bET\b/g, '美東') .replace(/\bAM\b/g, '上午') .replace(/\bPM\b/g, '下午') .trim(); } function localizeEarningsNote(note, symbol, name) { const parts = []; if (name && name !== symbol) parts.push(name); const eps = String(note || '').match(/EPS forecast\s+([\d.\-]+)/i); if (eps) parts.push(`EPS 預估 ${eps[1]}`); const fq = String(note || '').match(/·\s*([\d/]+)\s*$/); if (fq) parts.push(`財季 ${fq[1]}`); return parts.join(' · ') || `${symbol} 財報`; } async function translateWithMyMemory(text) { const src = String(text || '').trim().slice(0, 450); if (!src || !hasLatin(src)) return src; const cacheKey = `tr:en-zh-TW:${src}`; const cached = getCachedJSON(cacheKey, 30 * 86400000); if (cached) return cached; try { const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(src)}&langpair=en|zh-TW`; const res = await fetch(url, { headers: { 'User-Agent': 'finance-dashboard/1.0' } }); if (!res.ok) return src; const data = await res.json(); let out = data?.responseData?.translatedText || src; if (out === src || /QUERY LENGTH LIMIT/i.test(out)) return src; out = out.replace(/"/g, '"').replace(/'/g, "'").trim(); putCachedJSON(cacheKey, out); return out; } catch { return src; } } async function ensureZh(text, apiBudget) { let out = applyRules(text, PHRASE_RULES); if (!hasLatin(out)) return out; if (apiBudget.remaining <= 0) return out; apiBudget.remaining--; return translateWithMyMemory(out); } export async function localizeCalendarPayload(payload) { const apiBudget = { remaining: 24 }; const events = []; for (const ev of payload.events || []) { let title = ev.title || ''; let note = ev.note || ''; if (ev.category === 'earnings') { title = title.includes('財報') ? title : `${ev.symbol || ''} 財報`.trim(); note = localizeEarningsNote(note, ev.symbol, (note.split('·')[0] || '').trim()); } else { title = localizeTitle(title); note = localizeNote(note); if (hasLatin(title)) title = await ensureZh(title, apiBudget); if (hasLatin(note)) note = await ensureZh(note, apiBudget); } events.push({ ...ev, title, note, time: localizeTime(ev.time), source: SOURCE_ZH[ev.source] || ev.source, }); } return { ...payload, events, sources: (payload.sources || []).map(s => ({ ...s, name: SOURCE_ZH[s.name] || s.name, ok: s.ok, error: s.error, })), }; }