265 lines
8.4 KiB
JavaScript
265 lines
8.4 KiB
JavaScript
// Internationalization - EN/PL language support
|
|
// Detects browser language, persists choice, translates UI strings
|
|
|
|
const translations = {
|
|
en: {
|
|
// Navigation
|
|
'nav.dashboard': 'Dashboard',
|
|
'nav.hardware': 'Hardware',
|
|
'nav.demo': 'Live Demo',
|
|
'nav.architecture': 'Architecture',
|
|
'nav.performance': 'Performance',
|
|
'nav.applications': 'Applications',
|
|
'nav.sensing': 'Sensing',
|
|
'nav.training': 'Training',
|
|
|
|
// Dashboard
|
|
'dashboard.title': 'Revolutionary WiFi-Based Human Pose Detection',
|
|
'dashboard.subtitle': 'Human Tracking Through Walls Using WiFi Signals',
|
|
'dashboard.description': 'AI can track your full-body movement through walls using just WiFi signals. Researchers at Carnegie Mellon have trained a neural network to turn basic WiFi signals into detailed wireframe models of human bodies.',
|
|
'dashboard.status': 'System Status',
|
|
'dashboard.metrics': 'System Metrics',
|
|
'dashboard.features': 'Features',
|
|
'dashboard.liveStats': 'Live Statistics',
|
|
'dashboard.activePersons': 'Active Persons',
|
|
'dashboard.avgConfidence': 'Avg Confidence',
|
|
'dashboard.totalDetections': 'Total Detections',
|
|
'dashboard.zoneOccupancy': 'Zone Occupancy',
|
|
|
|
// Status
|
|
'status.apiServer': 'API Server',
|
|
'status.hardware': 'Hardware',
|
|
'status.inference': 'Inference',
|
|
'status.streaming': 'Streaming',
|
|
'status.dataSource': 'Data Source',
|
|
|
|
// Metrics
|
|
'metrics.cpu': 'CPU Usage',
|
|
'metrics.memory': 'Memory Usage',
|
|
'metrics.disk': 'Disk Usage',
|
|
|
|
// Benefits
|
|
'benefit.throughWalls': 'Through Walls',
|
|
'benefit.throughWallsDesc': 'Works through solid barriers with no line of sight required',
|
|
'benefit.privacy': 'Privacy-Preserving',
|
|
'benefit.privacyDesc': 'No cameras or visual recording - just WiFi signal analysis',
|
|
'benefit.realtime': 'Real-Time',
|
|
'benefit.realtimeDesc': 'Maps 24 body regions in real-time at 100Hz sampling rate',
|
|
'benefit.lowCost': 'Low Cost',
|
|
'benefit.lowCostDesc': 'Built using $30 commercial WiFi hardware',
|
|
|
|
// Stats
|
|
'stat.bodyRegions': 'Body Regions',
|
|
'stat.samplingRate': 'Sampling Rate',
|
|
'stat.accuracy': 'Accuracy (AP@50)',
|
|
'stat.hardwareCost': 'Hardware Cost',
|
|
|
|
// Actions
|
|
'action.startDetection': 'Start Detection',
|
|
'action.stopDetection': 'Stop Detection',
|
|
'action.toggleTheme': 'Toggle theme',
|
|
'action.exportData': 'Export data',
|
|
'action.screenshot': 'Take screenshot',
|
|
|
|
// Connection
|
|
'conn.connected': 'Connected',
|
|
'conn.connecting': 'Connecting...',
|
|
'conn.offline': 'Offline',
|
|
'conn.reconnecting': 'Reconnecting...',
|
|
'conn.live': 'Live',
|
|
'conn.simulated': 'Simulated',
|
|
|
|
// Misc
|
|
'misc.loading': 'Loading...',
|
|
'misc.error': 'An error occurred',
|
|
'misc.noData': 'No data available',
|
|
'misc.close': 'Close',
|
|
'misc.cancel': 'Cancel',
|
|
'misc.confirm': 'Confirm',
|
|
'misc.settings': 'Settings',
|
|
'misc.language': 'Language'
|
|
},
|
|
|
|
pl: {
|
|
// Navigation
|
|
'nav.dashboard': 'Panel',
|
|
'nav.hardware': 'Sprzet',
|
|
'nav.demo': 'Demo na zywo',
|
|
'nav.architecture': 'Architektura',
|
|
'nav.performance': 'Wydajnosc',
|
|
'nav.applications': 'Aplikacje',
|
|
'nav.sensing': 'Czujniki',
|
|
'nav.training': 'Trening',
|
|
|
|
// Dashboard
|
|
'dashboard.title': 'Rewolucyjne wykrywanie pozy czlowieka przez WiFi',
|
|
'dashboard.subtitle': 'Sledzenie ludzi przez sciany za pomoca sygnalow WiFi',
|
|
'dashboard.description': 'AI moze sledzic ruchy calego ciala przez sciany uzywajac jedynie sygnalow WiFi. Badacze z Carnegie Mellon wytrenowali siec neuronowa do zamiany sygnalow WiFi w szczegolowe modele szkieletowe.',
|
|
'dashboard.status': 'Status systemu',
|
|
'dashboard.metrics': 'Metryki systemu',
|
|
'dashboard.features': 'Funkcje',
|
|
'dashboard.liveStats': 'Statystyki na zywo',
|
|
'dashboard.activePersons': 'Aktywne osoby',
|
|
'dashboard.avgConfidence': 'Srednia pewnosc',
|
|
'dashboard.totalDetections': 'Laczne detekcje',
|
|
'dashboard.zoneOccupancy': 'Zajecie stref',
|
|
|
|
// Status
|
|
'status.apiServer': 'Serwer API',
|
|
'status.hardware': 'Sprzet',
|
|
'status.inference': 'Wnioskowanie',
|
|
'status.streaming': 'Streaming',
|
|
'status.dataSource': 'Zrodlo danych',
|
|
|
|
// Metrics
|
|
'metrics.cpu': 'Uzycie CPU',
|
|
'metrics.memory': 'Uzycie pamieci',
|
|
'metrics.disk': 'Uzycie dysku',
|
|
|
|
// Benefits
|
|
'benefit.throughWalls': 'Przez sciany',
|
|
'benefit.throughWallsDesc': 'Dziala przez przeszkody stale bez linii wzroku',
|
|
'benefit.privacy': 'Ochrona prywatnosci',
|
|
'benefit.privacyDesc': 'Brak kamer i nagrywania - tylko analiza sygnalow WiFi',
|
|
'benefit.realtime': 'Czas rzeczywisty',
|
|
'benefit.realtimeDesc': 'Mapuje 24 regiony ciala w czasie rzeczywistym przy 100Hz',
|
|
'benefit.lowCost': 'Niski koszt',
|
|
'benefit.lowCostDesc': 'Zbudowany z komercyjnego sprzetu WiFi za $30',
|
|
|
|
// Stats
|
|
'stat.bodyRegions': 'Regiony ciala',
|
|
'stat.samplingRate': 'Czestotliwosc',
|
|
'stat.accuracy': 'Dokladnosc (AP@50)',
|
|
'stat.hardwareCost': 'Koszt sprzetu',
|
|
|
|
// Actions
|
|
'action.startDetection': 'Rozpocznij detekcje',
|
|
'action.stopDetection': 'Zatrzymaj detekcje',
|
|
'action.toggleTheme': 'Zmien motyw',
|
|
'action.exportData': 'Eksportuj dane',
|
|
'action.screenshot': 'Zrob zrzut ekranu',
|
|
|
|
// Connection
|
|
'conn.connected': 'Polaczono',
|
|
'conn.connecting': 'Laczenie...',
|
|
'conn.offline': 'Offline',
|
|
'conn.reconnecting': 'Ponowne laczenie...',
|
|
'conn.live': 'Na zywo',
|
|
'conn.simulated': 'Symulacja',
|
|
|
|
// Misc
|
|
'misc.loading': 'Ladowanie...',
|
|
'misc.error': 'Wystapil blad',
|
|
'misc.noData': 'Brak danych',
|
|
'misc.close': 'Zamknij',
|
|
'misc.cancel': 'Anuluj',
|
|
'misc.confirm': 'Potwierdz',
|
|
'misc.settings': 'Ustawienia',
|
|
'misc.language': 'Jezyk'
|
|
}
|
|
};
|
|
|
|
export class I18n {
|
|
constructor() {
|
|
this.locale = this.getSavedLocale() || this.detectLocale();
|
|
this.listeners = [];
|
|
}
|
|
|
|
init() {
|
|
this.createSelector();
|
|
this.applyTranslations();
|
|
}
|
|
|
|
detectLocale() {
|
|
const lang = navigator.language?.toLowerCase() || 'en';
|
|
if (lang.startsWith('pl')) return 'pl';
|
|
return 'en';
|
|
}
|
|
|
|
getSavedLocale() {
|
|
try { return localStorage.getItem('ruview-locale'); }
|
|
catch { return null; }
|
|
}
|
|
|
|
saveLocale(locale) {
|
|
try { localStorage.setItem('ruview-locale', locale); }
|
|
catch { /* noop */ }
|
|
}
|
|
|
|
t(key) {
|
|
const dict = translations[this.locale] || translations.en;
|
|
return dict[key] || translations.en[key] || key;
|
|
}
|
|
|
|
setLocale(locale) {
|
|
if (!translations[locale]) return;
|
|
this.locale = locale;
|
|
this.saveLocale(locale);
|
|
document.documentElement.setAttribute('lang', locale);
|
|
this.applyTranslations();
|
|
this.listeners.forEach(cb => { try { cb(locale); } catch { /* noop */ } });
|
|
}
|
|
|
|
onLocaleChange(callback) {
|
|
this.listeners.push(callback);
|
|
return () => {
|
|
const i = this.listeners.indexOf(callback);
|
|
if (i > -1) this.listeners.splice(i, 1);
|
|
};
|
|
}
|
|
|
|
applyTranslations() {
|
|
// Translate elements with data-i18n attribute
|
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
|
const key = el.getAttribute('data-i18n');
|
|
el.textContent = this.t(key);
|
|
});
|
|
|
|
// Translate placeholders
|
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
|
const key = el.getAttribute('data-i18n-placeholder');
|
|
el.placeholder = this.t(key);
|
|
});
|
|
|
|
// Translate aria-labels
|
|
document.querySelectorAll('[data-i18n-aria]').forEach(el => {
|
|
const key = el.getAttribute('data-i18n-aria');
|
|
el.setAttribute('aria-label', this.t(key));
|
|
});
|
|
|
|
// Update language selector
|
|
const selector = document.getElementById('lang-selector');
|
|
if (selector) selector.value = this.locale;
|
|
}
|
|
|
|
createSelector() {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'lang-selector-wrap';
|
|
wrapper.innerHTML = `
|
|
<select id="lang-selector" class="lang-selector" aria-label="Language">
|
|
<option value="en">EN</option>
|
|
<option value="pl">PL</option>
|
|
</select>
|
|
`;
|
|
|
|
const select = wrapper.querySelector('select');
|
|
select.value = this.locale;
|
|
select.addEventListener('change', () => this.setLocale(select.value));
|
|
|
|
const headerInfo = document.querySelector('.header-info');
|
|
if (headerInfo) {
|
|
headerInfo.appendChild(wrapper);
|
|
}
|
|
}
|
|
|
|
getAvailableLocales() {
|
|
return Object.keys(translations);
|
|
}
|
|
|
|
dispose() {
|
|
this.listeners = [];
|
|
}
|
|
}
|
|
|
|
export const i18n = new I18n();
|