diff --git a/scripts/docs-stats.mjs b/scripts/docs-stats.mjs
index b6bf548e93..f47c4d106e 100644
--- a/scripts/docs-stats.mjs
+++ b/scripts/docs-stats.mjs
@@ -59,8 +59,8 @@ function computeStats() {
// ---- Server domain handlers (server/worldmonitor/*/) ----
const serverDomains = dirsIn('server/worldmonitor').length;
- // ---- Locales (src/locales/*.json) ----
- const locales = filesIn('src/locales').filter((f) => f.endsWith('.json')).length;
+ // ---- User-facing locales (src/locales/*.json, excluding shell fragments) ----
+ const locales = filesIn('src/locales').filter((f) => f.endsWith('.json') && !f.endsWith('.shell.json')).length;
// ---- CI workflows (.github/workflows/*.yml) ----
const workflows = filesIn('.github/workflows').filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')).sort();
diff --git a/src/App.ts b/src/App.ts
index 1a9870ac66..7f70379669 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -78,6 +78,7 @@ import { RefreshScheduler } from '@/app/refresh-scheduler';
import { PanelLayoutManager } from '@/app/panel-layout';
import { DataLoaderManager } from '@/app/data-loader';
import { EventHandlerManager } from '@/app/event-handlers';
+import { replaceRawI18nKeyPlaceholders } from '@/app/i18n-raw-key-healer';
import { resolveUserRegion, resolvePreciseUserCoordinates, type PreciseCoordinates } from '@/utils/user-location';
import { showProBanner } from '@/components/ProBanner';
import { initAuthState, subscribeAuthState } from '@/services/auth-state';
@@ -161,6 +162,13 @@ export class App {
private readonly handleConnectivityChange = (): void => {
this.updateConnectivityUi();
};
+ private readonly handleI18nResourcesLoaded = (ev: Event): void => {
+ const language = (ev as CustomEvent<{ language?: unknown }>).detail?.language;
+ if (language !== 'en') return;
+ // Scope this to the app container: body-level modals are user-opened after
+ // startup, by which point the full English bundle should already be loaded.
+ replaceRawI18nKeyPlaceholders(this.state.container, t);
+ };
private readonly handleFollowedCountriesCapDrop = (ev: Event): void => {
const detail = (ev as CustomEvent<{ kept?: unknown; dropped?: unknown }>).detail;
const dropped = typeof detail?.dropped === 'number' ? detail.dropped : 0;
@@ -923,6 +931,8 @@ export class App {
},
});
+ window.addEventListener('wm:i18n:resources-loaded', this.handleI18nResourcesLoaded);
+
await initDB();
await initI18n();
// Localize the static index.html shell —
, meta description, and
@@ -1429,6 +1439,7 @@ export class App {
window.removeEventListener('resize', this.handleViewportPrime);
window.removeEventListener('online', this.handleConnectivityChange);
window.removeEventListener('offline', this.handleConnectivityChange);
+ window.removeEventListener('wm:i18n:resources-loaded', this.handleI18nResourcesLoaded);
window.removeEventListener(WM_FOLLOWED_COUNTRIES_CAP_DROP, this.handleFollowedCountriesCapDrop);
if (this.visiblePanelPrimeRaf !== null) {
window.cancelAnimationFrame(this.visiblePanelPrimeRaf);
diff --git a/src/app/i18n-raw-key-healer.ts b/src/app/i18n-raw-key-healer.ts
new file mode 100644
index 0000000000..fe68ec92c8
--- /dev/null
+++ b/src/app/i18n-raw-key-healer.ts
@@ -0,0 +1,56 @@
+export type I18nTranslator = (key: string) => string;
+
+const RAW_I18N_KEY_RE = /^[a-z][A-Za-z0-9]*(?:\.[A-Za-z0-9_-]+)+$/;
+const TRANSLATABLE_ATTRIBUTES = ['aria-label', 'title', 'placeholder'] as const;
+const TEXT_NODE_TYPE = 3;
+
+export function translateRawI18nKeyPlaceholder(value: string, translate: I18nTranslator): string | null {
+ const key = value.trim();
+ if (!RAW_I18N_KEY_RE.test(key)) return null;
+
+ const translated = translate(key);
+ return translated !== key ? translated : null;
+}
+
+export function replaceRawI18nKeyPlaceholderText(value: string, translate: I18nTranslator): string | null {
+ const replacement = translateRawI18nKeyPlaceholder(value, translate);
+ if (replacement === null) return null;
+
+ const leading = value.match(/^\s*/)?.[0] ?? '';
+ const trailing = value.match(/\s*$/)?.[0] ?? '';
+ return `${leading}${replacement}${trailing}`;
+}
+
+export function replaceRawI18nKeyPlaceholders(root: ParentNode, translate: I18nTranslator): void {
+ const textNodes: Text[] = [];
+
+ const collectTextNodes = (node: Node): void => {
+ if (node.nodeType === TEXT_NODE_TYPE) {
+ textNodes.push(node as Text);
+ return;
+ }
+
+ for (const child of Array.from(node.childNodes)) {
+ collectTextNodes(child);
+ }
+ };
+
+ collectTextNodes(root as Node);
+
+ for (const node of textNodes) {
+ const next = replaceRawI18nKeyPlaceholderText(node.nodeValue ?? '', translate);
+ if (next !== null) {
+ node.nodeValue = next;
+ }
+ }
+
+ for (const el of Array.from(root.querySelectorAll('[aria-label], [title], [placeholder]'))) {
+ for (const attr of TRANSLATABLE_ATTRIBUTES) {
+ const value = el.getAttribute(attr);
+ if (!value) continue;
+
+ const replacement = translateRawI18nKeyPlaceholder(value, translate);
+ if (replacement !== null) el.setAttribute(attr, replacement);
+ }
+ }
+}
diff --git a/src/locales/en.shell.json b/src/locales/en.shell.json
new file mode 100644
index 0000000000..548c578b04
--- /dev/null
+++ b/src/locales/en.shell.json
@@ -0,0 +1,794 @@
+{
+ "shell": {
+ "documentTitle": "World Monitor — Real-Time Global Intelligence Dashboard",
+ "metaDescription": "Real-time global intelligence platform. Featured in WIRED. Used by 2M+ people across 190 countries. Conflicts, markets, military, OSINT in one view.",
+ "offlineTitle": "You're Offline",
+ "offlineMessage": "World Monitor requires an internet connection for real-time intelligence data.",
+ "offlineRetry": "Retry"
+ },
+ "header": {
+ "world": "WORLD",
+ "tech": "TECH",
+ "live": "LIVE",
+ "cached": "CACHED",
+ "unavailable": "UNAVAILABLE",
+ "search": "Search",
+ "settings": "SETTINGS",
+ "sources": "SOURCES",
+ "copyLink": "Link",
+ "downloadApp": "Download App",
+ "fullscreen": "Fullscreen",
+ "pinMap": "Pin map to top",
+ "selectRegion": "Select Region",
+ "viewOnGitHub": "View on GitHub",
+ "filterSources": "Filter sources...",
+ "sourcesEnabled": "{{enabled}}/{{total}} enabled",
+ "finance": "FINANCE",
+ "commodity": "COMMODITY",
+ "energy": "ENERGY",
+ "panelDisplayCaption": "Choose which panels to show on the dashboard",
+ "tabSettings": "Settings",
+ "tabPanels": "Panels",
+ "tabSources": "Sources",
+ "tabNotifications": "Notifications",
+ "languageLabel": "Language",
+ "sourceRegionAll": "All",
+ "sourceRegionWorldwide": "Worldwide",
+ "sourceRegionUS": "United States",
+ "sourceRegionMiddleEast": "Middle East",
+ "sourceRegionAfrica": "Africa",
+ "sourceRegionLatAm": "Latin America",
+ "sourceRegionAsiaPacific": "Asia-Pacific",
+ "sourceRegionEurope": "Europe",
+ "sourceRegionTopical": "Topical",
+ "sourceRegionIntel": "Intelligence",
+ "sourceRegionTechNews": "Tech News",
+ "sourceRegionAiMl": "AI & ML",
+ "sourceRegionStartupsVc": "Startups & VC",
+ "sourceRegionRegionalTech": "Regional Ecosystems",
+ "sourceRegionDeveloper": "Developer",
+ "sourceRegionCybersecurity": "Cybersecurity",
+ "sourceRegionTechPolicy": "Policy & Research",
+ "sourceRegionTechMedia": "Media & Podcasts",
+ "sourceRegionMarkets": "Markets & Analysis",
+ "sourceRegionFixedIncomeFx": "Fixed Income & FX",
+ "sourceRegionCommodities": "Commodities",
+ "sourceRegionCryptoDigital": "Crypto & Digital",
+ "sourceRegionCentralBanks": "Central Banks & Economy",
+ "sourceRegionDeals": "Deals & Corporate",
+ "sourceRegionFinRegulation": "Financial Regulation",
+ "sourceRegionGulfMena": "Gulf & MENA",
+ "filterPanels": "Filter panels...",
+ "resetLayout": "Reset Layout",
+ "resetLayoutTooltip": "Restore default panel arrangement",
+ "unsavedChanges": "You have unsaved panel changes. Discard them?",
+ "panelCatCore": "Core",
+ "panelCatIntelligence": "Intelligence",
+ "panelCatCorrelation": "Correlation",
+ "panelCatRegionalNews": "Regional News",
+ "panelCatMarketsFinance": "Markets & Finance",
+ "panelCatTopical": "Topical",
+ "panelCatDataTracking": "Data & Tracking",
+ "panelCatTechAi": "Tech & AI",
+ "panelCatStartupsVc": "Startups & VC",
+ "panelCatSecurityPolicy": "Security & Policy",
+ "panelCatMarkets": "Markets",
+ "panelCatFixedIncomeFx": "Fixed Income & FX",
+ "panelCatCommodities": "Commodities",
+ "panelCatCryptoDigital": "Crypto & Digital",
+ "panelCatCentralBanks": "Central Banks & Econ",
+ "panelCatDeals": "Deals & Institutional",
+ "panelCatGulfMena": "Gulf & MENA",
+ "panelCatCommodityPrices": "Prices & Markets",
+ "panelCatMining": "Mining & Supply Chain",
+ "panelCatCommodityEcon": "Economy & Trade",
+ "panelCatHappyNews": "Good News",
+ "panelCatHappyPlanet": "Planet & Giving"
+ },
+ "panels": {
+ "liveNews": "Live News",
+ "markets": "Markets",
+ "marketImplications": "AI Market Implications",
+ "map": "Global Situation",
+ "techMap": "Global Tech",
+ "techHubs": "Hot Tech Hubs",
+ "status": "System Status",
+ "insights": "AI Insights",
+ "strategicPosture": "AI Strategic Posture",
+ "cii": "Country Instability",
+ "strategicRisk": "Strategic Risk Overview",
+ "intel": "Intel Feed",
+ "gdeltIntel": "Live Intelligence",
+ "cascade": "Infrastructure Cascade",
+ "politics": "World News",
+ "us": "United States",
+ "europe": "Europe",
+ "middleeast": "Middle East",
+ "africa": "Africa",
+ "latam": "Latin America",
+ "asia": "Asia-Pacific",
+ "energy": "Energy & Resources",
+ "energyComplex": "Energy Complex",
+ "oilInventories": "Oil Inventories",
+ "energyCrisis": "Energy Crisis Tracker",
+ "goldIntelligence": "Gold Intelligence",
+ "gov": "Government",
+ "thinktanks": "Think Tanks",
+ "polymarket": "Predictions",
+ "commodities": "Metals & Materials",
+ "economic": "Macro Stress",
+ "tradePolicy": "Trade Policy",
+ "supplyChain": "Supply Chain",
+ "finance": "Financial",
+ "tech": "Technology",
+ "crypto": "Crypto",
+ "heatmap": "Sector Heatmap",
+ "ai": "AI/ML",
+ "layoffs": "Layoffs Tracker",
+ "monitors": "My Monitors",
+ "satelliteFires": "Fires",
+ "macroSignals": "BTC Regime",
+ "etfFlows": "BTC ETF Tracker",
+ "stablecoins": "Stablecoins",
+ "deduction": "Deduct Situation",
+ "wsbTickerScanner": "WSB Ticker Scanner",
+ "ucdpEvents": "Armed Conflict Events",
+ "giving": "Global Giving",
+ "displacement": "UNHCR Displacement",
+ "climate": "Climate Anomalies",
+ "populationExposure": "Population Exposure",
+ "securityAdvisories": "Security Advisories",
+ "orefSirens": "Israel Sirens",
+ "telegramIntel": "Telegram Intel",
+ "startups": "Startups & VC",
+ "vcblogs": "VC Insights & Essays",
+ "regionalStartups": "Global Startup News",
+ "unicorns": "Unicorn Tracker",
+ "accelerators": "Accelerators & Demo Days",
+ "security": "Cybersecurity",
+ "policy": "AI Policy & Regulation",
+ "regulation": "AI Regulation Dashboard",
+ "finRegulation": "Financial Regulation",
+ "hardware": "Semiconductors & Hardware",
+ "cloud": "Cloud & Infrastructure",
+ "dev": "Developer Community",
+ "github": "GitHub Trending",
+ "ipo": "IPO & SPAC",
+ "funding": "Funding & VC",
+ "producthunt": "Product Hunt",
+ "events": "Tech Events",
+ "serviceStatus": "Service Status",
+ "internetDisruptions": "Internet Disruptions",
+ "internetDisruptionsTabs": {
+ "outages": "Outages",
+ "ddos": "DDoS",
+ "anomalies": "Anomalies"
+ },
+ "techReadiness": "Tech Readiness Index",
+ "gccInvestments": "GCC Investments",
+ "geoHubs": "Geopolitical Hubs",
+ "liveWebcams": "Live Webcams",
+ "windyWebcams": "Windy Live Webcam",
+ "gulfEconomies": "Gulf Economies",
+ "groceryBasket": "Grocery Index",
+ "groceryItem": "Item",
+ "bigmac": "Real Big Mac Index",
+ "bigmacDesc": "Big Mac prices across Middle East countries (Big Mac Index)",
+ "bigmacWow": "WoW",
+ "bigmacCountry": "Country",
+ "faoFoodPriceIndex": "FAO Food Price Index",
+ "fuelPrices": "Fuel Prices",
+ "fuelPricesDesc": "Retail gasoline and diesel prices across 30+ countries worldwide",
+ "fuelPricesCountry": "Country",
+ "fuelPricesGasoline": "Gasoline",
+ "fuelPricesDiesel": "Diesel",
+ "fuelPricesSource": "Source",
+ "groceryBasketDesc": "Grocery basket price comparison across 24 countries worldwide",
+ "gulfIndices": "Gulf Indices",
+ "gulfCurrencies": "Gulf Currencies",
+ "gulfOil": "Gulf Oil",
+ "airlineIntel": "✈️ Airline Intelligence",
+ "consumerPrices": "Consumer Prices",
+ "fearGreed": "Fear & Greed",
+ "marketBreadth": "Market Breadth",
+ "climateNews": "Climate News"
+ },
+ "common": {
+ "loading": "Loading...",
+ "error": "Error",
+ "noData": "No data available",
+ "noDataAvailable": "No data available",
+ "retrying": "Retrying...",
+ "failedToLoad": "Temporarily unavailable — retrying",
+ "noDataShort": "No data",
+ "upstreamUnavailable": "Upstream API unavailable — will retry automatically",
+ "loadingUcdpEvents": "Loading armed conflict events",
+ "loadingStablecoins": "Loading stablecoins...",
+ "scanningThermalData": "Scanning thermal data",
+ "calculatingExposure": "Calculating exposure",
+ "computingSignals": "Computing signals...",
+ "loadingEtfData": "Loading ETF data...",
+ "loadingGiving": "Loading global giving data",
+ "loadingDisplacement": "Loading displacement data",
+ "loadingClimateData": "Loading climate data",
+ "failedTechReadiness": "Tech readiness data temporarily unavailable",
+ "failedRiskOverview": "Risk overview temporarily unavailable",
+ "failedPredictions": "Predictions temporarily unavailable",
+ "failedCII": "CII data temporarily unavailable",
+ "failedDependencyGraph": "Dependency graph temporarily unavailable",
+ "failedIntelFeed": "Intelligence feed temporarily unavailable",
+ "failedMarketData": "Market data temporarily unavailable",
+ "failedSectorData": "Sector data temporarily unavailable",
+ "failedCommodities": "Commodities data temporarily unavailable",
+ "failedCryptoData": "Crypto data temporarily unavailable",
+ "rateLimitedMarket": "Market data temporarily unavailable (rate limited) — retrying shortly",
+ "noNewsAvailable": "No news available",
+ "noActiveTechHubs": "No active tech hubs",
+ "noActiveGeoHubs": "No active geopolitical hubs",
+ "allSourcesDisabled": "All sources disabled",
+ "allIntelSourcesDisabled": "All Intel sources disabled",
+ "noEventsInCategory": "No events in this category",
+ "exportCsv": "Export CSV",
+ "exportJson": "Export JSON",
+ "exportData": "Export Data",
+ "selectAll": "Select All",
+ "selectNone": "Select None",
+ "unrest": "Unrest",
+ "conflict": "Conflict",
+ "security": "Mil. Activity",
+ "information": "Information",
+ "shareStory": "Share story",
+ "exportImage": "Export Image",
+ "exportPdf": "Export PDF",
+ "new": "NEW",
+ "live": "LIVE",
+ "cached": "CACHED",
+ "unavailable": "UNAVAILABLE",
+ "close": "Close",
+ "cancel": "Cancel",
+ "currentVariant": "(current)",
+ "retry": "Retry",
+ "refresh": "Refresh",
+ "all": "All"
+ },
+ "connectivity": {
+ "offlineCached": "Offline — showing cached data from {{freshness}}.",
+ "offlineUnavailable": "Offline — live data is currently unavailable.",
+ "cachedFallback": "Live data unavailable — showing cached data from {{freshness}}."
+ },
+ "premium": {
+ "pro": "PRO",
+ "lockedDesc": "Requires a World Monitor license key",
+ "signInToUnlock": "Sign in to unlock premium features",
+ "signIn": "Sign In to Unlock",
+ "upgradeDesc": "Upgrade to Pro for full access to premium analytics",
+ "upgradeToPro": "Upgrade to Pro",
+ "features": {
+ "orefSirens1": "Real-time Israel missile & rocket alerts",
+ "orefSirens2": "Siren zone mapping with threat classification",
+ "telegramIntel1": "Curated Telegram OSINT channels",
+ "telegramIntel2": "Near-real-time conflict & geopolitical updates"
+ }
+ },
+ "auth": {
+ "signIn": "Sign In",
+ "createAccount": "Create account",
+ "settings": "Settings"
+ },
+ "mcp": {
+ "connectPanel": "Connect MCP",
+ "modalTitle": "Connect MCP Server",
+ "serverUrl": "Server URL",
+ "authHeader": "Auth Header",
+ "optional": "optional",
+ "apiKey": "API Key",
+ "apiKeyPlaceholder": "Paste your API key",
+ "useCustomHeaders": "Advanced: use custom headers ↓",
+ "useApiKey": "← Use API key",
+ "connectBtn": "Connect & List Tools",
+ "connecting": "Connecting...",
+ "foundTools": "Found {{count}} tool(s)",
+ "connectFailed": "Connection failed",
+ "selectTool": "Select a tool",
+ "toolArgs": "Arguments (JSON)",
+ "panelTitle": "Panel Title",
+ "panelTitlePlaceholder": "My MCP Panel",
+ "refreshEvery": "Refresh every",
+ "seconds": "seconds",
+ "addPanel": "Add Panel",
+ "configure": "Configure MCP",
+ "refreshNow": "Refresh now",
+ "invalidJson": "Invalid JSON",
+ "confirmDelete": "Remove this MCP panel?",
+ "quickConnect": "Quick Connect",
+ "or": "or enter a custom server",
+ "generatingVisualization": "Building visualization...",
+ "visualizationFailed": "Visualization failed"
+ },
+ "widgets": {
+ "confirmDelete": "Remove this widget permanently?",
+ "chatTitle": "Widget Builder",
+ "modifyTitle": "Modify Widget",
+ "inputPlaceholder": "Describe your widget...",
+ "addToDashboard": "Add to Dashboard",
+ "applyChanges": "Apply Changes",
+ "send": "Send",
+ "modifyWithAi": "Modify widget with AI",
+ "ready": "Widget ready: {{title}}",
+ "fetching": "Fetching {{target}}...",
+ "requestTimedOut": "Request timed out. Please try again.",
+ "serverError": "Server error: {{status}}",
+ "unknownError": "Unknown error",
+ "generatedWidget": "Generated widget: {{title}}",
+ "checkingConnection": "Checking widget access…",
+ "preflightConnected": "Connected to the widget agent",
+ "preflightInvalidKey": "Widget key rejected. Update wm-widget-key and reload.",
+ "preflightUnavailable": "Widget agent is temporarily unavailable.",
+ "preflightAiUnavailable": "AI backend is unavailable. Try again later.",
+ "readyToGenerate": "Ready to generate. Pick an example or describe your widget.",
+ "readyToApply": "Preview ready for {{title}}. Review it, then add it to the dashboard.",
+ "modifyHint": "Previewing the current widget. Submit a change request to revise it.",
+ "generating": "Generating…",
+ "examplesTitle": "Prompt ideas",
+ "previewTitle": "Live Preview",
+ "phaseChecking": "Checking",
+ "phaseReadyToPrompt": "Ready",
+ "phaseFetching": "Fetching",
+ "phaseComposing": "Composing",
+ "phaseComplete": "Ready",
+ "phaseError": "Error",
+ "previewCheckingHeading": "Connecting the widget builder",
+ "previewReadyHeading": "Describe the widget you want",
+ "previewFetchingHeading": "Fetching live WorldMonitor data",
+ "previewComposingHeading": "Composing the widget layout",
+ "previewErrorHeading": "The preview needs attention",
+ "previewCheckingCopy": "We are validating your widget key and backend availability before enabling generation.",
+ "previewReadyCopy": "Use a prompt example or describe the exact live view you want. The preview will update here before you save it.",
+ "previewFetchingCopy": "The agent is calling approved WorldMonitor endpoints and shaping the dataset for the widget.",
+ "previewComposingCopy": "The preview is rendering with the latest live data and dashboard styling.",
+ "previewErrorCopy": "Fix the issue, then retry. Your existing widgets are unaffected.",
+ "createInteractive": "Create Interactive Widget",
+ "proBadge": "PRO",
+ "preflightProUnavailable": "PRO widget agent unavailable. Check PRO_WIDGET_KEY on the server.",
+ "preflightInvalidProKey": "PRO key rejected. Update wm-pro-key and reload.",
+ "preflightProSubscriptionRequired": "Pro subscription required. If you just upgraded, refresh the page; otherwise contact support.",
+ "preflightProRequired": "Pro subscription required to use the Widget Builder. Upgrade to unlock.",
+ "examples": {
+ "oilGold": "Show me today's crude oil price versus gold",
+ "cryptoMovers": "Create a widget for the top crypto movers in the last 24 hours",
+ "flightDelays": "Summarize the worst international flight delays right now",
+ "conflictHotspots": "Map the latest UCDP conflict hotspots with short labels"
+ },
+ "proExamples": {
+ "interactiveChart": "Interactive Chart.js chart comparing oil and gold prices",
+ "sortableTable": "Sortable crypto price table with search filter",
+ "animatedCounters": "Animated counters for key economic indicators",
+ "tabbedComparison": "Tabbed comparison of conflict events by region"
+ }
+ },
+ "countryBrief": {
+ "levels": {
+ "critical": "Critical",
+ "high": "High",
+ "elevated": "Elevated",
+ "moderate": "Moderate",
+ "normal": "Normal",
+ "low": "Low"
+ },
+ "trends": {
+ "rising": "Rising",
+ "falling": "Falling",
+ "stable": "Stable"
+ },
+ "fallback": {
+ "instabilityIndex": "**Instability Index: {{score}}/100** ({{level}}, {{trend}})",
+ "protestsDetected": "{{count}} active protests detected",
+ "aircraftTracked": "{{count}} military aircraft tracked",
+ "vesselsTracked": "{{count}} military vessels tracked",
+ "internetOutages": "{{count}} internet disruptions",
+ "recentEarthquakes": "{{count}} recent earthquakes",
+ "stockIndex": "Stock index: {{value}}",
+ "activeStrikes": "{{count}} active strikes detected"
+ }
+ },
+ "contextMenu": {
+ "openCountryBrief": "Open Country Brief",
+ "copyCoordinates": "Copy Coordinates"
+ },
+ "components": {
+ "deckgl": {
+ "views": {
+ "global": "Global",
+ "americas": "Americas",
+ "mena": "MENA",
+ "europe": "Europe",
+ "asia": "Asia",
+ "latam": "Latin America",
+ "africa": "Africa",
+ "oceania": "Oceania"
+ },
+ "layerHelp": {
+ "title": "Map Layers Guide"
+ },
+ "legend": {
+ "title": "LEGEND"
+ }
+ },
+ "map": {
+ "hideMap": "Hide Map",
+ "showMap": "Show Map",
+ "clearTrails": "Clear trails"
+ },
+ "panel": {
+ "addPanel": "Add Panel",
+ "closePanel": "Close panel",
+ "collapsePanel": "Collapse panel",
+ "dragToResize": "Drag to resize (double-click to reset)",
+ "expandPanel": "Expand panel",
+ "openSettings": "Open Settings",
+ "showMethodologyInfo": "Show methodology info"
+ },
+ "settings": {
+ "exportSettings": "Export Settings",
+ "importSettings": "Import Settings",
+ "importSuccess": "Imported {{count}} settings",
+ "importFailed": "Failed to import settings"
+ },
+ "aiTokens": {
+ "infoTooltip": "AI Tokens Prices and 24h/7d performance for AI and machine learning sector tokens — compute networks, decentralized AI infrastructure, and data protocols."
+ },
+ "airlineIntel": {
+ "infoTooltip": "Airline Intelligence Real-time airline operational data: route suspensions, airspace closures, carrier advisories, and emergency diversions. Sourced from aviation authority NOTAMs and carrier bulletins."
+ },
+ "altTokens": {
+ "infoTooltip": "Alt Tokens Prices and 24h/7d performance for alternative and emerging crypto tokens outside the major-cap tiers."
+ },
+ "bigmac": {
+ "infoTooltip": "Real Big Mac Index The Economist's Big Mac Index measures purchasing power parity by comparing McDonald's burger prices across countries. Updated when new data is published (quarterly/annually)."
+ },
+ "cascade": {
+ "infoTooltip": "Cascade Analysis Models infrastructure dependencies:- Subsea cables, pipelines, ports, chokepoints
- Select infrastructure to simulate failure
- Shows affected countries and capacity loss
- Identifies redundant routes
Data from TeleGeography and industry sources."
+ },
+ "centralBankWatch": {
+ "infoTooltip": "Central Bank Watch Latest statements, rate decisions, and communications from the Federal Reserve, ECB, Bank of England, and other major central banks worldwide."
+ },
+ "chatAnalyst": {
+ "infoTooltip": "WM Analyst AI analyst with live context across geopolitical, market, military, and economic domains. Powered by Claude with real-time data injected from active WorldMonitor feeds."
+ },
+ "chokepointStrip": {
+ "infoTooltip": "Live status for the seven global oil & gas shipping chokepoints. Flow estimates calibrated from Portwatch DWT + AIS observations. See /docs/methodology/chokepoints for methodology.",
+ "title": "Chokepoint Status"
+ },
+ "cii": {
+ "infoTooltip": "Methodology- Unrest: civil disorder & protests
- Conflict: armed conflict intensity
- Security: military flights/vessels over territory
- Information: news velocity and focal point correlation
- Hotspot proximity boost (strategic locations)
U:C:S:I values show component scores. Focal Point Detection correlates news entities with map signals for accurate scoring."
+ },
+ "climate": {
+ "infoTooltip": "Climate Anomaly Monitor 7-day temperature and precipitation anomalies versus 1991-2020 monthly normals. Data from Open-Meteo (ERA5 reanalysis).- Extreme: >5°C or >12mm/day anomaly
- Moderate: >3°C or >6mm/day anomaly
Tracks climate-sensitive zones for sustained departures from WMO baselines."
+ },
+ "climateNews": {
+ "infoTooltip": "Climate News Latest environment and climate intelligence from Carbon Brief, UNEP, NASA, The Guardian, and other authoritative sources. Updated every 30 minutes."
+ },
+ "commodities": {
+ "infoTooltip": "Commodities Tradeable non-energy commodity tape focused on metals and materials. Energy prices live in Energy Complex; macro stress indicators live in Macro Stress."
+ },
+ "conservationWins": {
+ "infoTooltip": "Conservation Wins Positive conservation news: species population recoveries, habitat restoration milestones, and de-extinction efforts. Sourced from IUCN, WWF, and conservation research publications."
+ },
+ "consumerPrices": {
+ "infoTooltip": "Consumer Prices Real-time basket price tracking:- Overview: Essentials index, value basket, and week-on-week change
- Categories: Per-category price trends with 30-day range
- Movers: Biggest rising and falling items this week
- Spread: Price variance across retailers for the same basket
- World: Official IMF annual inflation for every reporting economy
Data sourced from live retailer price scraping and IMF WEO."
+ },
+ "cotPositioning": {
+ "infoTooltip": "CFTC COT Positioning Commitments of Traders report: net positioning of commercials, large speculators, and small traders across commodity and financial futures. Published weekly (Friday). Source: CFTC."
+ },
+ "counters": {
+ "infoTooltip": "Live Counters Real-time extrapolated counters for global metrics: military spending, natural disasters, disease burden, and more. Figures are statistical estimates based on annual data rates."
+ },
+ "crypto": {
+ "infoTooltip": "Crypto Live prices, 24h changes, and volume for major cryptocurrencies. Data sourced from CoinGecko."
+ },
+ "dailyMarketBrief": {
+ "infoTooltip": "Daily Market Brief AI-generated daily summary of global market conditions, key macro trends, and sector-level signals — refreshes each trading day."
+ },
+ "defensePatents": {
+ "infoTooltip": "Weekly defense and dual-use patent filings by Raytheon, Lockheed, Huawei, DARPA, and other strategic organizations. Categories: H04B (comms), H01L (semiconductors), F42B (ammunition), G06N (AI), C12N (biotech). Source: USPTO PatentsView.",
+ "title": "R&D Signal"
+ },
+ "defiTokens": {
+ "infoTooltip": "DeFi Tokens Prices and 24h/7d performance for major decentralized finance protocol tokens — DEXes, lending platforms, and yield protocols."
+ },
+ "disasterCorrelation": {
+ "infoTooltip": "Disaster Cascade Detects converging natural disaster and infrastructure signals:- Correlates earthquakes, wildfires, floods, and weather extremes
- Tracks cascading effects on infrastructure and supply chains
- Highlights regions with compounding disaster risk
Click a card to zoom to the affected region."
+ },
+ "diseaseOutbreaks": {
+ "infoTooltip": "Disease Outbreaks Active outbreak alerts from WHO, ProMED, and health ministries. Tracks confirmed outbreaks, affected regions, case counts, and threat levels. Updated as new reports are published.",
+ "methodologyNote": "Alert / Warning / Watch levels come from an editorial keyword classifier — see /docs/methodology/disease-alert-level for the keyword table and limitations.",
+ "title": "Disease Outbreaks"
+ },
+ "displacement": {
+ "infoTooltip": "UNHCR Displacement Data Global refugee, asylum seeker, and IDP counts from UNHCR.- Origins: Countries people flee FROM
- Hosts: Countries hosting refugees
- Crisis badges: >1M | High: >500K displaced
Data updates yearly. CC BY 4.0 license."
+ },
+ "earningsCalendar": {
+ "infoTooltip": "Earnings Calendar Upcoming and recent earnings releases for major public companies. Shows EPS estimates vs actuals, revenue, and beat/miss classification. Source: financial data providers.",
+ "title": "Earnings Calendar"
+ },
+ "economic": {
+ "infoTooltip": "Macro Stress Macro gauges, government spending, and central bank data:- Indicators: VIX, rates, yield curve, labor, inflation
- Gov: Recent US government contract awards
- Central Banks: BIS policy rates and exchange rate data
"
+ },
+ "economicCalendar": {
+ "infoTooltip": "Economic Calendar Scheduled macroeconomic releases: CPI, NFP, GDP, PMI, and central bank decisions. Shows forecast vs actual vs prior. High-impact events highlighted. Source: economic data providers."
+ },
+ "economicCorrelation": {
+ "infoTooltip": "Economic Warfare Detects converging economic pressure signals:- Sanctions, trade restrictions, and currency movements
- Commodity disruptions linked to geopolitical actors
- Cross-domain correlation between economic and security events
Click a card to zoom to the affected region."
+ },
+ "energyComplex": {
+ "infoTooltip": "Energy Complex Physical and tradeable energy signals in one place:- EIA metrics: WTI, Brent, US production, and inventories
- Live tape: Tradeable energy prices like crude and natural gas
- Purpose: Separate physical energy stress from broader macro and commodity panels
"
+ },
+ "escalationCorrelation": {
+ "infoTooltip": "Escalation Monitor Detects converging geopolitical signals:- Correlates military movements, conflict events, and news spikes
- Scores convergence zones by severity (critical/high/medium/low)
- Tracks escalation or de-escalation trends over time
Click a card to zoom to the region on the map."
+ },
+ "etfFlows": {
+ "infoTooltip": "BTC ETF Tracker Tracks daily estimated fund flows for US spot Bitcoin ETFs:- Inflow/outflow direction and magnitude
- Volume and price change per fund
- Net aggregate flow across all tracked ETFs
"
+ },
+ "faoFoodPriceIndex": {
+ "infoTooltip": "FAO Food Price Index The UN Food and Agriculture Organization's FFPI tracks international export prices for a basket of food commodities (Cereals, Dairy, Meat, Oils, Sugar), base 2014-2016=100. Updated monthly on the first Friday of each month."
+ },
+ "forecast": {
+ "infoTooltip": "AI Forecasts AI-generated probability estimates for geopolitical and economic events:- Cross-domain coverage: conflict, markets, supply chain, cyber, political
- Each forecast shows estimated probability, confidence level, and time horizon
- Calibrated against prediction market baselines where available
Forecasts update as new intelligence signals arrive. Filter by domain using the tabs above."
+ },
+ "fsi": {
+ "infoTooltip": "Financial Stress Indicator Kansas City Fed Financial Stress Index (KCFSI): measures stress in US financial markets using 11 indicators including yield spreads, volatility, and cross-correlations. Positive values indicate above-average stress. Source: Federal Reserve Bank of Kansas City.",
+ "title": "Financial Stress Indicator"
+ },
+ "fuelPrices": {
+ "infoTooltip": "Fuel Prices Retail pump prices for gasoline and diesel across 30+ countries, normalized to USD/liter for comparison. Prices are sourced from official government open-data programs and updated weekly. Cheapest and most expensive countries are highlighted. WoW change shown when prior week data is available."
+ },
+ "gdeltIntel": {
+ "infoTooltip": "News Intelligence Real-time global news monitoring:- Curated topic categories (conflicts, cyber, etc.)
- Articles from 100+ languages translated
- Updates every 15 minutes
- 14-day media tone & volume trend per topic
- 14-day media tone & volume trend per topic
Source: GDELT Project (gdeltproject.org)"
+ },
+ "geoHubs": {
+ "infoTooltip": "Geopolitical Activity Hubs
Shows regions with the most news activity.
Hub types:
• 🏛️ Capitals — World capitals and government centers
• ⚔️ Conflict Zones — Active conflict areas
• ⚓ Strategic — Chokepoints and key regions
• 🏢 Organizations — UN, NATO, IAEA, etc.
Activity levels:
• High — Breaking news or 70+ score
• Elevated — Score 40-69
• Low — Score below 40
Click a hub to zoom to its location."
+ },
+ "giving": {
+ "infoTooltip": "Global Giving Activity Index Composite index tracking personal giving across crowdfunding platforms and crypto wallets.- Platforms: GoFundMe, GlobalGiving, JustGiving campaign sampling
- Crypto: On-chain charity wallet inflows (Endaoment, Giving Block)
- Institutional: OECD ODA, CAF World Giving Index, Candid grants
Index is directional (not exact dollar amounts). Combines live sampling with published annual reports."
+ },
+ "goldIntelligence": {
+ "infoTooltip": "Gold Intelligence Gold spot price, gold/silver ratio, gold vs platinum premium, cross-currency XAU prices (EUR, GBP, JPY, CNY, INR, CHF), and CFTC managed money positioning. Sources: Yahoo Finance, CFTC COT."
+ },
+ "groceryBasket": {
+ "infoTooltip": "Grocery Index Global grocery basket price comparison across 24 countries — tracks real-world consumer goods inflation beyond headline CPI."
+ },
+ "gulfEconomies": {
+ "infoTooltip": "Gulf Economies Real-time Gulf stock indices, currency rates, and oil prices for GCC economies (Saudi Arabia, UAE, Kuwait, Qatar, Bahrain, Oman)."
+ },
+ "heatmap": {
+ "infoTooltip": "Sector Heatmap S&P 500 sector performance at a glance. Color intensity reflects the magnitude of daily change. Green = gains, Red = losses."
+ },
+ "hormuzTracker": {
+ "infoTooltip": "Hormuz Trade Tracker Live AIS vessel tracking through the Strait of Hormuz, the world's most critical oil chokepoint. Monitors tanker counts, disruption events, and weekly flow changes. Approximately 20% of global oil supply transits here daily.",
+ "title": "Hormuz Trade Tracker"
+ },
+ "insights": {
+ "infoTooltip": "AI-Powered Analysis
• World Brief: AI summary (Groq/OpenRouter)
• Sentiment: News tone analysis
• Velocity: Fast-moving stories
• Focal Points: Correlates news entities with map signals (military, protests, outages)
Desktop only • Powered by Llama 3.3 + Focal Point Detection"
+ },
+ "intelligenceFindings": {
+ "title": "Intelligence Findings"
+ },
+ "internetDisruptions": {
+ "infoTooltip": "Internet Disruptions Real-time internet outages, DDoS attacks, and traffic anomalies from global network monitoring:- Outages: BGP-detected connectivity failures by country
- DDoS: Volumetric attack reports by protocol and vector
- Anomalies: Unusual traffic patterns from Cloudflare Radar
"
+ },
+ "investments": {
+ "infoTooltip": "Database of Saudi Arabia and UAE foreign direct investments in global critical infrastructure. Click a row to fly to the investment on the map."
+ },
+ "liquidityShifts": {
+ "infoTooltip": "Liquidity Shifts High-liquidity positioning and daily shift monitor for oil, gold, silver, equity index futures, and top US stocks. COT asset-manager net and leveraged-funds net vs daily price change.",
+ "title": "Liquidity Shifts"
+ },
+ "liveWebcams": {
+ "infoTooltip": "Live Webcams Live feeds from geopolitically significant locations worldwide: conflict zones, capital cities, border crossings, and strategic chokepoints. Streams sourced from YouTube and public networks."
+ },
+ "macroSignals": {
+ "infoTooltip": "BTC Regime Composite signal dashboard for Bitcoin positioning and risk appetite:- Liquidity: Net Fed liquidity proxy
- Flow: BTC vs QQQ 5-day returns
- Regime: QQQ vs XLP rotation (risk-on/off)
- BTC Trend: Price vs SMA50/200, Mayer Multiple
- Hash Rate: 30-day network hash change
- Fear & Greed: Market sentiment index
This panel is for regime and positioning, not broad macro data."
+ },
+ "macroTiles": {
+ "infoTooltip": "Macro Indicators Key macroeconomic indicators for the US and Euro Area: CPI, unemployment, GDP, and central bank rates. Values shown with prior period delta. Source: FRED (Federal Reserve Economic Data)."
+ },
+ "marketImplications": {
+ "infoTooltip": "AI Market Implications LLM-generated trade signals derived from live geopolitical, commodity, and macro state after each forecast cycle.- Direction: LONG / SHORT / HEDGE with confidence rating
- Timeframe: 1W, 2W, 1M, or 3M horizon
- Driver: Key catalyst behind the signal
- Risk: Caveat or invalidation condition
For informational purposes only. Not investment advice.",
+ "title": "AI Market Implications"
+ },
+ "markets": {
+ "infoTooltip": "Markets Real-time stock indices, equities, and crypto prices. Customize your watchlist with the Watchlist button. Sparklines show recent price trend."
+ },
+ "militaryCorrelation": {
+ "infoTooltip": "Force Posture Correlates military activity with geopolitical context:- Military aircraft and naval vessel concentrations by region
- Cross-references with active conflict zones and news spikes
- Highlights unusual force buildups or repositioning
Click a card to zoom to the region on the map."
+ },
+ "monitors": {
+ "infoTooltip": "Keyword Monitors Add any term to track live news mentions across all WorldMonitor sources. Results update in real time as matching articles are ingested."
+ },
+ "oilInventories": {
+ "infoTooltip": "Oil Inventories US crude oil and SPR stockpiles (EIA weekly), natural gas storage, EU gas fill percentage (GIE AGSI+), and OECD oil stocks days-of-cover (IEA monthly)."
+ },
+ "orefSirens": {
+ "infoTooltip": "Israel Sirens
Real-time rocket and missile siren alerts from Israel Home Front Command.
Data is polled every 10 seconds. A pulsing red indicator means active sirens are sounding."
+ },
+ "pizzint": {
+ "title": "Pentagon Pizza Index"
+ },
+ "populationExposure": {
+ "infoTooltip": "Population Exposure Estimates Estimated population within event impact radius. Based on WorldPop country density data.- Conflict: 50km radius
- Earthquake: 100km radius
- Flood: 100km radius
- Wildfire: 30km radius
"
+ },
+ "positioning247": {
+ "infoTooltip": "24/7 Positioning Perpetual contract positioning stress from Hyperliquid. Trades around the clock, showing what's building when traditional markets are closed. Composite score (0-100) from funding rate, volume, open interest, and spot-perp basis. Green = longs crowded (bullish lean), red = shorts crowded (bearish lean).",
+ "title": "24/7 Positioning"
+ },
+ "prediction": {
+ "infoTooltip": "Prediction Markets Real-money forecasting markets:- Prices reflect crowd probability estimates
- Higher volume = more reliable signal
- Geopolitical and current events focus
Sources: Polymarket, Kalshi"
+ },
+ "radiationWatch": {
+ "infoTooltip": "Seeded EPA RadNet and Safecast readings with anomaly scoring and source-confidence synthesis. This panel answers what is normal, what is elevated, and which anomalies are confirmed versus tentative.",
+ "title": "Radiation Watch"
+ },
+ "regulation": {
+ "infoTooltip": "Regulation Regulatory actions, policy changes, and enforcement updates from global regulators: financial, tech, energy, and trade authorities. Sourced from official government and regulatory body publications."
+ },
+ "renewable": {
+ "infoTooltip": "Renewable Energy Share of electricity generation from renewables by region and year, plus US installed capacity for solar, wind, and coal (MW). Sources: World Bank (EG.ELC.RNEW.ZS) and EIA."
+ },
+ "sanctionsPressure": {
+ "infoTooltip": "OFAC sanctions designations from the SDN and Consolidated Lists. Shows which countries face the highest designation pressure, what programs are driving it, and what has been newly added since the last update.",
+ "title": "Sanctions & Designations"
+ },
+ "satelliteFires": {
+ "infoTooltip": "NASA FIRMS VIIRS satellite thermal detections across monitored conflict regions. High-intensity = brightness >360K & confidence >80%."
+ },
+ "securityAdvisories": {
+ "infoTooltip": "Security Advisories
Travel advisories and security alerts from government foreign affairs agencies:
Sources:
🇺🇸 US State Dept Travel Advisories
🇦🇺 AU DFAT Smartraveller
🇬🇧 UK FCDO Travel Advice
🇳🇿 NZ MFAT SafeTravel
Levels:
🟥 Do Not Travel
🟧 Reconsider Travel
🟨 Exercise Caution
🟩 Normal Precautions"
+ },
+ "serviceStatus": {
+ "infoTooltip": "Service Status Live operational status of major cloud platforms, financial infrastructure, and critical internet services. Sourced from official status pages (AWS, Azure, GCP, Cloudflare, exchanges). Incidents may indicate broader disruption events."
+ },
+ "socialVelocity": {
+ "infoTooltip": "Social Velocity Trending topics and viral content with geopolitical significance. Measures social media velocity: how rapidly a story spreads relative to baseline. Helps surface emerging narratives before they hit mainstream news."
+ },
+ "stablecoins": {
+ "infoTooltip": "Stablecoins Peg health, market cap, and 24h volume for major USD-pegged tokens (USDT, USDC, DAI, BUSD). A broken peg signals systemic risk."
+ },
+ "stockAnalysis": {
+ "infoTooltip": "Premium Stock Analysis AI-powered analysis for stocks in your watchlist — buy/hold/sell signals, price targets, key risks, and catalysts updated daily."
+ },
+ "stockBacktest": {
+ "infoTooltip": "Premium Backtesting Historical performance backtests for stocks in your watchlist — past returns, max drawdown, and volatility metrics."
+ },
+ "strategicPosture": {
+ "infoTooltip": "MethodologyAggregates military aircraft and naval vessels by theater.
- Normal: Baseline activity
- Elevated: Above threshold (50+ aircraft)
- Critical: High concentration (100+ aircraft)
Strike Capable: Tankers + AWACS + Fighters present in sufficient numbers for sustained operations.
"
+ },
+ "strategicRisk": {
+ "infoTooltip": "Methodology Composite score (0-100) blending:- 50% Country Instability (top 5 weighted)
- 30% Geographic convergence zones
- 20% Infrastructure incidents
Auto-refreshes every 5 minutes."
+ },
+ "supplyChain": {
+ "infoTooltip": "Supply Chain Monitor Global logistics and resource tracking:- Chokepoints: Maritime transit status, disruption scores, and vessel counts
- Shipping: Baltic Dry Index and freight rate trends
- Minerals: Critical mineral concentration risk (HHI) and top producers
Click a chokepoint card to expand transit history chart."
+ },
+ "techEvents": {
+ "infoTooltip": "Tech Events Upcoming and recent technology events: conferences, product launches, regulatory hearings, and earnings calls. Aggregated from official event calendars and industry sources."
+ },
+ "techHubs": {
+ "infoTooltip": "Tech Hub Activity
Shows tech hubs with the most news activity.
Activity levels:
• High — Breaking news or 50+ score
• Elevated — Score 20-49
• Low — Score below 20
Click a hub to zoom to its location."
+ },
+ "techReadiness": {
+ "infoTooltip": "Global Tech Readiness
Composite score (0-100) based on World Bank data:
Metrics shown:
🌐 Internet Users (% of population)
📱 Mobile Subscriptions (per 100 people)
🔬 R&D Expenditure (% of GDP)
Weights: R&D (35%), Internet (30%), Broadband (20%), Mobile (15%)
— = No recent data available
Source: World Bank Open Data (2019-2024)"
+ },
+ "telegramIntel": {
+ "infoTooltip": "Real-time signals from monitored Telegram OSINT channels"
+ },
+ "thermalEscalation": {
+ "infoTooltip": "Seeded FIRMS/VIIRS thermal anomaly clusters with baseline comparison, persistence tracking, and strategic context. This panel answers where thermal activity is abnormal and which clusters may signal conflict, industrial disruption, or escalation.",
+ "title": "Thermal Escalation"
+ },
+ "tradePolicy": {
+ "infoTooltip": "Trade Policy WTO baseline and tariff-impact monitoring:- Overview: WTO MFN baseline rates with US effective-rate context when available
- Tariffs: WTO MFN tariff trends vs the US effective tariff estimate
- Trade Flows: Export/import volumes with year-over-year changes
- Barriers: Technical barriers to trade (TBT/SPS notifications)
- Revenue: Monthly US customs duties revenue (US Treasury MTS data)
"
+ },
+ "ucdpEvents": {
+ "infoTooltip": "Armed Conflict Events Event-level conflict data from Uppsala University (UCDP).- State-Based: Government vs rebel group
- Non-State: Armed group vs armed group
- One-Sided: Violence against civilians
Deaths shown as best estimate (low-high range). ACLED duplicates are filtered out automatically."
+ },
+ "verification": {
+ "title": "Information Verification Checklist"
+ },
+ "worldClock": {
+ "infoTooltip": "World Clock Live local times and market session status for major global financial centers. Shows open/closed status for NYSE, LSE, TSE, SGX, and other exchanges."
+ },
+ "wsbTickerScanner": {
+ "infoTooltip": "WSB Ticker Scanner Real-time ticker mentions from r/wallstreetbets, r/stocks, and r/investing. Ranked by mention frequency and engagement. Velocity measures how rapidly a ticker gains momentum across posts."
+ },
+ "yieldCurve": {
+ "infoTooltip": "Yield Curve & Rates US Treasury yield curve (2Y, 5Y, 10Y, 30Y) and key rate spreads including the 10Y-2Y inversion indicator. Also shows Euro area rates (EURIBOR, ESTR). An inverted curve historically precedes recessions. Source: FRED."
+ }
+ },
+ "modals": {
+ "runtimeConfig": {
+ "title": "Desktop Configuration",
+ "placeholder": {
+ "setSecret": "Set secret",
+ "staged": "Staged (save with OK)"
+ },
+ "reserveEarlyAccess": "Reserve Your Early Access",
+ "skipSetup": "Skip the setup — a single World Monitor license unlocks everything. Join the waitlist for early access.",
+ "status": {
+ "invalid": "Invalid",
+ "looksInvalid": "Looks invalid",
+ "missing": "Missing",
+ "needsKeys": "Needs Keys",
+ "ready": "Ready",
+ "staged": "Staged",
+ "valid": "Valid"
+ },
+ "summary": {
+ "available": "features available",
+ "desktop": "Desktop mode",
+ "secrets": "local secrets configured",
+ "web": "Web mode (read-only, server-managed credentials)"
+ }
+ },
+ "settingsWindow": {
+ "failed": "Save failed: {{error}}",
+ "invokeFail": "Failed to run {{command}}. Check desktop log.",
+ "logCleared": "Log cleared.",
+ "noTraffic": "No traffic recorded yet.",
+ "openApiLog": "Opened API log",
+ "openLogs": "Opened logs folder",
+ "saved": "Settings saved",
+ "shellCancel": "Cancel",
+ "shellSaveClose": "Save & Close",
+ "shellSearchPlaceholder": "Search settings...",
+ "shellTitle": "World Monitor Settings",
+ "sidecarError": "Could not reach sidecar to toggle verbose mode",
+ "sidecarUnreachable": "Sidecar not reachable.",
+ "table": {
+ "time": "Time",
+ "method": "Method",
+ "path": "Path",
+ "status": "Status",
+ "duration": "Duration"
+ },
+ "validating": "Validating API keys...",
+ "verboseOff": "Verbose sidecar logging OFF (saved)",
+ "verboseOn": "Verbose sidecar logging ON (saved)",
+ "verifyFailed": "Saved verified keys. Failed: {{errors}}",
+ "worldMonitor": {
+ "apiKey": {
+ "title": "License Key",
+ "placeholder": "wm_xxxxxxxxxxxxxxxxxxxxxxxx",
+ "description": "Paste your license to unlock every data source and AI feature instantly."
+ },
+ "dividerOr": "OR",
+ "register": {
+ "title": "Reserve Your Spot",
+ "description": "We're preparing to launch World Monitor licenses. Sign up now and be first in line — early members get priority access and founding-member pricing.",
+ "submitBtn": "Join Waitlist"
+ }
+ },
+ "freePanelLimit": "Free plan: max {{max}} panels. Upgrade to PRO for unlimited."
+ },
+ "mobileWarning": {
+ "title": "Mobile View"
+ },
+ "signal": {
+ "title": "INTELLIGENCE FINDING"
+ }
+ },
+ "popups": {
+ "cyberThreat": {
+ "title": "CYBER THREAT"
+ },
+ "datacenter": {
+ "cluster": {
+ "title": "{{count}} Data Centers"
+ }
+ },
+ "gpsJamming": {
+ "title": "GPS/GNSS Interference"
+ },
+ "pipeline": {
+ "title": "PIPELINE"
+ }
+ }
+}
diff --git a/src/services/i18n.ts b/src/services/i18n.ts
index d2ffd6d8cf..40e0ea38cf 100644
--- a/src/services/i18n.ts
+++ b/src/services/i18n.ts
@@ -1,8 +1,9 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
-// English is always needed as fallback — bundle it eagerly.
-import enTranslation from '../locales/en.json';
+// Keep only first-paint English strings in the entry chunk. The full English
+// dictionary is loaded through localeModules so it can split like other locales.
+import enShellTranslation from '../locales/en.shell.json';
// Explicit-choice localStorage key. Written ONLY when the user manually picks
// a language via Settings → Language. The default detector's `i18nextLng`
@@ -21,7 +22,7 @@ const loadedLanguages = new Set();
// Lazy-load only the locale that's actually needed — all others stay out of the bundle.
const localeModules = import.meta.glob(
- ['../locales/*.json', '!../locales/en.json'],
+ ['../locales/*.json', '!../locales/en.shell.json'],
{ import: 'default' },
);
@@ -52,16 +53,13 @@ async function ensureLanguageLoaded(lng: string): Promise {
}
let translation: TranslationDictionary;
- if (normalized === 'en') {
- translation = enTranslation as TranslationDictionary;
+ const loader = localeModules[`../locales/${normalized}.json`];
+ if (!loader) {
+ console.warn(`No locale file for "${normalized}", falling back to English`);
+ const englishLoader = localeModules['../locales/en.json'];
+ translation = englishLoader ? await englishLoader() : enShellTranslation as TranslationDictionary;
} else {
- const loader = localeModules[`../locales/${normalized}.json`];
- if (!loader) {
- console.warn(`No locale file for "${normalized}", falling back to English`);
- translation = enTranslation as TranslationDictionary;
- } else {
- translation = await loader();
- }
+ translation = await loader();
}
i18next.addResourceBundle(normalized, 'translation', translation, true, true);
@@ -69,6 +67,30 @@ async function ensureLanguageLoaded(lng: string): Promise {
return normalized;
}
+function notifyLanguageResourcesLoaded(language: SupportedLanguage): void {
+ if (normalizeLanguage(i18next.language || 'en') !== language) return;
+
+ void i18next.changeLanguage(i18next.language || language).catch((error) => {
+ console.warn(`Failed to notify listeners after loading "${language}" locale`, error);
+ });
+
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('wm:i18n:resources-loaded', { detail: { language } }));
+ }
+}
+
+function preloadEnglishTranslation(): void {
+ if (loadedLanguages.has('en')) return;
+ void ensureLanguageLoaded('en')
+ .then((language) => notifyLanguageResourcesLoaded(language))
+ .catch((error) => {
+ // English now lives in its own lazy chunk. If that chunk fails, the
+ // eager shell still renders first paint, but non-shell English keys may
+ // remain raw until the next successful page load.
+ console.warn('Failed to preload full English locale', error);
+ });
+}
+
// Initialize i18n
export async function initI18n(): Promise {
if (i18next.isInitialized) {
@@ -78,8 +100,6 @@ export async function initI18n(): Promise {
return;
}
- loadedLanguages.add('en');
-
// One-time migration: i18next-browser-languagedetector previously cached
// every detection result here, so users whose browser is now French but
// who landed on `en` at any point in the past stayed stuck on English.
@@ -106,7 +126,7 @@ export async function initI18n(): Promise {
.use(detector)
.init({
resources: {
- en: { translation: enTranslation as TranslationDictionary },
+ en: { translation: enShellTranslation as TranslationDictionary },
},
supportedLngs: [...SUPPORTED_LANGUAGES],
nonExplicitSupportedLngs: true,
@@ -121,8 +141,14 @@ export async function initI18n(): Promise {
},
});
- const detectedLanguage = await ensureLanguageLoaded(i18next.language || 'en');
- if (detectedLanguage !== 'en') {
+ const detectedLanguage = normalizeLanguage(i18next.language || 'en');
+ if (detectedLanguage === 'en') {
+ preloadEnglishTranslation();
+ } else {
+ await Promise.all([
+ ensureLanguageLoaded(detectedLanguage),
+ ensureLanguageLoaded('en'),
+ ]);
// Re-trigger translation resolution now that the detected bundle is loaded.
await i18next.changeLanguage(detectedLanguage);
}
diff --git a/tests/i18n-english-shell.test.mjs b/tests/i18n-english-shell.test.mjs
new file mode 100644
index 0000000000..de565e882a
--- /dev/null
+++ b/tests/i18n-english-shell.test.mjs
@@ -0,0 +1,281 @@
+import { readdirSync, readFileSync } from 'node:fs';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ replaceRawI18nKeyPlaceholderText,
+ replaceRawI18nKeyPlaceholders,
+ translateRawI18nKeyPlaceholder,
+} from '../src/app/i18n-raw-key-healer.ts';
+
+const I18N_SOURCE = 'src/services/i18n.ts';
+const APP_SOURCE = 'src/App.ts';
+const EN_LOCALE = 'src/locales/en.json';
+const EN_SHELL_LOCALE = 'src/locales/en.shell.json';
+const COMPONENTS_DIR = 'src/components';
+const SHELL_BUDGET_BYTES = 50 * 1024;
+
+function tsFilesUnder(dir) {
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
+ const path = `${dir}/${entry.name}`;
+ if (entry.isDirectory()) return tsFilesUnder(path);
+ return entry.isFile() && entry.name.endsWith('.ts') ? [path] : [];
+ });
+}
+
+const EAGER_CHROME_FILES = [
+ 'src/App.ts',
+ 'src/app/panel-layout.ts',
+ 'src/settings-main.ts',
+ 'src/settings-window.ts',
+ ...tsFilesUnder(COMPONENTS_DIR),
+];
+
+const SHELL_KEY_PREFIXES = [
+ 'shell.',
+ 'header.',
+ 'panels.',
+ 'common.',
+ 'connectivity.',
+ 'premium.',
+ 'auth.',
+ 'mcp.',
+ 'widgets.',
+ 'countryBrief.levels.',
+ 'countryBrief.trends.',
+ 'countryBrief.fallback.',
+ 'contextMenu.',
+ 'components.deckgl.views.',
+ 'components.map.',
+ 'components.panel.',
+ 'components.settings.',
+ 'modals.runtimeConfig.',
+ 'modals.settingsWindow.',
+];
+
+function readJson(path) {
+ return JSON.parse(readFileSync(path, 'utf8'));
+}
+
+function lookup(value, path) {
+ return path.split('.').reduce((current, key) => current?.[key], value);
+}
+
+function leafPaths(value, prefix = '') {
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+ return prefix ? [prefix] : [];
+ }
+ return Object.keys(value).flatMap((key) => {
+ const path = prefix ? `${prefix}.${key}` : key;
+ return leafPaths(value[key], path);
+ });
+}
+
+function extractLiteralTranslationKeys(source) {
+ return [...source.matchAll(/\bt\(\s*['`]([^'`$]+)['`]/g)]
+ .map((match) => match[1])
+ .filter((key) => key && !key.endsWith('.'));
+}
+
+function isShellKey(key) {
+ return SHELL_KEY_PREFIXES.some((prefix) => key.startsWith(prefix))
+ || key.endsWith('.infoTooltip')
+ || key.endsWith('.methodologyNote')
+ || key.endsWith('.title');
+}
+
+function eagerChromeKeys() {
+ const keys = new Set();
+ for (const file of EAGER_CHROME_FILES) {
+ for (const key of extractLiteralTranslationKeys(readFileSync(file, 'utf8'))) {
+ if (isShellKey(key)) keys.add(key);
+ }
+ }
+ return [...keys].sort();
+}
+
+describe('English i18n shell split', () => {
+ it('keeps the full English dictionary out of static i18n imports', () => {
+ const source = readFileSync(I18N_SOURCE, 'utf8');
+
+ assert.doesNotMatch(
+ source,
+ /import\s+[^;]*['"]\.\.\/locales\/en\.json['"]/,
+ 'src/services/i18n.ts must not statically import the full English locale',
+ );
+ assert.match(
+ source,
+ /import\s+[^;]*['"]\.\.\/locales\/en\.shell\.json['"]/,
+ 'src/services/i18n.ts should statically import only the English shell locale',
+ );
+ assert.match(
+ source,
+ /!\.\.\/locales\/en\.shell\.json/,
+ 'the locale glob should exclude only en.shell.json so en.json remains lazy-loadable',
+ );
+ assert.doesNotMatch(
+ source,
+ /!\.\.\/locales\/en\.json/,
+ 'the full English locale must not be excluded from lazy locale modules',
+ );
+ assert.match(
+ source,
+ /then\(\(language\)\s*=>\s*notifyLanguageResourcesLoaded\(language\)\)/,
+ 'full English preload should notify translation listeners after the lazy bundle loads',
+ );
+ assert.match(
+ source,
+ /wm:i18n:resources-loaded/,
+ 'full English preload should expose a browser event for resource-aware UI updates',
+ );
+
+ const appSource = readFileSync(APP_SOURCE, 'utf8');
+ assert.match(
+ appSource,
+ /addEventListener\('wm:i18n:resources-loaded',\s*this\.handleI18nResourcesLoaded\)/,
+ 'App should listen for full English preload completion',
+ );
+ assert.match(
+ appSource,
+ /replaceRawI18nKeyPlaceholders\(this\.state\.container,\s*t\)/,
+ 'App should heal already-rendered raw i18n keys after full English loads',
+ );
+ });
+
+ it('keeps first-paint English shell strings bounded and in sync with en.json', () => {
+ const full = readJson(EN_LOCALE);
+ const shell = readJson(EN_SHELL_LOCALE);
+ const shellBytes = Buffer.byteLength(readFileSync(EN_SHELL_LOCALE, 'utf8'));
+
+ assert.ok(
+ shellBytes < SHELL_BUDGET_BYTES,
+ `expected ${EN_SHELL_LOCALE} to stay below ${SHELL_BUDGET_BYTES} bytes, got ${shellBytes}`,
+ );
+
+ const requiredPaths = [
+ 'shell',
+ 'header',
+ 'panels',
+ 'common',
+ 'connectivity',
+ 'components.deckgl.views',
+ 'components.map.hideMap',
+ 'components.map.showMap',
+ 'components.panel.addPanel',
+ 'modals.runtimeConfig.title',
+ 'widgets.createInteractive',
+ 'widgets.proBadge',
+ 'countryBrief.levels',
+ 'countryBrief.trends',
+ 'countryBrief.fallback',
+ ...eagerChromeKeys(),
+ ];
+
+ for (const path of [...new Set(requiredPaths)].sort()) {
+ assert.notEqual(lookup(full, path), undefined, `${path} should exist in ${EN_LOCALE}`);
+ assert.deepEqual(lookup(shell, path), lookup(full, path), `${path} should match ${EN_LOCALE}`);
+ }
+
+ });
+
+ it('keeps every English shell leaf byte-for-byte aligned with en.json', () => {
+ const full = readJson(EN_LOCALE);
+ const shell = readJson(EN_SHELL_LOCALE);
+
+ for (const path of leafPaths(shell).sort()) {
+ assert.notEqual(lookup(full, path), undefined, `${path} shell leaf should exist in ${EN_LOCALE}`);
+ assert.deepEqual(
+ lookup(shell, path),
+ lookup(full, path),
+ `${path} shell leaf should match ${EN_LOCALE} exactly`,
+ );
+ }
+ });
+
+ it('heals exact raw i18n placeholders without rewriting unresolved or prose-like text', () => {
+ const translations = new Map([
+ ['components.panel.addPanel', 'Add panel'],
+ ['header.live', 'Live'],
+ ['common.search', 'Find $& $1 $$'],
+ ]);
+ const translate = (key) => translations.get(key) ?? key;
+
+ assert.equal(
+ translateRawI18nKeyPlaceholder('components.panel.addPanel', translate),
+ 'Add panel',
+ 'exact raw keys should translate when the full English dictionary has the key',
+ );
+ assert.equal(
+ translateRawI18nKeyPlaceholder('domain/v1.2-like text', translate),
+ null,
+ 'domain/version-like prose should not match the raw i18n key heuristic',
+ );
+ assert.equal(
+ translateRawI18nKeyPlaceholder('forecast.deepMissingKey', translate),
+ null,
+ 'unresolved i18n keys should remain untouched',
+ );
+ assert.equal(
+ replaceRawI18nKeyPlaceholderText('\n common.search \t', translate),
+ '\n Find $& $1 $$ \t',
+ 'replacement should preserve outer whitespace and treat $ literally',
+ );
+ });
+
+ it('heals text nodes and translatable attributes inside a container', () => {
+ class FakeText {
+ constructor(value) {
+ this.nodeType = 3;
+ this.nodeValue = value;
+ this.childNodes = [];
+ }
+ }
+
+ class FakeElement {
+ constructor(attrs = {}, children = []) {
+ this.nodeType = 1;
+ this.childNodes = children;
+ this.attrs = { ...attrs };
+ }
+
+ getAttribute(name) {
+ return this.attrs[name] ?? null;
+ }
+
+ setAttribute(name, value) {
+ this.attrs[name] = value;
+ }
+
+ querySelectorAll() {
+ const elements = [];
+ const visit = (node) => {
+ if (node.nodeType !== 1) return;
+ if (['aria-label', 'title', 'placeholder'].some((attr) => node.getAttribute(attr))) {
+ elements.push(node);
+ }
+ for (const child of node.childNodes) visit(child);
+ };
+ for (const child of this.childNodes) visit(child);
+ return elements;
+ }
+ }
+
+ const text = new FakeText(' components.panel.addPanel\n');
+ const unresolved = new FakeText(' domain/v1.2-like text ');
+ const child = new FakeElement({ 'aria-label': 'header.live', title: 'forecast.deepMissingKey' }, [
+ text,
+ unresolved,
+ ]);
+ const root = new FakeElement({}, [child]);
+ const translations = new Map([
+ ['components.panel.addPanel', 'Add panel'],
+ ['header.live', 'Live'],
+ ]);
+
+ replaceRawI18nKeyPlaceholders(root, (key) => translations.get(key) ?? key);
+
+ assert.equal(text.nodeValue, ' Add panel\n');
+ assert.equal(unresolved.nodeValue, ' domain/v1.2-like text ');
+ assert.equal(child.getAttribute('aria-label'), 'Live');
+ assert.equal(child.getAttribute('title'), 'forecast.deepMissingKey');
+ });
+});