finance-dashboard/lib/calendar-i18n.js

181 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 日曆事件繁體中文化:規則對照優先,剩餘英文再用 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,
})),
};
}