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