154 lines
5.0 KiB
JavaScript
154 lines
5.0 KiB
JavaScript
|
|
// 公司研究資料:欄位中文化(職稱/產業常用對照,非機器翻譯全文)
|
|||
|
|
|
|||
|
|
const TITLE_ZH = [
|
|||
|
|
[/chief executive officer|ceo/i, '執行長'],
|
|||
|
|
[/chief financial officer|cfo/i, '財務長'],
|
|||
|
|
[/chief operating officer|coo/i, '營運長'],
|
|||
|
|
[/chief technology officer|cto/i, '技術長'],
|
|||
|
|
[/executive vice president|evp/i, '執行副總'],
|
|||
|
|
[/senior vice president|svp/i, '資深副總'],
|
|||
|
|
[/vice president|vp/i, '副總'],
|
|||
|
|
[/president.*chief executive|president and chief executive/i, '執行長暨總裁'],
|
|||
|
|
[/president/i, '總裁'],
|
|||
|
|
[/general counsel/i, '法務長'],
|
|||
|
|
[/chief accounting officer/i, '會計長'],
|
|||
|
|
[/principal financial officer/i, '主要財務負責人'],
|
|||
|
|
[/principal executive officer/i, '主要執行負責人'],
|
|||
|
|
[/principal accounting officer/i, '主要會計負責人'],
|
|||
|
|
[/director/i, '董事'],
|
|||
|
|
[/chairman/i, '董事長'],
|
|||
|
|
[/operations/i, '營運'],
|
|||
|
|
[/worldwide field/i, '全球業務'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const SECTOR_ZH = {
|
|||
|
|
Technology: '科技',
|
|||
|
|
'Financial Services': '金融服務',
|
|||
|
|
Healthcare: '醫療保健',
|
|||
|
|
'Consumer Cyclical': '循環性消費',
|
|||
|
|
'Consumer Defensive': '防禦性消費',
|
|||
|
|
Energy: '能源',
|
|||
|
|
Industrials: '工業',
|
|||
|
|
'Basic Materials': '原物料',
|
|||
|
|
'Real Estate': '房地產',
|
|||
|
|
Utilities: '公用事業',
|
|||
|
|
'Communication Services': '通訊服務',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const INDUSTRY_HINTS = [
|
|||
|
|
[/semiconductor/i, '半導體'],
|
|||
|
|
[/software/i, '軟體'],
|
|||
|
|
[/internet/i, '網際網路'],
|
|||
|
|
[/bank/i, '銀行'],
|
|||
|
|
[/biotech|pharma/i, '生技/製藥'],
|
|||
|
|
[/retail/i, '零售'],
|
|||
|
|
[/auto/i, '汽車'],
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export function looksLikePersonName(name) {
|
|||
|
|
if (!name || name.length > 55) return false;
|
|||
|
|
if (/^Item \d/i.test(name) || name === 'Action' || name.startsWith('/s/')) return false;
|
|||
|
|
const n = name.toLowerCase();
|
|||
|
|
if (/financial|exhibit|schedule|statement|supplementary|governance|table of|designated|hedge|accounting|income|operations|revenue|consolidated|index|former|current|named|other|each|page|directors and|from our|served as/.test(n)) return false;
|
|||
|
|
const parts = name.trim().split(/\s+/);
|
|||
|
|
if (parts.length < 2 || parts.length > 5) return false;
|
|||
|
|
if (!/^[A-Z]/.test(parts[0])) return false;
|
|||
|
|
return parts.every(p => /^[A-Za-z'.-]+$/.test(p));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function looksLikeExecutiveTitle(title) {
|
|||
|
|
if (!title || title.length > 100) return false;
|
|||
|
|
if (/financial statement|exhibit|supplementary|schedule|table of contents|designated|hedge|accounting/i.test(title)) return false;
|
|||
|
|
if (/income from|cost of revenue|gross profit|net income|operating income/i.test(title)) return false;
|
|||
|
|
const t = title.toLowerCase();
|
|||
|
|
if (t === 'director' || t === 'directors') return false;
|
|||
|
|
return /chief|president|executive vice|general counsel|operations|officer|accounting|counsel|field/i.test(t);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function isOfficerRow(name, title) {
|
|||
|
|
return looksLikePersonName(name) && looksLikeExecutiveTitle(title);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function sanitizeOfficers(list) {
|
|||
|
|
return (list || []).filter(o => isOfficerRow(o.name, o.title));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function translateOfficerTitle(title) {
|
|||
|
|
const t = String(title || '').trim();
|
|||
|
|
if (!t) return '';
|
|||
|
|
for (const [re, zh] of TITLE_ZH) {
|
|||
|
|
if (re.test(t)) return zh;
|
|||
|
|
}
|
|||
|
|
return t;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function translateSector(sector) {
|
|||
|
|
const s = String(sector || '').trim();
|
|||
|
|
if (!s) return '—';
|
|||
|
|
return SECTOR_ZH[s] || s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function translateIndustry(industry) {
|
|||
|
|
const s = String(industry || '').trim();
|
|||
|
|
if (!s) return '—';
|
|||
|
|
for (const [re, zh] of INDUSTRY_HINTS) {
|
|||
|
|
if (re.test(s)) return `${zh}(${s})`;
|
|||
|
|
}
|
|||
|
|
return s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function localizeOfficer(o) {
|
|||
|
|
const title = o.titleZh || o.title || '';
|
|||
|
|
const titleZh = o.titleZh || translateOfficerTitle(title);
|
|||
|
|
return {
|
|||
|
|
...o,
|
|||
|
|
title,
|
|||
|
|
titleZh,
|
|||
|
|
titleDisplay: titleZh && titleZh !== title ? `${titleZh} · ${title}` : (titleZh || title),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function mergeCustomIntel(intel, custom) {
|
|||
|
|
if (!custom) return intel;
|
|||
|
|
const out = { ...intel, customUpdatedAt: custom.updatedAt || null };
|
|||
|
|
if (custom.profileZh) {
|
|||
|
|
out.profileZh = custom.profileZh;
|
|||
|
|
}
|
|||
|
|
if (custom.officers?.length) {
|
|||
|
|
out.management = {
|
|||
|
|
...out.management,
|
|||
|
|
officers: custom.officers.map(localizeOfficer),
|
|||
|
|
source: '本機自訂',
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
if (custom.news?.length) {
|
|||
|
|
out.news = custom.news.map(n => ({
|
|||
|
|
...n,
|
|||
|
|
titleZh: n.titleZh || n.title,
|
|||
|
|
descriptionZh: n.descriptionZh || n.description,
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
if (custom.managementNotes) {
|
|||
|
|
out.management = { ...out.management, notesZh: custom.managementNotes };
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function localizeIntel(intel) {
|
|||
|
|
if (!intel) return intel;
|
|||
|
|
const officers = sanitizeOfficers(intel.management?.officers || []).map(localizeOfficer);
|
|||
|
|
const news = (intel.news || []).map(n => ({
|
|||
|
|
...n,
|
|||
|
|
titleZh: n.titleZh || n.title,
|
|||
|
|
descriptionZh: n.descriptionZh || n.description,
|
|||
|
|
}));
|
|||
|
|
return {
|
|||
|
|
...intel,
|
|||
|
|
management: { ...intel.management, officers },
|
|||
|
|
news,
|
|||
|
|
searchesZh: (intel.management?.searches || []).map(s => ({
|
|||
|
|
...s,
|
|||
|
|
labelZh: s.labelZh || s.label.replace('Management', '管理層').replace('Leadership', '領導團隊'),
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
}
|