feat(i18n): Add internationalization support with English and French locales (#1)

feat(i18n): Add internationalization support with English and French locales
This commit is contained in:
Calesthio
2026-03-19 08:00:40 -07:00
committed by GitHub
5 changed files with 944 additions and 54 deletions

View File

@@ -363,6 +363,22 @@ body.low-perf .ticker-wrap::-webkit-scrollbar-thumb{background:rgba(100,240,200,
<script>
// === DATA ===
let D = null;
// === I18N ===
const L = window.__CRUCIX_LOCALE__ || {};
function t(keyPath, fallback) {
const keys = keyPath.split('.');
let value = L;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
return fallback || keyPath;
}
}
return typeof value === 'string' ? value : (fallback || keyPath);
}
// === GLOBALS ===
let globe = null;
let globeInitialized = false;
@@ -537,7 +553,7 @@ function togglePerfMode(){
function renderTopbar(){
const ts = new Date(D.meta.timestamp);
const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase();
const t = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
const timeStr = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
document.getElementById('topbar').innerHTML=`
<div class="top-left">
<span class="brand">CRUCIX MONITOR</span>
@@ -549,13 +565,13 @@ function renderTopbar(){
).join('')}
</div>
<div class="top-right">
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">PERF <span class="v" id="perfStatus">${lowPerfMode?'LOW':'HIGH'}</span></button>
<span class="meta-pill">SWEEP <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
<span class="meta-pill">${d} <span class="v">${t}</span></span>
<span class="meta-pill">SOURCES <span class="v">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span></span>
${D.delta?.summary ? `<span class="meta-pill">DELTA <span class="v">${D.delta.summary.direction==='risk-off'?'&#x25B2; RISK-OFF':D.delta.summary.direction==='risk-on'?'&#x25BC; RISK-ON':'&#x25C6; MIXED'}</span></span>` : ''}
<button class="guide-btn" onclick="openGlossary()">What Signals Mean</button>
<span class="alert-badge">HIGH ALERT</span>
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.perf','PERF')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.perfLow','LOW'):t('dashboard.perfHigh','HIGH')}</span></button>
<span class="meta-pill">${t('dashboard.sweep','SWEEP')} <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
<span class="meta-pill">${d} <span class="v">${timeStr}</span></span>
<span class="meta-pill">${t('dashboard.sources','SOURCES')} <span class="v">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span></span>
${D.delta?.summary ? `<span class="meta-pill">${t('dashboard.delta','DELTA')} <span class="v">${D.delta.summary.direction==='risk-off'?'&#x25B2; '+t('dashboard.riskOff','RISK-OFF'):D.delta.summary.direction==='risk-on'?'&#x25BC; '+t('dashboard.riskOn','RISK-ON'):'&#x25C6; '+t('dashboard.mixed','MIXED')}</span></span>` : ''}
<button class="guide-btn" onclick="openGlossary()">${t('dashboard.guideBtn','What Signals Mean')}</button>
<span class="alert-badge">${t('dashboard.highAlert','HIGH ALERT')}</span>
</div>`;
}
@@ -568,16 +584,16 @@ function renderLeftRail(){
const conflictEvents = D.acled?.totalEvents || 0;
const conflictFatal = D.acled?.totalFatalities || 0;
const layers=[
{name:'Air Activity',count:totalAir,dot:'air',sub:`${D.air.length} theaters`},
{name:'Thermal Spikes',count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} night det.`},
{name:'SDR Coverage',count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} online`},
{name:'Maritime Watch',count:D.chokepoints.length,dot:'maritime',sub:'chokepoints'},
{name:'Nuclear Sites',count:D.nuke.length,dot:'nuke',sub:'monitors'},
{name:'Conflict Events',count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} fatalities`},
{name:'Health Watch',count:D.who.length,dot:'health',sub:'WHO alerts'},
{name:'World News',count:newsCount,dot:'news',sub:'RSS geolocated'},
{name:'OSINT Feed',count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} urgent`},
{name:'Satellites',count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} new (30d)`}
{name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`},
{name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`},
{name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`},
{name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')},
{name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')},
{name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`},
{name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')},
{name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')},
{name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`},
{name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}
];
const allNormal=D.nuke.every(s=>!s.anom);
const nukeHtml=D.nuke.map(s=>`<div class="site-row"><span>${s.site}</span><span class="site-val">${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}</span></div>`).join('');
@@ -590,26 +606,26 @@ function renderLeftRail(){
document.getElementById('leftRail').innerHTML=`
<div class="g-panel">
<div class="sec-head"><h3>Sensor Grid</h3><span class="badge">LIVE</span></div>
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><span class="badge">${t('badges.live','LIVE')}</span></div>
${layers.map(l=>`<div class="layer-item"><div class="layer-left"><div class="ldot ${l.dot}"></div><div><div class="layer-name">${l.name}</div><div class="layer-sub">${l.sub}</div></div></div><div class="layer-count">${l.count}</div></div>`).join('')}
</div>
<div class="g-panel">
<div class="sec-head"><h3>Nuclear Watch</h3><span class="badge">RADIATION</span></div>
<div class="nuke-ok">${allNormal?'&#9679; ALL SITES NORMAL':'&#9888; ANOMALY DETECTED'}</div>
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
<div class="nuke-ok">${allNormal?'&#9679; '+t('nuclear.allSitesNormal','ALL SITES NORMAL'):'&#9888; '+t('nuclear.anomalyDetected','ANOMALY DETECTED')}</div>
${nukeHtml}
</div>
<div class="g-panel">
<div class="sec-head"><h3>Risk Gauges</h3><span class="badge">STRESS</span></div>
<div class="econ-row"><span class="elabel">VIX (Fear)</span><span class="eval" style="color:${vix?.value>20?'var(--warn)':'var(--accent)'}">${vix?.value||'--'}</span></div>
<div class="econ-row"><span class="elabel">HY Spread</span><span class="eval">${hy?.value||'--'}</span></div>
<div class="econ-row"><span class="elabel">USD Index</span><span class="eval">${usd?.value?.toFixed(1)||'--'}</span></div>
<div class="econ-row"><span class="elabel">Jobless Claims</span><span class="eval">${claims?.value?.toLocaleString()||'--'}</span></div>
<div class="econ-row"><span class="elabel">30Y Mortgage</span><span class="eval">${mort?.value||'--'}%</span></div>
<div class="econ-row"><span class="elabel">M2 Supply</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
<div class="econ-row"><span class="elabel">Nat. Debt</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
<div class="sec-head"><h3>${t('panels.riskGauges','Risk Gauges')}</h3><span class="badge">${t('badges.stress','STRESS')}</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.vix','VIX')} (Fear)</span><span class="eval" style="color:${vix?.value>20?'var(--warn)':'var(--accent)'}">${vix?.value||'--'}</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.hySpread','HY Spread')}</span><span class="eval">${hy?.value||'--'}</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.usdIndex','USD Index')}</span><span class="eval">${usd?.value?.toFixed(1)||'--'}</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.joblessClaims','Jobless Claims')}</span><span class="eval">${claims?.value?.toLocaleString()||'--'}</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.mortgage30y','30Y Mortgage')}</span><span class="eval">${mort?.value||'--'}%</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.m2Supply','M2 Supply')}</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
<div class="econ-row"><span class="elabel">${t('metrics.natDebt','Nat. Debt')}</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
</div>
<div class="g-panel">
<div class="sec-head"><h3>Space Watch</h3><span class="badge">CELESTRAK</span></div>
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><span class="badge">${t('badges.orbital','CELESTRAK')}</span></div>
${D.space ? `
<div class="econ-row"><span class="elabel">New Objects (30d)</span><span class="eval" style="color:var(--accent2)">${D.space.totalNewObjects||0}</span></div>
<div class="econ-row"><span class="elabel">Military Sats</span><span class="eval">${D.space.militarySats||0}</span></div>
@@ -638,8 +654,8 @@ function bindMapLifecycleEvents(){
function renderMapLegend(){
document.getElementById('mapLegend').innerHTML=
[{c:'#64f0c8',l:'Air Traffic'},{c:'#ff5f63',l:'Thermal/Fire'},{c:'rgba(255,120,80,0.8)',l:'Conflict'},{c:'#44ccff',l:'SDR Receiver'},
{c:'#ffe082',l:'Nuclear Site'},{c:'#b388ff',l:'Chokepoint'},{c:'#ffb84c',l:'OSINT Event'},{c:'#69f0ae',l:'Health Alert'},{c:'#81d4fa',l:'World News'},{c:'#ff9800',l:'Weather Alert'},{c:'#cddc39',l:'EPA RadNet'},{c:'#ffffff',l:'Space Station'},{c:'#6495ed',l:'GDELT Event'}]
[{c:'#64f0c8',l:t('map.airTraffic','Air Traffic')},{c:'#ff5f63',l:t('map.thermalFire','Thermal/Fire')},{c:'rgba(255,120,80,0.8)',l:t('map.conflict','Conflict')},{c:'#44ccff',l:t('map.sdrReceiver','SDR Receiver')},
{c:'#ffe082',l:t('map.nuclearSite','Nuclear Site')},{c:'#b388ff',l:t('map.chokepoint','Chokepoint')},{c:'#ffb84c',l:t('map.osintEvent','OSINT Event')},{c:'#69f0ae',l:t('map.healthAlert','Health Alert')},{c:'#81d4fa',l:t('map.worldNews','World News')},{c:'#ff9800',l:t('map.weatherAlert','Weather Alert')},{c:'#cddc39',l:t('map.epaRadNet','EPA RadNet')},{c:'#ffffff',l:t('map.spaceStation','Space Station')},{c:'#6495ed',l:t('map.gdeltEvent','GDELT Event')}]
.map(x=>`<div class="leg-item"><div class="leg-dot" style="background:${x.c}"></div>${x.l}</div>`).join('');
}
@@ -1406,14 +1422,14 @@ function renderLower(){
const deltaHtml = hasDelta ? deltaRows.join('') : '<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">No changes since last sweep</div>';
const tickerPanel = `<div class="g-panel lp-ticker" style="display:flex;flex-direction:column">
<div class="sec-head"><h3>Live News Ticker</h3><span class="badge">${feed.length} ITEMS</span></div>
<div class="sec-head"><h3>${t('panels.newsTicker','Live News Ticker')}</h3><span class="badge">${feed.length} ${t('badges.items','ITEMS')}</span></div>
<div class="ticker-wrap" style="--ticker-duration:${tickerDuration}s">
<div class="ticker-track">${tickerCards}${lowPerfMode ? '' : tickerCards}</div>
</div>
</div>`;
const osintPanel = mobile ? buildOsintPanel('lp-osint', 240) : '';
const macroPanel = `<div class="g-panel lp-macro">
<div class="sec-head"><h3>Macro + Markets</h3><span class="badge">${mkt.timestamp?'LIVE':'DELAYED'}</span></div>
<div class="sec-head"><h3>${t('panels.macroMarkets','Macro + Markets')}</h3><span class="badge">${mkt.timestamp?t('badges.live','LIVE'):t('badges.delayed','DELAYED')}</span></div>
${hasMarkets?`<div style="margin-bottom:8px">
<div style="font-family:var(--mono);font-size:9px;color:var(--dim);margin-bottom:4px;letter-spacing:1px">INDEXES</div>
<div class="metrics-row">${indexCards}</div>
@@ -1435,16 +1451,16 @@ function renderLower(){
</div>
</div>`;
const ideasPanel = `<div class="g-panel lp-ideas">
<div class="sec-head"><h3>Leverageable Ideas</h3>${D.ideasSource==='llm'?'<span class="ideas-src llm">AI ENHANCED</span>':D.ideasSource==='disabled'?'<span class="ideas-src static">LLM OFF</span>':'<span class="ideas-src static">PENDING</span>'}</div>
<div class="sec-head"><h3>${t('panels.tradeIdeas','Leverageable Ideas')}</h3>${D.ideasSource==='llm'?'<span class="ideas-src llm">'+t('ideas.aiEnhanced','AI ENHANCED')+'</span>':D.ideasSource==='disabled'?'<span class="ideas-src static">'+t('ideas.llmOff','LLM OFF')+'</span>':'<span class="ideas-src static">'+t('ideas.pending','PENDING')+'</span>'}</div>
${ideasHtml}
<div class="disclosure">FOR INFORMATIONAL PURPOSES ONLY. This is not financial advice, a recommendation to buy or sell any security, or a solicitation of any kind. All signal-based observations are derived from publicly available OSINT data and should not be relied upon for investment decisions. Consult a licensed financial advisor before making any investment. Past performance does not guarantee future results.</div>
</div>`;
const deltaPanel = `<div class="g-panel lp-delta">
<div class="sec-head"><h3>Sweep Delta</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?ds.direction.toUpperCase():'BASELINE'}</span></div>
<div class="sec-head"><h3>${t('panels.sweepDelta','Sweep Delta')}</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?t('delta.'+ds.direction,ds.direction.toUpperCase()):t('delta.baseline','BASELINE')}</span></div>
${hasDelta?`<div style="display:flex;gap:12px;margin-bottom:6px;font-family:var(--mono);font-size:10px">
<span style="color:var(--dim)">Changes: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
<span style="color:var(--dim)">Critical: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
${ds.signalBreakdown?`<span style="color:var(--dim)">New: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> &#8593;${ds.signalBreakdown.escalated} &#8595;${ds.signalBreakdown.deescalated}</span>`:''}
<span style="color:var(--dim)">${t('delta.changes','Changes')}: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
<span style="color:var(--dim)">${t('delta.critical','Critical')}: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
${ds.signalBreakdown?`<span style="color:var(--dim)">${t('delta.new','New')}: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> &#8593;${ds.signalBreakdown.escalated} &#8595;${ds.signalBreakdown.deescalated}</span>`:''}
</div>`:''}
<div class="delta-list">${deltaHtml}</div>
</div>`;
@@ -1470,12 +1486,12 @@ function renderRight(){
document.getElementById('rightRail').innerHTML=`
<div class="g-panel right-signals">
<div class="sec-head"><h3>Cross-Source Signals</h3><span class="badge">WORLDVIEW</span></div>
<div class="sec-head"><h3>${t('panels.crossSourceSignals','Cross-Source Signals')}</h3><span class="badge">${t('badges.worldview','WORLDVIEW')}</span></div>
${signals}
</div>
${mobile ? '' : buildOsintPanel('right-osint', 260)}
<div class="g-panel right-core">
<div class="sec-head"><h3>Signal Core</h3><span class="badge">HOT METRICS</span></div>
<div class="sec-head"><h3>${t('panels.signalCore','Signal Core')}</h3><span class="badge">${t('badges.hotMetrics','HOT METRICS')}</span></div>
${signalMetrics.map(s=>`<div class="sm"><span class="sml">${s.l}</span><div class="smb"><span style="width:${s.p}%"></span></div><span class="smv">${s.v}</span></div>`).join('')}
</div>`;
}
@@ -1489,18 +1505,19 @@ function safeExternalUrl(raw){try{const u=new URL(raw,location.href);return u.pr
function runBoot(){
const acledStatus = D.acled?.totalEvents > 0 ? `<span class="ok">${D.acled.totalEvents} EVENTS</span>` : '<span style="color:var(--warn)">DEGRADED</span>';
const lines=[
{text:'INITIALIZING CRUCIX ENGINE v2.1.0',delay:0},
{text:`CONNECTING ${D.meta.sourcesQueried} OSINT SOURCES...`,delay:400},
{text:'&#9500;&#9472; OPENSKY &#183; FIRMS &#183; KIWISDR &#183; MARITIME',delay:700},
{text:'&#9500;&#9472; FRED &#183; BLS &#183; EIA &#183; TREASURY &#183; GSCPI',delay:900},
{text:'&#9500;&#9472; TELEGRAM &#183; SAFECAST &#183; EPA &#183; WHO &#183; OFAC',delay:1100},
{text:'&#9492;&#9472; GDELT &#183; NOAA &#183; PATENTS &#183; BLUESKY &#183; REDDIT',delay:1300},
{text:`SWEEP COMPLETE &#8212; <span class="count">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span> SOURCES <span class="ok">OK</span>`,delay:1700},
{text:`ACLED CONFLICT LAYER: ${acledStatus}`,delay:1900},
{text:'FLIGHT CORRIDORS: <span class="ok">ACTIVE</span> &#183; DUAL PROJECTION: <span class="ok">READY</span>',delay:2100},
{text:'INTELLIGENCE SYNTHESIS: <span class="ok">ACTIVE</span>',delay:2400},
{text:t('boot.initializing','INITIALIZING CRUCIX ENGINE v2.1.0'),delay:0},
{text:t('boot.connecting','CONNECTING {count} OSINT SOURCES...').replace('{count}',D.meta.sourcesQueried),delay:400},
{text:'&#9500;&#9472; '+t('boot.sourceGroup1','OPENSKY · FIRMS · KIWISDR · MARITIME'),delay:700},
{text:'&#9500;&#9472; '+t('boot.sourceGroup2','FRED · BLS · EIA · TREASURY · GSCPI'),delay:900},
{text:'&#9500;&#9472; '+t('boot.sourceGroup3','TELEGRAM · SAFECAST · EPA · WHO · OFAC'),delay:1100},
{text:'&#9492;&#9472; '+t('boot.sourceGroup4','GDELT · NOAA · PATENTS · BLUESKY · REDDIT'),delay:1300},
{text:t('boot.sweepComplete','SWEEP COMPLETE — {ok}/{total} SOURCES').replace('{ok}',`<span class="count">${D.meta.sourcesOk}</span>`).replace('{total}',D.meta.sourcesQueried)+' <span class="ok">'+t('boot.ok','OK')+'</span>',delay:1700},
{text:t('boot.acledLayer','ACLED CONFLICT LAYER')+': '+acledStatus,delay:1900},
{text:t('boot.flightCorridors','FLIGHT CORRIDORS')+': <span class="ok">'+t('boot.active','ACTIVE')+'</span> &#183; '+t('boot.dualProjection','DUAL PROJECTION')+': <span class="ok">'+t('boot.ready','READY')+'</span>',delay:2100},
{text:t('boot.intelligenceSynthesis','INTELLIGENCE SYNTHESIS')+': <span class="ok">'+t('boot.active','ACTIVE')+'</span>',delay:2400},
];
const container=document.getElementById('bootLines');
document.getElementById('bootFinal').textContent=t('dashboard.terminalActive','TERMINAL ACTIVE');
const tl=gsap.timeline();
tl.to('.logo-ring',{opacity:1,duration:0.6,ease:'power2.out'},0);
tl.to(container,{opacity:1,duration:0.3},0.3);
@@ -1546,7 +1563,7 @@ function buildOsintPanel(panelClass='', maxHeight=260){
}).join('');
const osintDuration=Math.max(25,osintItems.length*3);
return `<div class="g-panel ${panelClass}" style="display:flex;flex-direction:column">
<div class="sec-head"><h3>OSINT Stream</h3><span class="badge">${D.tg.urgent.length} URGENT</span></div>
<div class="sec-head"><h3>${t('panels.osintStream','OSINT Stream')}</h3><span class="badge">${D.tg.urgent.length} ${t('badges.urgent','URGENT')}</span></div>
<div class="ticker-wrap" style="--ticker-duration:${osintDuration}s;max-height:${maxHeight}px">
<div class="ticker-track">${osintCards}${lowPerfMode ? '' : osintCards}</div>
</div>

137
lib/i18n.mjs Normal file
View File

@@ -0,0 +1,137 @@
// Internationalization (i18n) Module
// Loads locale files and provides translation functions
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LOCALES_DIR = join(__dirname, '..', 'locales');
// Supported languages
const SUPPORTED_LOCALES = ['en', 'fr'];
const DEFAULT_LOCALE = 'en';
// Cache loaded locales
const localeCache = new Map();
/**
* Get the current language from environment
* @returns {string} Language code (e.g., 'en', 'fr')
*/
export function getLanguage() {
// CRUCIX_LANG takes priority to avoid conflict with Linux system LANGUAGE variable
const lang = (process.env.CRUCIX_LANG || process.env.LANGUAGE || process.env.LANG || DEFAULT_LOCALE)
.toLowerCase()
.slice(0, 2);
return SUPPORTED_LOCALES.includes(lang) ? lang : DEFAULT_LOCALE;
}
/**
* Load a locale file
* @param {string} lang - Language code
* @returns {object} Locale data
*/
function loadLocale(lang) {
if (localeCache.has(lang)) {
return localeCache.get(lang);
}
const localePath = join(LOCALES_DIR, `${lang}.json`);
if (!existsSync(localePath)) {
console.warn(`[i18n] Locale file not found: ${localePath}, falling back to ${DEFAULT_LOCALE}`);
return loadLocale(DEFAULT_LOCALE);
}
try {
const data = JSON.parse(readFileSync(localePath, 'utf-8'));
localeCache.set(lang, data);
return data;
} catch (err) {
console.error(`[i18n] Failed to load locale ${lang}:`, err.message);
if (lang !== DEFAULT_LOCALE) {
return loadLocale(DEFAULT_LOCALE);
}
return {};
}
}
/**
* Get the current locale data
* @returns {object} Current locale data
*/
export function getLocale() {
return loadLocale(getLanguage());
}
/**
* Translate a key path (e.g., 'dashboard.title')
* @param {string} keyPath - Dot-separated key path
* @param {object} params - Optional parameters for interpolation
* @returns {string} Translated string or key if not found
*/
export function t(keyPath, params = {}) {
const locale = getLocale();
const keys = keyPath.split('.');
let value = locale;
for (const key of keys) {
if (value && typeof value === 'object' && key in value) {
value = value[key];
} else {
console.warn(`[i18n] Missing translation: ${keyPath}`);
return keyPath;
}
}
if (typeof value !== 'string') {
return keyPath;
}
// Interpolate parameters: {param} -> value
return value.replace(/\{(\w+)\}/g, (_, key) => {
return params[key] !== undefined ? params[key] : `{${key}}`;
});
}
/**
* Get LLM system prompt in current language
* @returns {string} System prompt for LLM
*/
export function getLLMPrompt() {
const locale = getLocale();
// Use loadLocale('en') for fallback since getLocale() doesn't accept a language argument
const fallbackLocale = loadLocale('en');
return locale.llm?.systemPrompt || fallbackLocale.llm?.systemPrompt || '';
}
/**
* Get all supported locales info
* @returns {Array} Array of locale info objects
*/
export function getSupportedLocales() {
return SUPPORTED_LOCALES.map(code => {
const locale = loadLocale(code);
return {
code,
name: locale.meta?.name || code,
nativeName: locale.meta?.nativeName || code
};
});
}
/**
* Check if a language is supported
* @param {string} lang - Language code
* @returns {boolean}
*/
export function isSupported(lang) {
return SUPPORTED_LOCALES.includes(lang?.toLowerCase()?.slice(0, 2));
}
// Export current language on module load
export const currentLanguage = getLanguage();
console.log(`[i18n] Language: ${currentLanguage}`);

359
locales/en.json Normal file
View File

@@ -0,0 +1,359 @@
{
"meta": {
"code": "en",
"name": "English",
"nativeName": "English"
},
"dashboard": {
"title": "CRUCIX — Intelligence Terminal",
"bootTitle": "CRUCIX INTELLIGENCE ENGINE",
"bootSubtitle": "Local Palantir · 31 Sources",
"waitingForSweep": "Waiting for first sweep...",
"sourcesOk": "Sources OK",
"lastSweep": "Last sweep",
"nextSweep": "Next sweep",
"sweep": "SWEEP",
"sources": "SOURCES",
"delta": "DELTA",
"highAlert": "HIGH ALERT",
"riskOff": "RISK-OFF",
"riskOn": "RISK-ON",
"mixed": "MIXED",
"terminalActive": "TERMINAL ACTIVE",
"perf": "PERF",
"perfLow": "LOW",
"perfHigh": "HIGH",
"guideBtn": "What Signals Mean"
},
"boot": {
"initializing": "INITIALIZING CRUCIX ENGINE v2.1.0",
"connecting": "CONNECTING {count} OSINT SOURCES...",
"sourceGroup1": "OPENSKY · FIRMS · KIWISDR · MARITIME",
"sourceGroup2": "FRED · BLS · EIA · TREASURY · GSCPI",
"sourceGroup3": "TELEGRAM · SAFECAST · EPA · WHO · OFAC",
"sourceGroup4": "GDELT · NOAA · PATENTS · BLUESKY · REDDIT",
"sourceGroup5": "USGS · ECB · CVE · COPERNICUS · CELESTRAK",
"sweepComplete": "SWEEP COMPLETE — {ok}/{total} SOURCES",
"ok": "OK",
"acledLayer": "ACLED CONFLICT LAYER",
"events": "EVENTS",
"degraded": "DEGRADED",
"flightCorridors": "FLIGHT CORRIDORS",
"active": "ACTIVE",
"dualProjection": "DUAL PROJECTION",
"ready": "READY",
"intelligenceSynthesis": "INTELLIGENCE SYNTHESIS"
},
"panels": {
"sensorGrid": "Sensor Grid",
"tradeIdeas": "Leverageable Ideas",
"osintFeed": "OSINT Feed",
"osintStream": "OSINT Stream",
"nuclearWatch": "Nuclear Watch",
"newsTicker": "Live News Ticker",
"sweepDelta": "Sweep Delta",
"macroMarkets": "Macro + Markets",
"healthAlerts": "Health Alerts",
"riskGauges": "Risk Gauges",
"crossSourceSignals": "Cross-Source Signals",
"signalCore": "Signal Core",
"seismicWatch": "Seismic Watch",
"cyberWatch": "Cyber Watch",
"spaceWatch": "Space Watch",
"europeAlerts": "Europe Alerts",
"ecbIndicators": "ECB Indicators"
},
"layers": {
"airActivity": "Air Activity",
"thermalSpikes": "Thermal Spikes",
"sdrCoverage": "SDR Coverage",
"maritimeWatch": "Maritime Watch",
"nuclearSites": "Nuclear Sites",
"conflictEvents": "Conflict Events",
"healthWatch": "Health Watch",
"worldNews": "World News",
"osintFeed": "OSINT Feed",
"theaters": "theaters",
"nightDet": "night det.",
"online": "online",
"chokepoints": "chokepoints",
"monitors": "monitors",
"fatalities": "fatalities",
"whoAlerts": "WHO alerts",
"rssGeolocated": "RSS geolocated",
"earthquakes": "Earthquakes",
"seismicEvents": "Seismic Events",
"cyberVulns": "Cyber Vulnerabilities",
"spaceActivity": "Space Activity",
"europeEmergency": "Europe Emergency"
},
"map": {
"worldNews": "World News",
"healthAlert": "Health Alert",
"chokepoint": "Chokepoint",
"nuclearSite": "Nuclear Site",
"osintEvent": "OSINT Event",
"thermalDetection": "Thermal Detection",
"aircraft": "Aircraft",
"rssGeolocated": "RSS geolocated",
"airTraffic": "Air Traffic",
"thermalFire": "Thermal/Fire",
"conflict": "Conflict",
"sdrReceiver": "SDR Receiver",
"scrollToZoom": "SCROLL TO ZOOM · DRAG TO PAN",
"globeMode": "GLOBE MODE",
"flatMode": "FLAT MODE",
"earthquake": "Earthquake",
"disaster": "Disaster",
"weatherAlert": "Weather Alert",
"epaRadNet": "EPA RadNet",
"spaceStation": "Space Station",
"gdeltEvent": "GDELT Event"
},
"ideas": {
"confidence": "Confidence",
"horizon": "Horizon",
"risk": "Risk",
"signals": "Signals",
"rationale": "Rationale",
"aiEnhanced": "AI ENHANCED",
"llmOff": "LLM OFF",
"pending": "PENDING",
"llmNotConfigured": "LLM NOT CONFIGURED",
"llmHelp": "Set LLM_PROVIDER + credentials in .env to enable AI-powered trade ideas",
"disclosure": "FOR INFORMATIONAL PURPOSES ONLY. This is not financial advice, a recommendation to buy or sell any security, or a solicitation of any kind. All signal-based observations are derived from publicly available OSINT data and should not be relied upon for investment decisions. Consult a licensed financial advisor before making any investment. Past performance does not guarantee future results."
},
"regions": {
"world": "World",
"americas": "Americas",
"europe": "Europe",
"middleEast": "Middle East",
"asiaPacific": "Asia Pacific",
"africa": "Africa"
},
"badges": {
"radiation": "RADIATION",
"live": "LIVE",
"delayed": "DELAYED",
"items": "ITEMS",
"urgent": "URGENT",
"worldview": "WORLDVIEW",
"hotMetrics": "HOT METRICS",
"stress": "STRESS",
"sweeping": "SWEEPING...",
"europe": "EUROPE",
"orbital": "ORBITAL"
},
"delta": {
"baseline": "BASELINE",
"escalation": "ESCALATION",
"deescalation": "DE-ESCALATION",
"stable": "STABLE",
"newSignals": "New Signals",
"resolved": "Resolved",
"noChanges": "No changes since last sweep",
"changes": "Changes",
"critical": "Critical",
"new": "NEW"
},
"metrics": {
"wtiCrude": "WTI Crude",
"brent": "Brent",
"natGas": "Nat Gas",
"vix": "VIX",
"fedFunds": "Fed Funds",
"gscpi": "GSCPI",
"cpiMom": "CPI MoM",
"unemployment": "Unemployment",
"hySpread": "HY Spread",
"usdIndex": "USD Index",
"joblessClaims": "Jobless Claims",
"mortgage30y": "30Y Mortgage",
"m2Supply": "M2 Supply",
"natDebt": "Nat. Debt",
"wti5day": "WTI 5-DAY",
"indexes": "INDEXES",
"crypto": "CRYPTO",
"energyMacro": "ENERGY + MACRO",
"vsPrior": "vs prior",
"ecbRate": "ECB Rate",
"eurusd": "EUR/USD",
"euM3": "EU M3",
"euHicp": "EU HICP",
"earthquakes7d": "Earthquakes (7d)",
"criticalCves": "Critical CVEs",
"spaceObjects": "Space Objects",
"starlink": "Starlink",
"europeAlerts": "EU Alerts"
},
"signalMetrics": {
"incidentTempo": "Incident Tempo",
"airTheaters": "Air Theaters",
"thermalSpikes": "Thermal Spikes",
"sdrNodes": "SDR Nodes",
"chokepoints": "Chokepoints",
"whoAlerts": "WHO Alerts"
},
"nuclear": {
"allSitesNormal": "ALL SITES NORMAL",
"anomalyDetected": "ANOMALY DETECTED",
"noData": "No data"
},
"seismic": {
"noRecentQuakes": "NO SIGNIFICANT QUAKES",
"majorQuake": "MAJOR QUAKE DETECTED",
"tsunamiRisk": "TSUNAMI RISK",
"mag": "M"
},
"cyber": {
"noAlerts": "NO CRITICAL CVES",
"criticalAlert": "CRITICAL VULNERABILITIES",
"cvss": "CVSS",
"critical": "CRITICAL"
},
"space": {
"noActivity": "NORMAL ACTIVITY",
"launchDetected": "LAUNCH ACTIVITY",
"objectsTracked": "objects tracked",
"newLast30d": "new (30d)",
"satellites": "sats",
"recentLaunches": "Recent Launches",
"totalTracked": "Total Tracked",
"byCountry": "BY COUNTRY",
"constellations": "CONSTELLATIONS",
"launches": "launches"
},
"europe": {
"noAlerts": "NO ACTIVE ALERTS",
"activeAlerts": "ACTIVE ALERTS",
"fires": "Fires",
"floods": "Floods",
"types": "types"
},
"time": {
"justNow": "just now",
"hoursAgo": "{hours}h ago",
"daysAgo": "{days}d ago"
},
"bot": {
"commands": {
"status": "Get current system health, last sweep time, source status",
"sweep": "Trigger a manual sweep cycle",
"brief": "Get a compact text summary of the latest intelligence",
"portfolio": "Show current positions and P&L (if Alpaca connected)",
"alerts": "Show recent alert history",
"mute": "Mute alerts for 1h (or /mute 2h, /mute 4h)",
"unmute": "Resume alerts",
"help": "Show available commands"
},
"messages": {
"alertsMuted": "🔇 Alerts muted for {hours}h — until {time} UTC",
"useUnmute": "Use /unmute to resume.",
"alertsResumed": "🔔 Alerts resumed. You'll receive the next signal evaluation.",
"sweepTriggered": "🚀 Manual sweep triggered. You'll receive alerts if anything significant is detected.",
"sweepInProgress": "🔄 Sweep already in progress. Please wait.",
"noDataYet": "⏳ No data yet — waiting for first sweep to complete.",
"noRecentAlerts": "No recent alerts.",
"recentAlerts": "📋 Recent Alerts (last {count})",
"commandsTip": "Tip: Commands are case-insensitive",
"commandFailed": "❌ Command failed: {error}",
"portfolioNotAvailable": "📊 Portfolio integration requires Alpaca MCP connection.\nUse the Crucix dashboard or Claude agent for portfolio queries."
},
"status": {
"title": "🖥️ CRUCIX STATUS",
"uptime": "Uptime",
"lastSweep": "Last sweep",
"nextSweep": "Next sweep",
"sweepInProgress": "Sweep in progress",
"yes": "🔄 Yes",
"no": "⏸️ No",
"sources": "Sources",
"failed": "failed",
"llm": "LLM",
"enabled": "✅",
"disabled": "❌ Disabled",
"sseClients": "SSE clients",
"dashboard": "Dashboard",
"pending": "pending",
"never": "never"
},
"brief": {
"title": "📋 CRUCIX BRIEF",
"direction": "Direction",
"changes": "changes",
"criticalChanges": "critical",
"osint": "📡 OSINT",
"urgentSignals": "urgent signals",
"totalPosts": "total posts",
"topIdeas": "💡 Top Ideas"
},
"alertTiers": {
"flash": "FLASH",
"priority": "PRIORITY",
"routine": "ROUTINE"
}
},
"alerts": {
"tiers": {
"flash": {
"label": "FLASH",
"description": "Immediate action required — market-moving, time-critical"
},
"priority": {
"label": "PRIORITY",
"description": "Important signal cluster — act within hours"
},
"routine": {
"label": "ROUTINE",
"description": "Noteworthy change — FYI, no urgency"
}
},
"confidence": {
"high": "HIGH",
"medium": "MEDIUM",
"low": "LOW"
},
"fields": {
"direction": "Direction",
"confidence": "Confidence",
"crossCorrelation": "Cross-Correlation",
"action": "Action",
"signals": "Signals",
"monitor": "Monitor"
},
"messages": {
"alertsMuted": "Alerts Muted",
"mutedUntil": "Alerts silenced for {hours}h — until {time} UTC.",
"useUnmuteToResume": "Use /unmute to resume.",
"alertsResumed": "Alerts Resumed",
"willReceiveNext": "You will receive the next signal evaluation."
},
"ruleBasedHeadlines": {
"nuclearAnomaly": "Nuclear Anomaly Detected",
"crossDomainSignals": "{count} Critical Cross-Domain Signals",
"escalatingSignals": "{count} Escalating Signals",
"osintSurge": "OSINT Surge: {count} New Urgent Posts",
"signalChangeDetected": "Signal Change Detected"
}
},
"discord": {
"commands": {
"status": "System health, last sweep time, source status",
"sweep": "Trigger a manual sweep cycle",
"brief": "Compact intelligence summary",
"portfolio": "Portfolio status (if Alpaca connected)",
"alerts": "Recent alert history",
"mute": "Mute alerts (default 1h)",
"unmute": "Resume alerts",
"muteHoursOption": "Hours to mute (default: 1)"
}
},
"llm": {
"systemPrompt": "You are a quantitative analyst at a macro intelligence firm. You receive structured OSINT + economic data from 25 sources and produce 5-8 actionable trade ideas.\n\nRules:\n- Each idea must cite specific data points from the input\n- Include entry rationale, risk factors, and time horizon\n- Blend geopolitical, economic, and market signals — cross-correlate across domains\n- Be specific: name instruments (tickers, futures, ETFs), not vague sectors\n- If delta shows significant changes, lead with those\n- Do NOT repeat ideas from the \"previous ideas\" list unless conditions have materially changed\n- Rate confidence: HIGH (multiple confirming signals), MEDIUM (thesis supported), LOW (speculative)\n\nOutput ONLY valid JSON array. Each object:\n{\n \"title\": \"Short title (max 10 words)\",\n \"type\": \"LONG|SHORT|HEDGE|WATCH|AVOID\",\n \"ticker\": \"Primary instrument\",\n \"confidence\": \"HIGH|MEDIUM|LOW\",\n \"rationale\": \"2-3 sentence explanation citing specific data\",\n \"risk\": \"Key risk factor\",\n \"horizon\": \"Intraday|Days|Weeks|Months\",\n \"signals\": [\"signal1\", \"signal2\"]\n}"
},
"api": {
"errors": {
"noDataYet": "No data yet — first sweep in progress"
}
}
}

359
locales/fr.json Normal file
View File

@@ -0,0 +1,359 @@
{
"meta": {
"code": "fr",
"name": "French",
"nativeName": "Français"
},
"dashboard": {
"title": "CRUCIX — Terminal de Renseignement",
"bootTitle": "CRUCIX MOTEUR DE RENSEIGNEMENT",
"bootSubtitle": "Palantir Local · 31 Sources",
"waitingForSweep": "En attente du premier scan...",
"sourcesOk": "Sources OK",
"lastSweep": "Dernier scan",
"nextSweep": "Prochain scan",
"sweep": "SCAN",
"sources": "SOURCES",
"delta": "DELTA",
"highAlert": "ALERTE HAUTE",
"riskOff": "RISK-OFF",
"riskOn": "RISK-ON",
"mixed": "MIXTE",
"terminalActive": "TERMINAL ACTIF",
"perf": "PERF",
"perfLow": "BAS",
"perfHigh": "HAUT",
"guideBtn": "Signification des Signaux"
},
"boot": {
"initializing": "INITIALISATION MOTEUR CRUCIX v2.1.0",
"connecting": "CONNEXION À {count} SOURCES OSINT...",
"sourceGroup1": "OPENSKY · FIRMS · KIWISDR · MARITIME",
"sourceGroup2": "FRED · BLS · EIA · TREASURY · GSCPI",
"sourceGroup3": "TELEGRAM · SAFECAST · EPA · OMS · OFAC",
"sourceGroup4": "GDELT · NOAA · BREVETS · BLUESKY · REDDIT",
"sourceGroup5": "USGS · BCE · CVE · COPERNICUS · CELESTRAK",
"sweepComplete": "SCAN TERMINÉ — {ok}/{total} SOURCES",
"ok": "OK",
"acledLayer": "COUCHE CONFLIT ACLED",
"events": "ÉVÉNEMENTS",
"degraded": "DÉGRADÉ",
"flightCorridors": "CORRIDORS AÉRIENS",
"active": "ACTIF",
"dualProjection": "DOUBLE PROJECTION",
"ready": "PRÊT",
"intelligenceSynthesis": "SYNTHÈSE RENSEIGNEMENT"
},
"panels": {
"sensorGrid": "Grille de Capteurs",
"tradeIdeas": "Idées de Trade",
"osintFeed": "Flux OSINT",
"osintStream": "Flux OSINT",
"nuclearWatch": "Surveillance Nucléaire",
"newsTicker": "Fil d'Actualités",
"sweepDelta": "Changements",
"macroMarkets": "Macro + Marchés",
"healthAlerts": "Alertes Santé",
"riskGauges": "Indicateurs de Risque",
"crossSourceSignals": "Signaux Multi-Sources",
"signalCore": "Noyau de Signaux",
"seismicWatch": "Surveillance Sismique",
"cyberWatch": "Surveillance Cyber",
"spaceWatch": "Surveillance Spatiale",
"europeAlerts": "Alertes Europe",
"ecbIndicators": "Indicateurs BCE"
},
"layers": {
"airActivity": "Activité Aérienne",
"thermalSpikes": "Pics Thermiques",
"sdrCoverage": "Couverture SDR",
"maritimeWatch": "Surveillance Maritime",
"nuclearSites": "Sites Nucléaires",
"conflictEvents": "Événements Conflits",
"healthWatch": "Surveillance Santé",
"worldNews": "Actualités Mondiales",
"osintFeed": "Flux OSINT",
"theaters": "théâtres",
"nightDet": "dét. nocturnes",
"online": "en ligne",
"chokepoints": "points strat.",
"monitors": "moniteurs",
"fatalities": "victimes",
"whoAlerts": "alertes OMS",
"rssGeolocated": "RSS géolocalisé",
"earthquakes": "Séismes",
"seismicEvents": "Événements Sismiques",
"cyberVulns": "Vulnérabilités Cyber",
"spaceActivity": "Activité Spatiale",
"europeEmergency": "Urgences Europe"
},
"map": {
"worldNews": "Actualités",
"healthAlert": "Alerte Santé",
"chokepoint": "Point Stratégique",
"nuclearSite": "Site Nucléaire",
"osintEvent": "Événement OSINT",
"thermalDetection": "Détection Thermique",
"aircraft": "Aéronef",
"rssGeolocated": "RSS géolocalisé",
"airTraffic": "Trafic Aérien",
"thermalFire": "Thermique/Feu",
"conflict": "Conflit",
"sdrReceiver": "Récepteur SDR",
"scrollToZoom": "MOLETTE POUR ZOOMER · GLISSER POUR DÉPLACER",
"globeMode": "MODE GLOBE",
"flatMode": "MODE PLAT",
"earthquake": "Séisme",
"disaster": "Catastrophe",
"weatherAlert": "Alerte Météo",
"epaRadNet": "EPA RadNet",
"spaceStation": "Station Spatiale",
"gdeltEvent": "Événement GDELT"
},
"ideas": {
"confidence": "Confiance",
"horizon": "Horizon",
"risk": "Risque",
"signals": "Signaux",
"rationale": "Analyse",
"aiEnhanced": "IA AMÉLIORÉE",
"llmOff": "LLM OFF",
"pending": "EN ATTENTE",
"llmNotConfigured": "LLM NON CONFIGURÉ",
"llmHelp": "Définir LLM_PROVIDER + identifiants dans .env pour activer les idées de trade IA",
"disclosure": "À TITRE INFORMATIF UNIQUEMENT. Ceci ne constitue pas un conseil financier, une recommandation d'achat ou de vente de titre, ni une sollicitation quelconque. Toutes les observations basées sur les signaux sont dérivées de données OSINT publiques et ne doivent pas être utilisées pour prendre des décisions d'investissement. Consultez un conseiller financier agréé avant tout investissement. Les performances passées ne garantissent pas les résultats futurs."
},
"regions": {
"world": "Monde",
"americas": "Amériques",
"europe": "Europe",
"middleEast": "Moyen-Orient",
"asiaPacific": "Asie-Pacifique",
"africa": "Afrique"
},
"badges": {
"radiation": "RADIATION",
"live": "EN DIRECT",
"delayed": "DIFFÉRÉ",
"items": "ÉLÉMENTS",
"urgent": "URGENT",
"worldview": "VUE GLOBALE",
"hotMetrics": "MÉTRIQUES CLÉS",
"stress": "STRESS",
"sweeping": "SCAN EN COURS...",
"europe": "EUROPE",
"orbital": "ORBITAL"
},
"delta": {
"baseline": "RÉFÉRENCE",
"escalation": "ESCALADE",
"deescalation": "DÉSESCALADE",
"stable": "STABLE",
"newSignals": "Nouveaux Signaux",
"resolved": "Résolus",
"noChanges": "Aucun changement depuis le dernier scan",
"changes": "Changements",
"critical": "Critiques",
"new": "NOUVEAU"
},
"metrics": {
"wtiCrude": "Pétrole WTI",
"brent": "Brent",
"natGas": "Gaz Naturel",
"vix": "VIX",
"fedFunds": "Taux Fed",
"gscpi": "GSCPI",
"cpiMom": "IPC MoM",
"unemployment": "Chômage",
"hySpread": "Spread HY",
"usdIndex": "Indice USD",
"joblessClaims": "Inscriptions Chômage",
"mortgage30y": "Hypothèque 30A",
"m2Supply": "Masse M2",
"natDebt": "Dette Nat.",
"wti5day": "WTI 5 JOURS",
"indexes": "INDICES",
"crypto": "CRYPTO",
"energyMacro": "ÉNERGIE + MACRO",
"vsPrior": "vs précédent",
"ecbRate": "Taux BCE",
"eurusd": "EUR/USD",
"euM3": "M3 UE",
"euHicp": "IPCH UE",
"earthquakes7d": "Séismes (7j)",
"criticalCves": "CVE Critiques",
"spaceObjects": "Objets Spatiaux",
"starlink": "Starlink",
"europeAlerts": "Alertes UE"
},
"signalMetrics": {
"incidentTempo": "Tempo Incidents",
"airTheaters": "Théâtres Aériens",
"thermalSpikes": "Pics Thermiques",
"sdrNodes": "Nœuds SDR",
"chokepoints": "Points Strat.",
"whoAlerts": "Alertes OMS"
},
"nuclear": {
"allSitesNormal": "TOUS LES SITES NORMAUX",
"anomalyDetected": "ANOMALIE DÉTECTÉE",
"noData": "Pas de données"
},
"seismic": {
"noRecentQuakes": "PAS DE SÉISME SIGNIFICATIF",
"majorQuake": "SÉISME MAJEUR DÉTECTÉ",
"tsunamiRisk": "RISQUE TSUNAMI",
"mag": "M"
},
"cyber": {
"noAlerts": "PAS DE CVE CRITIQUE",
"criticalAlert": "VULNÉRABILITÉS CRITIQUES",
"cvss": "CVSS",
"critical": "CRITIQUE"
},
"space": {
"noActivity": "ACTIVITÉ NORMALE",
"launchDetected": "ACTIVITÉ DE LANCEMENT",
"objectsTracked": "objets suivis",
"newLast30d": "nouv. (30j)",
"satellites": "sats",
"recentLaunches": "Lancements Récents",
"totalTracked": "Total Suivi",
"byCountry": "PAR PAYS",
"constellations": "CONSTELLATIONS",
"launches": "lancements"
},
"europe": {
"noAlerts": "PAS D'ALERTES ACTIVES",
"activeAlerts": "ALERTES ACTIVES",
"fires": "Incendies",
"floods": "Inondations",
"types": "types"
},
"time": {
"justNow": "à l'instant",
"hoursAgo": "il y a {hours}h",
"daysAgo": "il y a {days}j"
},
"bot": {
"commands": {
"status": "État du système, dernier scan, statut des sources",
"sweep": "Déclencher un scan manuel",
"brief": "Résumé compact des derniers renseignements",
"portfolio": "Positions et P&L (si Alpaca connecté)",
"alerts": "Historique des alertes récentes",
"mute": "Couper les alertes 1h (ou /mute 2h, /mute 4h)",
"unmute": "Reprendre les alertes",
"help": "Afficher les commandes disponibles"
},
"messages": {
"alertsMuted": "🔇 Alertes coupées pour {hours}h — jusqu'à {time} UTC",
"useUnmute": "Utilisez /unmute pour reprendre.",
"alertsResumed": "🔔 Alertes reprises. Vous recevrez la prochaine évaluation de signal.",
"sweepTriggered": "🚀 Scan manuel déclenché. Vous recevrez des alertes si quelque chose de significatif est détecté.",
"sweepInProgress": "🔄 Scan déjà en cours. Veuillez patienter.",
"noDataYet": "⏳ Pas encore de données — en attente du premier scan.",
"noRecentAlerts": "Pas d'alertes récentes.",
"recentAlerts": "📋 Alertes Récentes (les {count} dernières)",
"commandsTip": "Astuce : Les commandes ne sont pas sensibles à la casse",
"commandFailed": "❌ Commande échouée : {error}",
"portfolioNotAvailable": "📊 L'intégration portfolio nécessite la connexion Alpaca MCP.\nUtilisez le dashboard Crucix ou l'agent Claude pour les requêtes portfolio."
},
"status": {
"title": "🖥️ STATUT CRUCIX",
"uptime": "Disponibilité",
"lastSweep": "Dernier scan",
"nextSweep": "Prochain scan",
"sweepInProgress": "Scan en cours",
"yes": "🔄 Oui",
"no": "⏸️ Non",
"sources": "Sources",
"failed": "échouées",
"llm": "LLM",
"enabled": "✅",
"disabled": "❌ Désactivé",
"sseClients": "Clients SSE",
"dashboard": "Dashboard",
"pending": "en attente",
"never": "jamais"
},
"brief": {
"title": "📋 BRIEF CRUCIX",
"direction": "Direction",
"changes": "changements",
"criticalChanges": "critiques",
"osint": "📡 OSINT",
"urgentSignals": "signaux urgents",
"totalPosts": "posts totaux",
"topIdeas": "💡 Meilleures Idées"
},
"alertTiers": {
"flash": "FLASH",
"priority": "PRIORITÉ",
"routine": "ROUTINE"
}
},
"alerts": {
"tiers": {
"flash": {
"label": "FLASH",
"description": "Action immédiate requise — impact marché, temps critique"
},
"priority": {
"label": "PRIORITÉ",
"description": "Cluster de signaux important — agir dans les heures"
},
"routine": {
"label": "ROUTINE",
"description": "Changement notable — informatif, pas d'urgence"
}
},
"confidence": {
"high": "HAUTE",
"medium": "MOYENNE",
"low": "BASSE"
},
"fields": {
"direction": "Direction",
"confidence": "Confiance",
"crossCorrelation": "Corrélation Croisée",
"action": "Action",
"signals": "Signaux",
"monitor": "Surveiller"
},
"messages": {
"alertsMuted": "Alertes Coupées",
"mutedUntil": "Alertes suspendues pour {hours}h — jusqu'à {time} UTC.",
"useUnmuteToResume": "Utilisez /unmute pour reprendre.",
"alertsResumed": "Alertes Reprises",
"willReceiveNext": "Vous recevrez la prochaine évaluation de signal."
},
"ruleBasedHeadlines": {
"nuclearAnomaly": "Anomalie Nucléaire Détectée",
"crossDomainSignals": "{count} Signaux Critiques Multi-Domaines",
"escalatingSignals": "{count} Signaux en Escalade",
"osintSurge": "Surge OSINT : {count} Nouveaux Posts Urgents",
"signalChangeDetected": "Changement de Signal Détecté"
}
},
"discord": {
"commands": {
"status": "Santé système, dernier scan, statut sources",
"sweep": "Déclencher un scan manuel",
"brief": "Résumé compact de renseignement",
"portfolio": "Statut portfolio (si Alpaca connecté)",
"alerts": "Historique des alertes récentes",
"mute": "Couper les alertes (par défaut 1h)",
"unmute": "Reprendre les alertes",
"muteHoursOption": "Heures de coupure (par défaut : 1)"
}
},
"llm": {
"systemPrompt": "Tu es un analyste quantitatif dans une firme de renseignement macro. Tu reçois des données OSINT + économiques structurées de 25 sources et tu produis 5-8 idées de trade actionnables.\n\nRègles:\n- Chaque idée doit citer des données spécifiques de l'input\n- Inclure le rationnel d'entrée, les facteurs de risque et l'horizon temporel\n- Croiser les signaux géopolitiques, économiques et de marché\n- Être spécifique: nommer les instruments (tickers, futures, ETFs), pas des secteurs vagues\n- Si le delta montre des changements significatifs, commencer par ceux-là\n- NE PAS répéter les idées de la liste \"previous ideas\" sauf si les conditions ont matériellement changé\n- Évaluer la confiance: HIGH (signaux multiples confirmants), MEDIUM (thèse supportée), LOW (spéculatif)\n\nOutput UNIQUEMENT un tableau JSON valide. Chaque objet:\n{\n \"title\": \"Titre court en français (max 10 mots)\",\n \"type\": \"LONG|SHORT|HEDGE|WATCH|AVOID\",\n \"ticker\": \"Instrument principal\",\n \"confidence\": \"HIGH|MEDIUM|LOW\",\n \"rationale\": \"Explication 2-3 phrases en français citant les données spécifiques\",\n \"risk\": \"Facteur de risque principal en français\",\n \"horizon\": \"Intraday|Days|Weeks|Months\",\n \"signals\": [\"signal1\", \"signal2\"]\n}"
},
"api": {
"errors": {
"noDataYet": "Pas encore de données — premier scan en cours"
}
}
}

View File

@@ -8,6 +8,7 @@ import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { exec } from 'child_process';
import config from './crucix.config.mjs';
import { getLocale, currentLanguage, getSupportedLocales } from './lib/i18n.mjs';
import { fullBriefing } from './apis/briefing.mjs';
import { synthesize, generateIdeas } from './dashboard/inject.mjs';
import { MemoryManager } from './lib/delta/index.mjs';
@@ -231,12 +232,20 @@ if (discordAlerter.isConfigured) {
const app = express();
app.use(express.static(join(ROOT, 'dashboard/public')));
// Serve loading page until first sweep completes, then the dashboard
// Serve loading page until first sweep completes, then the dashboard with injected locale
app.get('/', (req, res) => {
if (!currentData) {
res.sendFile(join(ROOT, 'dashboard/public/loading.html'));
} else {
res.sendFile(join(ROOT, 'dashboard/public/jarvis.html'));
const htmlPath = join(ROOT, 'dashboard/public/jarvis.html');
let html = readFileSync(htmlPath, 'utf-8');
// Inject locale data into the HTML
const locale = getLocale();
const localeScript = `<script>window.__CRUCIX_LOCALE__ = ${JSON.stringify(locale).replace(/<\/script>/gi, '<\\/script>')};</script>`;
html = html.replace('</head>', `${localeScript}\n</head>`);
res.type('html').send(html);
}
});
@@ -263,6 +272,15 @@ app.get('/api/health', (req, res) => {
llmProvider: config.llm.provider,
telegramEnabled: !!(config.telegram.botToken && config.telegram.chatId),
refreshIntervalMinutes: config.refreshIntervalMinutes,
language: currentLanguage,
});
});
// API: available locales
app.get('/api/locales', (req, res) => {
res.json({
current: currentLanguage,
supported: getSupportedLocales(),
});
});