diff --git a/languages/de.json b/languages/de.json index ee00324..2fe0ae6 100644 --- a/languages/de.json +++ b/languages/de.json @@ -1,14 +1,14 @@ { "id": "de", "name": "Deutsch", - "version": 1, + "version": 2, "author": "JRC", "strings": { "general.save": "Speichern", "general.saved": "Gespeichert", "general.cancel": "Abbrechen", - "general.delete": "Loeschen", - "general.close": "Schliessen", + "general.delete": "Löschen", + "general.close": "Schließen", "general.retry": "Erneut versuchen", "general.copy": "Kopieren", "general.enabled": "Aktiviert", @@ -17,8 +17,8 @@ "general.off": "Aus", "general.yes": "Ja", "general.no": "Nein", - "general.add": "Hinzufuegen", - "general.noProfileSelected": "Kein Profil ausgewaehlt", + "general.add": "Hinzufügen", + "general.noProfileSelected": "Kein Profil ausgewählt", "sidebar.newProfile": "Neues Profil", "sidebar.fromTemplate": "Aus Vorlage", @@ -33,25 +33,28 @@ "tabs.logs": "Protokolle", "tabs.profile": "Profil", + "ctx.select": "Auswählen", + "ctx.clearConsole": "Konsole leeren", + "ctx.error": "Fehler", + "console.run": "Starten", "console.stop": "Stoppen", "console.forceKill": "Sofort beenden", "console.forceKillHint": "Sofort beenden (ohne sauberes Herunterfahren)", - "console.openWorkDir": "Arbeitsverzeichnis oeffnen", + "console.openWorkDir": "Arbeitsverzeichnis öffnen", "console.scrollToBottom": "nach unten scrollen", "console.search": "Suchen (Strg+F)", "console.clear": "Leeren (Strg+L)", "console.lines": "Zeilen", "console.noMatches": "Keine Treffer", - "console.searchPlaceholder": "Konsole durchsuchen... (Enter naechster, Shift+Enter vorheriger)", + "console.searchPlaceholder": "Konsole durchsuchen... (Enter nächster, Shift+Enter vorheriger)", "console.inputPlaceholder": "Befehl senden... (Hoch/Runter Verlauf, Strg+L leeren, Strg+F suchen)", "console.inputDisabled": "Prozess starten um Befehle zu senden", "console.waiting": "Warte auf Ausgabe...", - "console.notRunning": "Prozess laeuft nicht. Druecke Starten um zu beginnen.", + "console.notRunning": "Prozess läuft nicht. Drücke Starten um zu beginnen.", "console.noJar": "Keine JAR konfiguriert. Gehe zu Konfigurieren.", "console.copyLine": "Zeile kopieren", "console.copyAll": "Gesamte Ausgabe kopieren", - "console.pid": "PID", "config.general": "Allgemein", "config.files": "Dateien & Pfade", @@ -59,152 +62,140 @@ "config.props": "Eigenschaften (-D)", "config.args": "Programmargumente", "config.env": "Umgebung", - "config.unsavedChanges": "Ungespeicherte Aenderungen", + "config.unsavedChanges": "Nicht gespeicherte Änderungen", "config.restartNeeded": "Neustart erforderlich", - "config.autoStart": "Autostart", - "config.autoStartLabel": "Automatisch beim App-Start starten", - "config.autoStartHint": "Dieses Profil automatisch starten wenn JRC geoeffnet wird", - "config.autoRestart": "Auto-Neustart", - "config.autoRestartLabel": "JAR bei Absturz automatisch neustarten", - "config.autoRestartHint": "Startet den Prozess neu wenn er mit einem Fehlercode beendet wird", - "config.restartDelay": "Neustart-Verzoegerung", - "config.restartDelayHint": "Sekunden bis zum Neustart", + "config.autoStart": "Automatisch starten beim App-Start", + "config.autoStartHint": "Diese JAR wird automatisch gestartet wenn Java Runner Client geöffnet wird.", + "config.autoRestart": "Automatischer Neustart bei Absturz", + "config.autoRestartHint": "Startet den Prozess nach einem unerwarteten Beenden automatisch neu.", + "config.autoRestartInterval": "Neustart-Verzögerung", + "config.fileLogging": "Sitzungsprotokolle in Datei speichern", + "config.fileLoggingHint": "Schreibt Konsolenausgaben in .log-Dateien im Konfigurationsverzeichnis pro Sitzung", + "config.restartProcess": "Prozess neustarten", "config.logging": "Protokollierung", - "config.fileLoggingLabel": "Sitzungsprotokolle in Datei speichern", - "config.fileLoggingHint": "Konsolenausgabe in .log-Dateien im Konfigurationsverzeichnis pro Sitzung speichern", "config.process": "Prozess", - "config.restartProcess": "Prozess neustarten", - "config.jarSelection": "JAR-Auswahl", - "config.workingDir": "Arbeitsverzeichnis", - "config.workingDirHint": "Leer lassen um das JAR-Verzeichnis zu verwenden", - "config.workingDirPlaceholder": "Standard: JAR-Verzeichnis", - "config.javaExec": "Java-Programm", - "config.javaExecHint": "Leer lassen um java aus dem PATH zu verwenden", - "config.javaExecPlaceholder": "java (verwendet System-PATH)", - "config.jvmArgsTitle": "JVM-Argumente", - "config.jvmArgsHint": "Flags die der JVM vor -jar uebergeben werden, z.B. -Xmx2g -XX:+UseG1GC", + "config.jvmTitle": "JVM-Argumente", + "config.jvmHint": "Flags die vor -jar an die JVM übergeben werden, z.B. -Xmx2g -XX:+UseG1GC", "config.propsTitle": "Systemeigenschaften", - "config.propsHint": "Uebergeben als -Dkey=value. Spring-Profile, Ports, Log-Level, etc.", - "config.progArgsTitle": "Programmargumente", - "config.progArgsHint": "Angehaengt nach dem JAR-Pfad, z.B. --nogui --world myWorld", + "config.propsHint": "Übergeben als -Dkey=value. Spring-Profile, Ports, Logging-Level usw.", + "config.argsTitle": "Programmargumente", + "config.argsHint": "Argumente die nach der JAR übergeben werden, z.B. --nogui --world myWorld", "config.envTitle": "Umgebungsvariablen", - "config.envHint": "Werden in die Prozessumgebung eingefuegt. Ueberschreiben System-Umgebungsvariablen mit gleichem Schluessel.", + "config.envHint": "Werden in die Prozessumgebung eingefügt. Überschreiben System-Umgebungsvariablen mit gleichem Schlüssel.", "config.commandPreview": "Befehlsvorschau", "config.pendingArgTitle": "Nicht gespeicherte Eingabe", - "config.pendingArgMessage": "Du hast Text im Eingabefeld der noch nicht hinzugefuegt wurde.\n\nKlicke zuerst \"+ Hinzufuegen\", sonst wird er nicht uebernommen.\n\nTrotzdem wechseln?", + "config.pendingArgMessage": "Du hast Text im Eingabefeld der noch nicht hinzugefügt wurde.\n\nKlicke zuerst \"+ Hinzufügen\", sonst wird er nicht übernommen.\n\nTrotzdem wechseln?", "config.pendingArgConfirm": "Wechseln", "config.pendingArgCancel": "Bleiben", - "profile.identity": "Profil-Identitaet", + "profile.identity": "Profil-Identität", "profile.name": "Name", "profile.accentColour": "Akzentfarbe", "profile.accentColourHint": "Wird in der Seitenleiste und als Tab-Hervorhebung verwendet.", - "profile.customColour": "Eigene Farbe waehlen", + "profile.customColour": "Eigene Farbe wählen", "profile.dangerZone": "Gefahrenzone", - "profile.deleteProfile": "Profil loeschen", - "profile.deleteHint": "Entfernt dieses Profil und alle Einstellungen dauerhaft. Shift halten um Bestaetigung zu ueberspringen.", - "profile.deleteConfirmTitle": "Profil loeschen?", - "profile.deleteConfirmMessage": "\"{name}\" wird dauerhaft entfernt. Dies kann nicht rueckgaengig gemacht werden.", + "profile.deleteProfile": "Profil löschen", + "profile.deleteHint": "Entfernt dieses Profil und alle Einstellungen dauerhaft. Shift halten um Bestätigung zu überspringen.", + "profile.deleteConfirmTitle": "Profil löschen?", + "profile.deleteConfirmMessage": "\"{name}\" wird dauerhaft entfernt. Dies kann nicht rückgängig gemacht werden.", "logs.title": "Sitzungsprotokolle", "logs.files": "Dateien", "logs.refresh": "Aktualisieren", - "logs.openDir": "Protokollverzeichnis oeffnen", + "logs.openDir": "Protokollverzeichnis öffnen", "logs.noFiles": "Noch keine Protokolldateien. Starte und stoppe einen Prozess um eine zu erstellen.", - "logs.selectFile": "Waehle eine Protokolldatei um den Inhalt anzuzeigen", + "logs.selectFile": "Wähle eine Protokolldatei um den Inhalt anzuzeigen", "logs.loading": "Laden...", - "logs.deleteHint": "Protokolldatei loeschen (Shift halten um Bestaetigung zu ueberspringen)", - "logs.deleteTitle": "Protokolldatei loeschen?", - "logs.deleteMessage": "\"{name}\" wird dauerhaft geloescht.", - "logs.disabled": "Dateiprotokollierung ist fuer dieses Profil deaktiviert.", + "logs.deleteHint": "Protokolldatei löschen (Shift halten um Bestätigung zu überspringen)", + "logs.deleteTitle": "Protokolldatei löschen?", + "logs.deleteMessage": "\"{name}\" wird dauerhaft gelöscht.", + "logs.disabled": "Dateiprotokollierung ist für dieses Profil deaktiviert.", "logs.disabledHint": "Aktiviere sie unter Konfigurieren > Allgemein > Sitzungsprotokolle in Datei speichern.", "settings.title": "Anwendungseinstellungen", "settings.saved": "Einstellungen gespeichert", - "settings.unsaved": "Ungespeicherte Aenderungen", - "settings.saveChanges": "Aenderungen speichern", - + "settings.unsaved": "Nicht gespeicherte Änderungen", + "settings.saveChanges": "Änderungen speichern", "settings.general": "Allgemein", "settings.startup": "Autostart", "settings.launchOnStartup": "Beim Windows-Start starten", "settings.launchOnStartupHint": "Java Runner Client startet automatisch beim Anmelden", - "settings.startMinimized": "Minimiert im Infobereich starten", - "settings.startMinimizedHint": "Fenster erscheint nicht beim Start -- nur das Symbol im Infobereich", - "settings.minimizeToTray": "Beim Schliessen in den Infobereich minimieren", - "settings.minimizeToTrayHint": "Beim Schliessen des Fensters bleiben die App und laufende JARs im Hintergrund aktiv", - + "settings.startMinimized": "Minimiert in den Tray starten", + "settings.startMinimizedHint": "Das Fenster erscheint nicht beim Start -- nur das Tray-Symbol", + "settings.minimizeToTray": "Beim Schließen in den Tray minimieren", + "settings.minimizeToTrayHint": "Das Schließen des Fensters hält die App und laufende JARs im Hintergrund aktiv", "settings.console": "Konsole", - "settings.fontSize": "Schriftgroesse", - "settings.fontSizeHint": "Schriftgroesse der Konsolenausgabe in Pixeln", + "settings.fontSize": "Schriftgröße", + "settings.fontSizeHint": "Schriftgröße der Konsolenausgabe in Pixeln", "settings.lineNumbers": "Zeilennummern anzeigen", - "settings.lineNumbersHint": "Zeilennummernspalte in der Konsolenausgabe anzeigen", + "settings.lineNumbersHint": "Zeigt eine Zeilennummern-Spalte in der Konsolenausgabe", "settings.timestamps": "Zeitstempel anzeigen", - "settings.timestampsHint": "Zeitstempel fuer jede Konsolenzeile anzeigen", + "settings.timestampsHint": "Zeigt einen Zeitstempel für jede Konsolenzeile", "settings.wordWrap": "Zeilenumbruch", "settings.wordWrapHint": "Lange Zeilen umbrechen statt horizontal zu scrollen", "settings.maxLines": "Maximale Zeilen im Puffer", - "settings.maxLinesHint": "Aeltere Zeilen werden verworfen wenn das Limit erreicht ist", - "settings.historySize": "Befehlsverlauf-Groesse", + "settings.maxLinesHint": "Ältere Zeilen werden verworfen wenn das Limit erreicht ist", + "settings.historySize": "Befehlsverlauf-Größe", "settings.historySizeHint": "Gespeicherte Befehle pro Sitzung (Hoch/Runter zum Navigieren)", - "settings.appearance": "Darstellung", "settings.theme": "Design", - "settings.themeHint": "Visuelles Design der Anwendung", + "settings.themeHint": "Visuelles Design auswählen", "settings.themeBuiltin": "Eingebaut", - "settings.themeCheckUpdate": "Nach Design-Update suchen", + "settings.themeCheckUpdate": "Nach Design-Updates suchen", "settings.language": "Sprache", - "settings.languageHint": "Anzeigesprache der Anwendung", - "settings.languageCheckUpdate": "Nach Sprach-Update suchen", - + "settings.languageHint": "Anzeigesprache auswählen", + "settings.languageCheckUpdate": "Nach Sprach-Updates suchen", + "appearance.refresh": "Aktualisieren", + "appearance.loadFromGithub": "Von GitHub laden", + "appearance.fetchThemesFailed": "Designs konnten nicht abgerufen werden.", + "appearance.fetchLangsFailed": "Sprachen konnten nicht abgerufen werden.", + "appearance.development": "Entwicklung", + "appearance.syncTitle": "Lokale Projektdateien synchronisieren", + "appearance.syncHint": "Designs und Sprachen aus /themes und /languages im Projektverzeichnis laden", + "appearance.sync": "Synchronisieren", + "appearance.synced": "Synchronisiert", "settings.advanced": "Erweitert", "settings.devMode": "Entwickleroptionen", - "settings.devModeLabel": "Entwicklermodus umschalten (Rechte-Shift + 7)", + "settings.devModeLabel": "Entwicklermodus umschalten (Rechts-Shift + 7)", "settings.devModeHint": "Aktiviert den Entwickler-Tab und DevTools. Mit Vorsicht verwenden.", "settings.restApi": "REST-API", "settings.restApiLabel": "REST-API aktivieren", - "settings.restApiHint": "Stellt eine lokale HTTP-API fuer Automatisierung bereit (Standard-Port {port})", + "settings.restApiHint": "Stellt eine lokale HTTP-API für Automatisierung bereit (Standard-Port {port})", "settings.restApiPort": "Port", - "settings.restApiPortHint": "Neustart erforderlich um den Port zu aendern", - + "settings.restApiPortHint": "Neustart erforderlich um den Port zu ändern", "settings.updates": "Aktualisierungen", "settings.updateCenter": "Aktualisierungscenter", - "settings.checkAll": "Alle pruefen", + "settings.checkAll": "Alle prüfen", "settings.updateAll": "Alle aktualisieren", "settings.upToDate": "Aktuell", - "settings.updateAvailable": "Update verfuegbar", - "settings.checking": "Pruefe...", - "settings.checkFailed": "Pruefung fehlgeschlagen", - - "settings.about": "Ueber", + "settings.updateAvailable": "Update verfügbar", + "settings.checking": "Prüfe...", + "settings.checkFailed": "Prüfung fehlgeschlagen", + "settings.about": "Über", "settings.version": "Version", "settings.stack": "Technologie", "settings.configPath": "Konfiguration", - "release.title": "Release-Details", "release.preRelease": "Vorabversion", "release.stable": "Stabil", - "release.trustedDev": "Vertrauenswuerdiger Entwickler", + "release.trustedDev": "Vertrauenswürdiger Entwickler", "release.automation": "Automatisiertes Release", "release.unknownPublisher": "Unbekannter Herausgeber", - "release.unknownPublisherHint": "Dieses Release wurde von einem GitHub-Benutzer veroeffentlicht der nicht in der vertrauenswuerdigen Liste steht. Es wurde dennoch durch die GitHub-Repository-Sicherheit genehmigt.", + "release.unknownPublisherHint": "Dieses Release wurde von einem GitHub-Benutzer veröffentlicht der nicht in der vertrauenswürdigen Liste steht. Es wurde dennoch durch die GitHub-Repository-Sicherheit genehmigt.", "release.downloads": "Downloads", "release.otherAssets": "Weitere Dateien", "release.releaseNotes": "Release-Hinweise", "release.viewOnGithub": "Auf GitHub ansehen", - "utilities.title": "Werkzeuge", - "utilities.activityLog": "Aktivitaetsprotokoll", + "utilities.activityLog": "Aktivitätsprotokoll", "utilities.processScanner": "Prozess-Scanner", - "panels.settings": "Anwendungseinstellungen", "panels.faq": "FAQ", "panels.utilities": "Werkzeuge", "panels.developer": "Entwickler", - "dev.mode": "Entwicklermodus", - "faq.searchPlaceholder": "FAQ durchsuchen...", "faq.noResults": "Keine Ergebnisse gefunden.", - "faq.noItems": "Keine Eintraege in diesem Thema." + "faq.noItems": "Keine Einträge in diesem Thema." } } diff --git a/languages/en.json b/languages/en.json index d5324c9..5d11f90 100644 --- a/languages/en.json +++ b/languages/en.json @@ -1,7 +1,7 @@ { "id": "en", "name": "English", - "version": 1, + "version": 2, "author": "JRC", "strings": { "general.save": "Save", @@ -9,14 +9,88 @@ "general.cancel": "Cancel", "general.delete": "Delete", "general.close": "Close", + "general.retry": "Retry", + "general.copy": "Copy", + "general.enabled": "Enabled", + "general.disabled": "Disabled", + "general.on": "On", + "general.off": "Off", + "general.yes": "Yes", + "general.no": "No", + "general.add": "Add", "general.noProfileSelected": "No profile selected", "sidebar.newProfile": "New Profile", "sidebar.fromTemplate": "From Template", + "sidebar.noProfiles": "No profiles yet.", + "sidebar.utilities": "Utilities", + "sidebar.faq": "FAQ", "sidebar.settings": "Settings", + "sidebar.developer": "Developer", + "tabs.console": "Console", + "tabs.configure": "Configure", + "tabs.logs": "Logs", + "tabs.profile": "Profile", + "ctx.select": "Select", + "ctx.clearConsole": "Clear Console", + "ctx.error": "Error", "console.run": "Run", "console.stop": "Stop", "console.forceKill": "Force Kill", + "console.forceKillHint": "Force kill (skip graceful shutdown)", + "console.openWorkDir": "Open working directory", + "console.scrollToBottom": "scroll to bottom", + "console.search": "Search (Ctrl+F)", + "console.clear": "Clear (Ctrl+L)", + "console.lines": "lines", + "console.noMatches": "No matches", + "console.searchPlaceholder": "Search console... (Enter next, Shift+Enter prev)", + "console.inputPlaceholder": "Send command... (up/down history, Ctrl+L clear, Ctrl+F search)", + "console.inputDisabled": "Start the process to send commands", + "console.waiting": "Waiting for output...", "console.notRunning": "Process not running. Press Run to start.", - "settings.title": "Application Settings" + "console.noJar": "No JAR configured. Go to Configure to set one.", + "console.copyLine": "Copy line", + "console.copyAll": "Copy all output", + "settings.title": "Application Settings", + "settings.saved": "Settings saved", + "settings.unsaved": "Unsaved changes", + "settings.saveChanges": "Save Changes", + "settings.startup": "Startup", + "settings.launchOnStartup": "Launch on Windows startup", + "settings.launchOnStartupHint": "Java Runner Client starts automatically when you log in", + "settings.startMinimized": "Start minimized to tray", + "settings.startMinimizedHint": "Window won't appear on startup -- only the system tray icon", + "settings.minimizeToTray": "Minimize to tray on close", + "settings.minimizeToTrayHint": "Closing the window keeps the app and running JARs alive in the background", + "settings.console": "Console", + "settings.fontSize": "Font size", + "settings.fontSizeHint": "Console output font size in pixels", + "settings.lineNumbers": "Show line numbers", + "settings.lineNumbersHint": "Display a line number gutter in console output", + "settings.timestamps": "Show timestamps", + "settings.timestampsHint": "Display a timestamp for each console line", + "settings.wordWrap": "Word wrap", + "settings.wordWrapHint": "Wrap long lines instead of horizontal scrolling", + "settings.maxLines": "Max lines in buffer", + "settings.maxLinesHint": "Older lines are discarded when the limit is reached", + "settings.historySize": "Command history size", + "settings.historySizeHint": "Commands stored per session (Up/Down to navigate)", + "settings.theme": "Theme", + "settings.themeHint": "Select a visual theme", + "settings.language": "Language", + "settings.languageHint": "Select a display language", + "appearance.refresh": "Refresh", + "appearance.loadFromGithub": "Load from GitHub", + "appearance.fetchThemesFailed": "Failed to fetch themes.", + "appearance.fetchLangsFailed": "Failed to fetch languages.", + "appearance.development": "Development", + "appearance.syncTitle": "Sync local project files", + "appearance.syncHint": "Load themes and languages from /themes and /languages in the project root", + "appearance.sync": "Sync", + "appearance.synced": "Synced", + "panels.settings": "Application Settings", + "panels.faq": "FAQ", + "panels.utilities": "Utilities", + "panels.developer": "Developer" } } diff --git a/src/main/AssetManager.ts b/src/main/AssetManager.ts index 324490c..d58d65f 100644 --- a/src/main/AssetManager.ts +++ b/src/main/AssetManager.ts @@ -4,7 +4,6 @@ import path from 'path'; import https from 'https'; import { GITHUB_CONFIG } from './shared/config/GitHub.config'; import { BUILTIN_THEME, THEME_GITHUB_PATH } from './shared/config/Theme.config'; -import { LANGUAGE_GITHUB_PATH } from './shared/config/Language.config'; import { ENGLISH } from './shared/config/DefaultLanguage.config'; import type { ThemeDefinition, LocalThemeState } from './shared/types/Theme.types'; import type { LanguageDefinition, LocalLanguageState } from './shared/types/Language.types'; @@ -180,12 +179,12 @@ export function setActiveLanguage(langId: string): LanguageDefinition { export async function fetchRemoteLanguages(): Promise<{ ok: boolean; languages?: LanguageDefinition[]; error?: string }> { try { - const listing = await httpsGetJson(contentsUrl(LANGUAGE_GITHUB_PATH)); + const listing = await httpsGetJson(contentsUrl(GITHUB_CONFIG.languagesPath)); if (!Array.isArray(listing)) return { ok: false, error: 'Languages folder not found' }; const languages: LanguageDefinition[] = []; for (const f of (listing as Array<{ name: string }>).filter((f) => f.name.endsWith('.json'))) { try { - const lang = (await httpsGetJson(rawUrl(LANGUAGE_GITHUB_PATH, f.name))) as LanguageDefinition; + const lang = (await httpsGetJson(rawUrl(GITHUB_CONFIG.languagesPath, f.name))) as LanguageDefinition; if (lang.id && lang.name && lang.strings) languages.push(lang); } catch { /* skip */ } } diff --git a/src/main/shared/config/DefaultLanguage.config.ts b/src/main/shared/config/DefaultLanguage.config.ts index fd6865a..0ffd5c8 100644 --- a/src/main/shared/config/DefaultLanguage.config.ts +++ b/src/main/shared/config/DefaultLanguage.config.ts @@ -1,231 +1,247 @@ import type { LanguageDefinition } from '../types/Language.types'; +const ENGLISH_STRINGS = { + // General + 'general.save': 'Save', + 'general.saved': 'Saved', + 'general.cancel': 'Cancel', + 'general.delete': 'Delete', + 'general.close': 'Close', + 'general.retry': 'Retry', + 'general.copy': 'Copy', + 'general.enabled': 'Enabled', + 'general.disabled': 'Disabled', + 'general.on': 'On', + 'general.off': 'Off', + 'general.yes': 'Yes', + 'general.no': 'No', + 'general.add': 'Add', + 'general.noProfileSelected': 'No profile selected', + + // Sidebar + 'sidebar.newProfile': 'New Profile', + 'sidebar.fromTemplate': 'From Template', + 'sidebar.noProfiles': 'No profiles yet.', + 'sidebar.utilities': 'Utilities', + 'sidebar.faq': 'FAQ', + 'sidebar.settings': 'Settings', + 'sidebar.developer': 'Developer', + + // Profile tabs + 'tabs.console': 'Console', + 'tabs.configure': 'Configure', + 'tabs.logs': 'Logs', + 'tabs.profile': 'Profile', + + // Context menu + 'ctx.select': 'Select', + 'ctx.clearConsole': 'Clear Console', + 'ctx.error': 'Error', + + // Console + 'console.run': 'Run', + 'console.stop': 'Stop', + 'console.forceKill': 'Force Kill', + 'console.forceKillHint': 'Force kill (skip graceful shutdown)', + 'console.openWorkDir': 'Open working directory', + 'console.scrollToBottom': 'scroll to bottom', + 'console.search': 'Search (Ctrl+F)', + 'console.clear': 'Clear (Ctrl+L)', + 'console.lines': 'lines', + 'console.noMatches': 'No matches', + 'console.searchPlaceholder': 'Search console... (Enter next, Shift+Enter prev)', + 'console.inputPlaceholder': 'Send command... (up/down history, Ctrl+L clear, Ctrl+F search)', + 'console.inputDisabled': 'Start the process to send commands', + 'console.waiting': 'Waiting for output...', + 'console.notRunning': 'Process not running. Press Run to start.', + 'console.noJar': 'No JAR configured. Go to Configure to set one.', + 'console.copyLine': 'Copy line', + 'console.copyAll': 'Copy all output', + + // Config tab + 'config.general': 'General', + 'config.files': 'Files & Paths', + 'config.jvm': 'JVM Args', + 'config.props': 'Properties (-D)', + 'config.args': 'Program Args', + 'config.env': 'Environment', + 'config.unsavedChanges': 'unsaved changes', + 'config.restartNeeded': 'restart needed', + 'config.autoStart': 'Auto-start on app launch', + 'config.autoStartHint': 'This JAR starts automatically when Java Runner Client opens.', + 'config.autoRestart': 'Auto-restart on crash', + 'config.autoRestartHint': 'Automatically restarts the process after an unexpected exit.', + 'config.autoRestartInterval': 'Restart delay', + 'config.fileLogging': 'Save session logs to file', + 'config.fileLoggingHint': + 'Write console output to .log files in the config directory per session', + 'config.restartProcess': 'Restart process', + 'config.logging': 'Logging', + 'config.process': 'Process', + 'config.jvmTitle': 'JVM Arguments', + 'config.jvmHint': 'Flags passed to the JVM before -jar, e.g. -Xmx2g -XX:+UseG1GC', + 'config.propsTitle': 'System Properties', + 'config.propsHint': 'Passed as -Dkey=value. Spring profiles, ports, logging levels, etc.', + 'config.argsTitle': 'Program Arguments', + 'config.argsHint': 'Arguments passed after the JAR, e.g. --nogui --world myWorld', + 'config.envTitle': 'Environment Variables', + 'config.envHint': + 'Injected into the process environment. Overrides system variables with the same key.', + 'config.commandPreview': 'Command preview', + 'config.pendingArgTitle': 'Unsaved argument input', + 'config.pendingArgMessage': + 'You have text in the argument input that hasn\'t been added yet.\n\nClick "+ Add" first, otherwise it will not take effect.\n\nSwitch anyway?', + 'config.pendingArgConfirm': 'Switch', + 'config.pendingArgCancel': 'Stay', + + // Profile tab + 'profile.identity': 'Profile Identity', + 'profile.name': 'Name', + 'profile.accentColour': 'Accent Colour', + 'profile.accentColourHint': 'Used in the sidebar and as the tab highlight colour.', + 'profile.customColour': 'Pick custom colour', + 'profile.dangerZone': 'Danger Zone', + 'profile.deleteProfile': 'Delete profile', + 'profile.deleteHint': + 'Permanently removes this profile and all its configuration. Hold Shift to skip confirmation.', + 'profile.deleteConfirmTitle': 'Delete profile?', + 'profile.deleteConfirmMessage': '"{name}" will be permanently removed. This cannot be undone.', + + // Logs tab + 'logs.title': 'Session Logs', + 'logs.files': 'files', + 'logs.refresh': 'Refresh', + 'logs.openDir': 'Open logs directory', + 'logs.noFiles': 'No log files yet. Start and stop a process to create one.', + 'logs.selectFile': 'Select a log file to view its contents', + 'logs.loading': 'Loading...', + 'logs.deleteHint': 'Delete log file (hold Shift to skip confirmation)', + 'logs.deleteTitle': 'Delete log file?', + 'logs.deleteMessage': '"{name}" will be permanently deleted.', + 'logs.disabled': 'File logging is disabled for this profile.', + 'logs.disabledHint': 'Enable it in Configure > General > Save session logs to file.', + + // Settings + 'settings.title': 'Application Settings', + 'settings.saved': 'Settings saved', + 'settings.unsaved': 'Unsaved changes', + 'settings.saveChanges': 'Save Changes', + + // Settings: General + 'settings.general': 'General', + 'settings.startup': 'Startup', + 'settings.launchOnStartup': 'Launch on Windows startup', + 'settings.launchOnStartupHint': 'Java Runner Client starts automatically when you log in', + 'settings.startMinimized': 'Start minimized to tray', + 'settings.startMinimizedHint': "Window won't appear on startup -- only the system tray icon", + 'settings.minimizeToTray': 'Minimize to tray on close', + 'settings.minimizeToTrayHint': + 'Closing the window keeps the app and running JARs alive in the background', + + // Settings: Console + 'settings.console': 'Console', + 'settings.fontSize': 'Font size', + 'settings.fontSizeHint': 'Console output font size in pixels', + 'settings.lineNumbers': 'Show line numbers', + 'settings.lineNumbersHint': 'Display a line number gutter in console output', + 'settings.timestamps': 'Show timestamps', + 'settings.timestampsHint': 'Display a timestamp for each console line', + 'settings.wordWrap': 'Word wrap', + 'settings.wordWrapHint': 'Wrap long lines instead of horizontal scrolling', + 'settings.maxLines': 'Max lines in buffer', + 'settings.maxLinesHint': 'Older lines are discarded when the limit is reached', + 'settings.historySize': 'Command history size', + 'settings.historySizeHint': 'Commands stored per session (Up/Down to navigate)', + + // Settings: Appearance + 'settings.appearance': 'Appearance', + 'settings.theme': 'Theme', + 'settings.themeHint': 'Select a visual theme', + 'settings.themeBuiltin': 'Built-in', + 'settings.themeCheckUpdate': 'Check for theme update', + 'settings.language': 'Language', + 'settings.languageHint': 'Select a display language', + 'settings.languageCheckUpdate': 'Check for language update', + + // Appearance section + 'appearance.refresh': 'Refresh', + 'appearance.loadFromGithub': 'Load from GitHub', + 'appearance.fetchThemesFailed': 'Failed to fetch themes.', + 'appearance.fetchLangsFailed': 'Failed to fetch languages.', + 'appearance.development': 'Development', + 'appearance.syncTitle': 'Sync local project files', + 'appearance.syncHint': + 'Load themes and languages from /themes and /languages in the project root', + 'appearance.sync': 'Sync', + 'appearance.synced': 'Synced', + + // Settings: Advanced + 'settings.advanced': 'Advanced', + 'settings.devMode': 'Developer Options', + 'settings.devModeLabel': 'Toggle Developer Mode (Right-Shift + 7)', + 'settings.devModeHint': 'Enables the Developer tab and DevTools. Use with caution.', + 'settings.restApi': 'REST API', + 'settings.restApiLabel': 'Enable REST API', + 'settings.restApiHint': 'Exposes a local HTTP API for automation (default port {port})', + 'settings.restApiPort': 'Port', + 'settings.restApiPortHint': 'Restart required to change the port', + + // Settings: Updates + 'settings.updates': 'Updates', + 'settings.updateCenter': 'Update Center', + 'settings.checkAll': 'Check All', + 'settings.updateAll': 'Update All', + 'settings.upToDate': 'Up to date', + 'settings.updateAvailable': 'Update available', + 'settings.checking': 'Checking...', + 'settings.checkFailed': 'Check failed', + + // Settings: About + 'settings.about': 'About', + 'settings.version': 'Version', + 'settings.stack': 'Stack', + 'settings.configPath': 'Config', + + // Release modal + 'release.title': 'Release Details', + 'release.preRelease': 'Pre-release', + 'release.stable': 'Stable', + 'release.trustedDev': 'Trusted Developer', + 'release.automation': 'Automated Release', + 'release.unknownPublisher': 'Unknown Publisher', + 'release.unknownPublisherHint': + 'This release was published by a GitHub user not in the trusted list. It was still permitted by GitHub repository security.', + 'release.downloads': 'downloads', + 'release.otherAssets': 'Other assets', + 'release.releaseNotes': 'Release notes', + 'release.viewOnGithub': 'View on GitHub', + + // Utilities + 'utilities.title': 'Utilities', + 'utilities.activityLog': 'Activity Log', + 'utilities.processScanner': 'Process Scanner', + + // Panel headers + 'panels.settings': 'Application Settings', + 'panels.faq': 'FAQ', + 'panels.utilities': 'Utilities', + 'panels.developer': 'Developer', + + // Developer + 'dev.mode': 'Developer Mode', + + // FAQ + 'faq.searchPlaceholder': 'Search FAQ...', + 'faq.noResults': 'No results found.', + 'faq.noItems': 'No items in this topic.', +} as const; + export const ENGLISH: LanguageDefinition = { id: 'en', name: 'English', - version: 1, + version: 2, author: 'JRC', - strings: { - // General - 'general.save': 'Save', - 'general.saved': 'Saved', - 'general.cancel': 'Cancel', - 'general.delete': 'Delete', - 'general.close': 'Close', - 'general.retry': 'Retry', - 'general.copy': 'Copy', - 'general.enabled': 'Enabled', - 'general.disabled': 'Disabled', - 'general.on': 'On', - 'general.off': 'Off', - 'general.yes': 'Yes', - 'general.no': 'No', - 'general.add': 'Add', - 'general.noProfileSelected': 'No profile selected', - - // Sidebar - 'sidebar.newProfile': 'New Profile', - 'sidebar.fromTemplate': 'From Template', - 'sidebar.noProfiles': 'No profiles yet.', - 'sidebar.utilities': 'Utilities', - 'sidebar.faq': 'FAQ', - 'sidebar.settings': 'Settings', - 'sidebar.developer': 'Developer', - - // Profile tabs - 'tabs.console': 'Console', - 'tabs.configure': 'Configure', - 'tabs.logs': 'Logs', - 'tabs.profile': 'Profile', - - // Console - 'console.run': 'Run', - 'console.stop': 'Stop', - 'console.forceKill': 'Force Kill', - 'console.forceKillHint': 'Force kill (skip graceful shutdown)', - 'console.openWorkDir': 'Open working directory', - 'console.scrollToBottom': 'scroll to bottom', - 'console.search': 'Search (Ctrl+F)', - 'console.clear': 'Clear (Ctrl+L)', - 'console.lines': 'lines', - 'console.noMatches': 'No matches', - 'console.searchPlaceholder': 'Search console... (Enter next, Shift+Enter prev)', - 'console.inputPlaceholder': 'Send command... (up/down history, Ctrl+L clear, Ctrl+F search)', - 'console.inputDisabled': 'Start the process to send commands', - 'console.waiting': 'Waiting for output...', - 'console.notRunning': 'Process not running. Press Run to start.', - 'console.noJar': 'No JAR configured. Go to Configure to set one.', - 'console.copyLine': 'Copy line', - 'console.copyAll': 'Copy all output', - 'console.pid': 'PID', - - // Config sections - 'config.general': 'General', - 'config.files': 'Files & Paths', - 'config.jvm': 'JVM Args', - 'config.props': 'Properties (-D)', - 'config.args': 'Program Args', - 'config.env': 'Environment', - 'config.unsavedChanges': 'unsaved changes', - 'config.restartNeeded': 'restart needed', - 'config.autoStart': 'Auto-start', - 'config.autoStartLabel': 'Auto-start on app launch', - 'config.autoStartHint': 'Automatically run this profile when JRC starts', - 'config.autoRestart': 'Auto-restart', - 'config.autoRestartLabel': 'Automatically restart JAR on crash', - 'config.autoRestartHint': 'Restarts the process if it exits with a non-zero code', - 'config.restartDelay': 'Restart delay', - 'config.restartDelayHint': 'Seconds to wait before restarting', - 'config.logging': 'Logging', - 'config.fileLoggingLabel': 'Save session logs to file', - 'config.fileLoggingHint': 'Write console output to .log files in the config directory per session', - 'config.process': 'Process', - 'config.restartProcess': 'Restart process', - 'config.jarSelection': 'JAR Selection', - 'config.workingDir': 'Working Directory', - 'config.workingDirHint': 'Leave empty to use the directory containing the JAR', - 'config.workingDirPlaceholder': 'Defaults to JAR directory', - 'config.javaExec': 'Java Executable', - 'config.javaExecHint': 'Leave empty to use the java found on PATH', - 'config.javaExecPlaceholder': 'java (uses system PATH)', - 'config.jvmArgsTitle': 'JVM Arguments', - 'config.jvmArgsHint': 'Flags passed to the JVM before -jar, e.g. -Xmx2g -XX:+UseG1GC', - 'config.propsTitle': 'System Properties', - 'config.propsHint': 'Passed as -Dkey=value. Spring profiles, ports, logging levels, etc.', - 'config.progArgsTitle': 'Program Arguments', - 'config.progArgsHint': 'Appended after the JAR path, e.g. --nogui --world myWorld', - 'config.envTitle': 'Environment Variables', - 'config.envHint': 'Injected into the process environment. Overrides system env vars with the same key.', - 'config.commandPreview': 'Command preview', - 'config.pendingArgTitle': 'Unsaved argument input', - 'config.pendingArgMessage': 'You have text in the argument input that hasn\'t been added yet.\n\nClick "+ Add" first, otherwise it will not take effect.\n\nSwitch anyway?', - 'config.pendingArgConfirm': 'Switch', - 'config.pendingArgCancel': 'Stay', - - // Profile tab - 'profile.identity': 'Profile Identity', - 'profile.name': 'Name', - 'profile.accentColour': 'Accent Colour', - 'profile.accentColourHint': 'Used in the sidebar and as the tab highlight colour.', - 'profile.customColour': 'Pick custom colour', - 'profile.dangerZone': 'Danger Zone', - 'profile.deleteProfile': 'Delete profile', - 'profile.deleteHint': 'Permanently removes this profile and all its configuration. Hold Shift to skip confirmation.', - 'profile.deleteConfirmTitle': 'Delete profile?', - 'profile.deleteConfirmMessage': '"{name}" will be permanently removed. This cannot be undone.', - - // Logs tab - 'logs.title': 'Session Logs', - 'logs.files': 'files', - 'logs.refresh': 'Refresh', - 'logs.openDir': 'Open logs directory', - 'logs.noFiles': 'No log files yet. Start and stop a process to create one.', - 'logs.selectFile': 'Select a log file to view its contents', - 'logs.loading': 'Loading...', - 'logs.deleteHint': 'Delete log file (hold Shift to skip confirmation)', - 'logs.deleteTitle': 'Delete log file?', - 'logs.deleteMessage': '"{name}" will be permanently deleted.', - 'logs.disabled': 'File logging is disabled for this profile.', - 'logs.disabledHint': 'Enable it in Configure > General > Save session logs to file.', - - // Settings - 'settings.title': 'Application Settings', - 'settings.saved': 'Settings saved', - 'settings.unsaved': 'Unsaved changes', - 'settings.saveChanges': 'Save Changes', - - // Settings: General - 'settings.general': 'General', - 'settings.startup': 'Startup', - 'settings.launchOnStartup': 'Launch on Windows startup', - 'settings.launchOnStartupHint': 'Java Runner Client starts automatically when you log in', - 'settings.startMinimized': 'Start minimized to tray', - 'settings.startMinimizedHint': "Window won't appear on startup -- only the system tray icon", - 'settings.minimizeToTray': 'Minimize to tray on close', - 'settings.minimizeToTrayHint': 'Closing the window keeps the app and running JARs alive in the background', - - // Settings: Console - 'settings.console': 'Console', - 'settings.fontSize': 'Font size', - 'settings.fontSizeHint': 'Console output font size in pixels', - 'settings.lineNumbers': 'Show line numbers', - 'settings.lineNumbersHint': 'Display a line number gutter in console output', - 'settings.timestamps': 'Show timestamps', - 'settings.timestampsHint': 'Display a timestamp for each console line', - 'settings.wordWrap': 'Word wrap', - 'settings.wordWrapHint': 'Wrap long lines instead of horizontal scrolling', - 'settings.maxLines': 'Max lines in buffer', - 'settings.maxLinesHint': 'Older lines are discarded when the limit is reached', - 'settings.historySize': 'Command history size', - 'settings.historySizeHint': 'Commands stored per session (Up/Down to navigate)', - - // Settings: Appearance - 'settings.appearance': 'Appearance', - 'settings.theme': 'Theme', - 'settings.themeHint': 'Visual theme for the application', - 'settings.themeBuiltin': 'Built-in', - 'settings.themeCheckUpdate': 'Check for theme update', - 'settings.language': 'Language', - 'settings.languageHint': 'Application display language', - 'settings.languageCheckUpdate': 'Check for language update', - - // Settings: Advanced - 'settings.advanced': 'Advanced', - 'settings.devMode': 'Developer Options', - 'settings.devModeLabel': 'Toggle Developer Mode (Right-Shift + 7)', - 'settings.devModeHint': 'Enables the Developer tab and DevTools. Use with caution.', - 'settings.restApi': 'REST API', - 'settings.restApiLabel': 'Enable REST API', - 'settings.restApiHint': 'Exposes a local HTTP API for automation (default port {port})', - 'settings.restApiPort': 'Port', - 'settings.restApiPortHint': 'Restart required to change the port', - - // Settings: Updates - 'settings.updates': 'Updates', - 'settings.updateCenter': 'Update Center', - 'settings.checkAll': 'Check All', - 'settings.updateAll': 'Update All', - 'settings.upToDate': 'Up to date', - 'settings.updateAvailable': 'Update available', - 'settings.checking': 'Checking...', - 'settings.checkFailed': 'Check failed', - - // Settings: About - 'settings.about': 'About', - 'settings.version': 'Version', - 'settings.stack': 'Stack', - 'settings.configPath': 'Config', - - // Release modal - 'release.title': 'Release Details', - 'release.preRelease': 'Pre-release', - 'release.stable': 'Stable', - 'release.trustedDev': 'Trusted Developer', - 'release.automation': 'Automated Release', - 'release.unknownPublisher': 'Unknown Publisher', - 'release.unknownPublisherHint': 'This release was published by a GitHub user not in the trusted list. It was still permitted by GitHub repository security.', - 'release.downloads': 'downloads', - 'release.otherAssets': 'Other assets', - 'release.releaseNotes': 'Release notes', - 'release.viewOnGithub': 'View on GitHub', - - // Utilities - 'utilities.title': 'Utilities', - 'utilities.activityLog': 'Activity Log', - 'utilities.processScanner': 'Process Scanner', - - // Panel headers - 'panels.settings': 'Application Settings', - 'panels.faq': 'FAQ', - 'panels.utilities': 'Utilities', - 'panels.developer': 'Developer', - - // Developer - 'dev.mode': 'Developer Mode', - - // FAQ - 'faq.searchPlaceholder': 'Search FAQ...', - 'faq.noResults': 'No results found.', - 'faq.noItems': 'No items in this topic.', - }, + strings: ENGLISH_STRINGS, }; + +export { ENGLISH_STRINGS }; diff --git a/src/main/shared/config/GitHub.config.ts b/src/main/shared/config/GitHub.config.ts index dd186b4..0bb3001 100644 --- a/src/main/shared/config/GitHub.config.ts +++ b/src/main/shared/config/GitHub.config.ts @@ -1,3 +1,10 @@ +export type TrustLevel = 'trusted' | 'automation' | 'unknown'; + +export interface TrustedPublisher { + login: string; + label: string; +} + export const GITHUB_CONFIG = { owner: 'timonmdy', repo: 'java-runner-client', @@ -6,6 +13,12 @@ export const GITHUB_CONFIG = { languagesPath: 'languages', templateMinVersion: 1, apiBase: 'https://api.github.com', + + trustedPublishers: [ + { login: 'timonmdy', label: 'Lead Developer' }, + ] as TrustedPublisher[], + + automationAccounts: ['github-actions[bot]', 'github-actions'], } as const; export function releasesUrl(): string { @@ -23,3 +36,18 @@ export function templateListUrl(): string { export function rawTemplateUrl(filename: string): string { return `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/main/${GITHUB_CONFIG.templatesPath}/${filename}`; } + +export function getPublisherTrust(login: string): { level: TrustLevel; label: string } { + const trusted = GITHUB_CONFIG.trustedPublishers.find( + (p) => p.login.toLowerCase() === login.toLowerCase(), + ); + if (trusted) return { level: 'trusted', label: trusted.label }; + + if ( + GITHUB_CONFIG.automationAccounts.some((a) => a.toLowerCase() === login.toLowerCase()) + ) { + return { level: 'automation', label: 'GitHub Actions' }; + } + + return { level: 'unknown', label: 'Unknown publisher' }; +} diff --git a/src/main/shared/config/Language.config.ts b/src/main/shared/config/Language.config.ts deleted file mode 100644 index 3b7ef07..0000000 --- a/src/main/shared/config/Language.config.ts +++ /dev/null @@ -1 +0,0 @@ -export const LANGUAGE_GITHUB_PATH = 'languages'; diff --git a/src/main/shared/config/TrustedPublishers.config.ts b/src/main/shared/config/TrustedPublishers.config.ts deleted file mode 100644 index a4d5e19..0000000 --- a/src/main/shared/config/TrustedPublishers.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type TrustLevel = 'trusted' | 'automation' | 'unknown'; - -export interface TrustedPublisher { - login: string; - label: string; -} - -export const TRUSTED_PUBLISHERS: TrustedPublisher[] = [ - { login: 'timonmdy', label: 'Lead Developer' }, -]; - -export const AUTOMATION_ACCOUNTS = ['github-actions[bot]', 'github-actions']; - -export function getPublisherTrust(login: string): { level: TrustLevel; label: string } { - const trusted = TRUSTED_PUBLISHERS.find( - (p) => p.login.toLowerCase() === login.toLowerCase() - ); - if (trusted) return { level: 'trusted', label: trusted.label }; - - if (AUTOMATION_ACCOUNTS.some((a) => a.toLowerCase() === login.toLowerCase())) { - return { level: 'automation', label: 'GitHub Actions' }; - } - - return { level: 'unknown', label: 'Unknown publisher' }; -} diff --git a/src/renderer/components/MainLayout.tsx b/src/renderer/components/MainLayout.tsx index 007a102..b693925 100644 --- a/src/renderer/components/MainLayout.tsx +++ b/src/renderer/components/MainLayout.tsx @@ -12,35 +12,35 @@ import { DeveloperTab } from './developer/DeveloperTab'; import { PanelHeader } from './layout/PanelHeader'; import { useApp } from '../AppProvider'; import { useDevMode } from '../hooks/useDevMode'; +import { useTranslation } from '../i18n/I18nProvider'; import { VscTerminal, VscAccount } from 'react-icons/vsc'; import { LuList, LuScrollText } from 'react-icons/lu'; -// Panels rendered in the side-panel view (replace main tabs area) const SIDE_PANELS = ['settings', 'faq', 'utilities', 'developer'] as const; type SidePanel = (typeof SIDE_PANELS)[number]; -const PANEL_LABELS: Record = { - settings: 'Application Settings', - faq: 'FAQ', - utilities: 'Utilities', - developer: 'Developer', +const PANEL_LABEL_KEYS: Record = { + settings: 'panels.settings', + faq: 'panels.faq', + utilities: 'panels.utilities', + developer: 'panels.developer', }; function isSidePanel(seg: string): seg is SidePanel { return (SIDE_PANELS as readonly string[]).includes(seg); } -// Profile-specific tabs shown in the tab bar const PROFILE_TABS = [ - { path: 'console', label: 'Console', Icon: VscTerminal }, - { path: 'config', label: 'Configure', Icon: LuList }, - { path: 'logs', label: 'Logs', Icon: LuScrollText }, - { path: 'profile', label: 'Profile', Icon: VscAccount }, + { path: 'console', labelKey: 'tabs.console', Icon: VscTerminal }, + { path: 'config', labelKey: 'tabs.configure', Icon: LuList }, + { path: 'logs', labelKey: 'tabs.logs', Icon: LuScrollText }, + { path: 'profile', labelKey: 'tabs.profile', Icon: VscAccount }, ] as const; export function MainLayout() { const { state, activeProfile, isRunning } = useApp(); const devMode = useDevMode(); + const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); @@ -52,14 +52,12 @@ export function MainLayout() { const color = activeProfile?.color ?? '#4ade80'; const running = activeProfile ? isRunning(activeProfile.id) : false; - // Redirect away from developer panel when dev mode is disabled useEffect(() => { if (!devMode && activePanel === 'developer') { navigate('/console', { replace: true }); } }, [devMode, activePanel, navigate]); - // Navigate to console when active profile changes const prevIdRef = React.useRef(state.activeProfileId); useEffect(() => { if (state.activeProfileId !== prevIdRef.current) { @@ -74,9 +72,8 @@ export function MainLayout() {
{activePanel ? ( - // Side panel view <> - +
} /> @@ -88,7 +85,6 @@ export function MainLayout() {
) : ( - // Profile tab view <>
{PROFILE_TABS.map((tab) => { @@ -106,7 +102,7 @@ export function MainLayout() { style={isActive ? { borderBottomColor: color, color } : {}} > - {tab.label} + {t(tab.labelKey)} {tab.path === 'console' && running && ( (null); + const [lineCtxMenu, setLineCtxMenu] = useState<{ x: number; y: number; text: string } | null>( + null, + ); const scrollRef = useRef(null); const inputRef = useRef(null); @@ -108,7 +129,7 @@ export function ConsoleTab() { await stopProcess(profileId); } else { if (!hasJarConfigured(activeProfile)) { - setErrorMsg('No JAR configured. Go to Configure to set one.'); + setErrorMsg(t('console.noJar')); return; } setStarting(true); @@ -116,7 +137,7 @@ export function ConsoleTab() { setStarting(false); if (!res.ok) setErrorMsg(res.error ?? 'Failed to start'); } - }, [activeProfile, running, profileId, stopProcess, startProcess]); + }, [activeProfile, running, profileId, stopProcess, startProcess, t]); const handleForceStop = useCallback(async () => { if (!profileId) return; @@ -133,7 +154,7 @@ export function ConsoleTab() { if (!cmd || !running) return; await sendInput(profileId, cmd); setCmdHistory((prev) => - [cmd, ...prev.filter((c) => c !== cmd)].slice(0, settings?.consoleHistorySize ?? 200) + [cmd, ...prev.filter((c) => c !== cmd)].slice(0, settings?.consoleHistorySize ?? 200), ); setInputValue(''); setHistoryIdx(-1); @@ -169,7 +190,7 @@ export function ConsoleTab() { openSearch(); } }, - [handleSend, historyIdx, cmdHistory, clearConsole, profileId, openSearch] + [handleSend, historyIdx, cmdHistory, clearConsole, profileId, openSearch], ); useEffect(() => { @@ -197,7 +218,7 @@ export function ConsoleTab() { if (!activeProfile) { return (
- No profile selected + {t('general.noProfileSelected')}
); } @@ -207,13 +228,12 @@ export function ConsoleTab() { const lineCtxItems: ContextMenuItem[] = lineCtxMenu ? [ { - label: 'Copy line', + label: t('console.copyLine'), onClick: () => navigator.clipboard.writeText(lineCtxMenu.text), }, { - label: 'Copy all output', - onClick: () => - navigator.clipboard.writeText(lines.map((l) => l.text).join('\n')), + label: t('console.copyAll'), + onClick: () => navigator.clipboard.writeText(lines.map((l) => l.text).join('\n')), }, ] : []; @@ -228,12 +248,17 @@ export function ConsoleTab() { loading={starting} style={!running ? { backgroundColor: color, color: '#08090d', borderColor: color } : {}} > - {running ? 'Stop' : 'Run'} + {running ? t('console.stop') : t('console.run')} {running && ( - )} @@ -252,7 +277,7 @@ export function ConsoleTab() { @@ -266,14 +291,14 @@ export function ConsoleTab() { className="text-xs font-mono transition-colors" style={{ color }} > - scroll to bottom + {t('console.scrollToBottom')} )} @@ -281,13 +306,13 @@ export function ConsoleTab() { - {lines.length.toLocaleString()} lines + {lines.length.toLocaleString()} {t('console.lines')}
@@ -308,63 +333,53 @@ export function ConsoleTab() { } if (e.key === 'Escape') closeSearch(); }} - placeholder="Search console... (Enter next, Shift+Enter prev)" - className="flex-1 bg-base-950 border border-surface-border rounded px-2.5 py-1 - text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40" + placeholder={t('console.searchPlaceholder')} + className="flex-1 bg-base-950 border border-surface-border rounded-md px-2.5 py-1 text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/40 transition-colors" + style={{ fontSize: Math.max(fontSize - 1, 11) }} /> - {searchTerm && ( - - {matchIndices.length === 0 - ? 'No matches' - : `${clampedIdx + 1} / ${matchIndices.length}`} - - )} + + {matchIndices.length > 0 + ? `${clampedIdx + 1}/${matchIndices.length}` + : searchTerm + ? t('console.noMatches') + : ''} + -
)} {errorMsg && ( -
- {errorMsg} - +
+ {errorMsg}
)}
!searchOpen && inputRef.current?.focus()} - className="flex-1 overflow-y-auto overflow-x-auto bg-base-950 select-text" - style={{ fontSize, lineHeight: 1.6, fontFamily: 'monospace' }} + className="flex-1 overflow-auto bg-base-950 select-text" + style={{ fontSize }} >
{lines.length === 0 && (
- {running ? 'Waiting for output...' : 'Process not running. Press Run to start.'} + {running ? t('console.waiting') : t('console.notRunning')}
)} {lines.map((line, i) => { @@ -407,11 +422,7 @@ export function ConsoleTab() { onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} disabled={!running} - placeholder={ - running - ? 'Send command... (up/down history, Ctrl+L clear, Ctrl+F search)' - : 'Start the process to send commands' - } + placeholder={running ? t('console.inputPlaceholder') : t('console.inputDisabled')} className="flex-1 bg-transparent text-xs font-mono text-text-primary placeholder:text-text-muted focus:outline-none disabled:opacity-40" style={{ fontSize }} /> @@ -451,46 +462,61 @@ const ConsoleLineRow = React.forwardRef< isAnyMatch: boolean; onContextMenu: (e: React.MouseEvent, text: string) => void; } ->(({ line, lineNum, showLineNum, showTimestamp, wordWrap, searchTerm, isCurrentMatch, isAnyMatch, onContextMenu }, ref) => { - const text = line.text || ' '; - const content = - searchTerm && isAnyMatch ? renderHighlighted(text, searchTerm, isCurrentMatch) : text; +>( + ( + { + line, + lineNum, + showLineNum, + showTimestamp, + wordWrap, + searchTerm, + isCurrentMatch, + isAnyMatch, + onContextMenu, + }, + ref, + ) => { + const text = line.text || ' '; + const content = + searchTerm && isAnyMatch ? renderHighlighted(text, searchTerm, isCurrentMatch) : text; - return ( -
onContextMenu(e, line.text)} - className={[ - 'flex gap-0 px-2', - LINE_COLORS[line.type], - isCurrentMatch - ? 'bg-yellow-400/10' - : isAnyMatch - ? 'bg-yellow-400/5' - : 'hover:bg-white/[0.02]', - ].join(' ')} - > - {showLineNum && ( - - {lineNum} - - )} - {showTimestamp && ( - - {formatTimestamp(line.timestamp)} - - )} - onContextMenu(e, line.text)} className={[ - 'font-mono flex-1', - wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre', + 'flex gap-0 px-2', + LINE_COLORS[line.type], + isCurrentMatch + ? 'bg-yellow-400/10' + : isAnyMatch + ? 'bg-yellow-400/5' + : 'hover:bg-white/[0.02]', ].join(' ')} > - {content} - -
- ); -}); + {showLineNum && ( + + {lineNum} + + )} + {showTimestamp && ( + + {formatTimestamp(line.timestamp)} + + )} + + {content} + +
+ ); + }, +); ConsoleLineRow.displayName = 'ConsoleLineRow'; function renderHighlighted(text: string, term: string, isCurrent: boolean): React.ReactNode { @@ -512,7 +538,7 @@ function renderHighlighted(text: string, term: string, isCurrent: boolean): Reac } > {text.slice(idx, idx + term.length)} - + , ); last = idx + term.length; idx = lower.indexOf(term, last); diff --git a/src/renderer/components/profiles/ProfileSidebar.tsx b/src/renderer/components/profiles/ProfileSidebar.tsx index 2887dd4..6a6c4dd 100644 --- a/src/renderer/components/profiles/ProfileSidebar.tsx +++ b/src/renderer/components/profiles/ProfileSidebar.tsx @@ -16,6 +16,7 @@ import { } from 'react-icons/vsc'; import { useApp, PROFILE_COLORS } from '../../AppProvider'; import { useDevMode } from '../../hooks/useDevMode'; +import { useTranslation } from '../../i18n/I18nProvider'; import { Dialog } from '../common/Dialog'; import { ContextMenu, ContextMenuItem } from '../common/ContextMenu'; import { TemplateModal } from './TemplateModal'; @@ -46,6 +47,7 @@ export function ProfileSidebar({ activeSidePanel }: Props) { reorderProfiles, } = useApp(); const devMode = useDevMode(); + const { t } = useTranslation(); const [ctxMenu, setCtxMenu] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); @@ -66,13 +68,13 @@ export function ProfileSidebar({ activeSidePanel }: Props) { const handleStart = useCallback( async (profile: Profile) => { if (!hasJarConfigured(profile)) { - setActionError(`"${profile.name}" has no JAR configured.`); + setActionError(`"${profile.name}" ${t('console.noJar')}`); return; } const res = await startProcess(profile); if (!res.ok) setActionError(res.error ?? 'Failed to start'); }, - [startProcess] + [startProcess, t], ); const handleStop = useCallback( @@ -80,27 +82,27 @@ export function ProfileSidebar({ activeSidePanel }: Props) { const res = await stopProcess(profile.id); if (!res.ok) setActionError(res.error ?? 'Failed to stop'); }, - [stopProcess] + [stopProcess], ); const ctxItems: ContextMenuItem[] = ctxProfile ? [ ctxRunning ? { - label: 'Stop', + label: t('console.stop'), icon: , danger: true, onClick: () => handleStop(ctxProfile), } : { - label: 'Start', + label: t('console.run'), icon: , disabled: !hasJarConfigured(ctxProfile), onClick: () => handleStart(ctxProfile), }, { type: 'separator' }, { - label: 'Select', + label: t('ctx.select'), icon: , onClick: () => { setActiveProfile(ctxProfile.id); @@ -108,13 +110,13 @@ export function ProfileSidebar({ activeSidePanel }: Props) { }, }, { - label: 'Clear Console', + label: t('ctx.clearConsole'), icon: , onClick: () => clearConsole(ctxProfile.id), }, { type: 'separator' }, { - label: 'Delete', + label: t('general.delete'), icon: , danger: true, disabled: !canDelete, @@ -139,13 +141,13 @@ export function ProfileSidebar({ activeSidePanel }: Props) { onClick={() => createProfile()} className="w-full flex items-center gap-2 px-2.5 py-1.5 rounded-md text-xs font-mono text-text-muted hover:text-accent hover:bg-surface-raised transition-colors border border-dashed border-surface-border hover:border-accent/40" > - New Profile + {t('sidebar.newProfile')}
@@ -157,7 +159,7 @@ export function ProfileSidebar({ activeSidePanel }: Props) { > {state.profiles.length === 0 && (

- No profiles yet. + {t('sidebar.noProfiles')}

)} {state.profiles.map((profile) => ( @@ -185,26 +187,26 @@ export function ProfileSidebar({ activeSidePanel }: Props) {
openPanel('/utilities')} icon={} /> openPanel('/faq')} icon={} /> openPanel('/settings')} icon={} /> {devMode && ( openPanel('/developer')} icon={} @@ -227,18 +229,18 @@ export function ProfileSidebar({ activeSidePanel }: Props) { setActionError(null)} onCancel={() => setActionError(null)} /> { if (deleteTarget) await deleteProfile(deleteTarget.id); @@ -311,7 +313,9 @@ function ProfileItem({ )} - {running && } + {running && ( + + )} ); } diff --git a/src/renderer/components/settings/PublisherBadge.tsx b/src/renderer/components/settings/PublisherBadge.tsx index 23549a5..fdc972c 100644 --- a/src/renderer/components/settings/PublisherBadge.tsx +++ b/src/renderer/components/settings/PublisherBadge.tsx @@ -1,13 +1,16 @@ import React from 'react'; import { VscVerified, VscWarning } from 'react-icons/vsc'; import { LuBot } from 'react-icons/lu'; -import { getPublisherTrust, TrustLevel } from '../../../main/shared/config/TrustedPublishers.config'; +import { getPublisherTrust, TrustLevel } from '../../../main/shared/config/GitHub.config'; interface Props { login: string; } -const BADGE_STYLES: Record = { +const BADGE_STYLES: Record< + TrustLevel, + { bg: string; border: string; text: string; icon: React.ReactNode } +> = { trusted: { bg: 'bg-green-500/10', border: 'border-green-500/30', diff --git a/src/renderer/components/settings/SettingsTab.tsx b/src/renderer/components/settings/SettingsTab.tsx index f8dea31..7ececc8 100644 --- a/src/renderer/components/settings/SettingsTab.tsx +++ b/src/renderer/components/settings/SettingsTab.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useApp } from '../../AppProvider'; +import { useTranslation } from '../../i18n/I18nProvider'; import { Button } from '../common/Button'; import { SidebarLayout } from '../layout/SidebarLayout'; import { SETTINGS_TOPICS } from '../../../main/shared/config/Settings.config'; @@ -15,6 +16,7 @@ type SetFn = (patch: Partial) => void; export function SettingsTab() { const { state, saveSettings } = useApp(); + const { t } = useTranslation(); const [draft, setDraft] = useState(null); const [saved, setSaved] = useState(false); const [activeTopic, setActiveTopic] = useState(SETTINGS_TOPICS[0].id); @@ -63,18 +65,18 @@ export function SettingsTab() { setTimeout(() => setSaved(false), 2000); }; - // Sections that don't need draft/set props - const isStandaloneSection = activeTopic === 'appearance' || activeTopic === 'updates' || activeTopic === 'about'; + const isStandaloneSection = + activeTopic === 'appearance' || activeTopic === 'updates' || activeTopic === 'about'; return (
{(isDirty || saved) && (
- {saved ? 'Settings saved' : 'Unsaved changes'} + {saved ? t('settings.saved') : t('settings.unsaved')}
)} diff --git a/src/renderer/components/settings/sections/AppearanceSection.tsx b/src/renderer/components/settings/sections/AppearanceSection.tsx index 300affe..4b8f526 100644 --- a/src/renderer/components/settings/sections/AppearanceSection.tsx +++ b/src/renderer/components/settings/sections/AppearanceSection.tsx @@ -11,7 +11,7 @@ type FetchState = 'idle' | 'loading' | 'done' | 'error'; export function AppearanceSection() { const { theme, setTheme, availableThemes, refreshThemes } = useTheme(); - const { language, setLanguage, availableLanguages, refreshLanguages } = useTranslation(); + const { language, t, setLanguage, availableLanguages, refreshLanguages } = useTranslation(); const [remoteThemes, setRemoteThemes] = useState([]); const [remoteLangs, setRemoteLangs] = useState([]); @@ -27,26 +27,30 @@ export function AppearanceSection() { const fetchThemes = async () => { setThemeFetch('loading'); const res = await window.api.fetchRemoteThemes(); - if (res.ok && res.themes) { setRemoteThemes(res.themes); setThemeFetch('done'); } - else setThemeFetch('error'); + if (res.ok && res.themes) { + setRemoteThemes(res.themes); + setThemeFetch('done'); + } else setThemeFetch('error'); }; const fetchLangs = async () => { setLangFetch('loading'); const res = await window.api.fetchRemoteLanguages(); - if (res.ok && res.languages) { setRemoteLangs(res.languages); setLangFetch('done'); } - else setLangFetch('error'); + if (res.ok && res.languages) { + setRemoteLangs(res.languages); + setLangFetch('done'); + } else setLangFetch('error'); }; - const selectTheme = async (t: ThemeDefinition) => { - await window.api.installTheme(t); - await window.api.setActiveTheme(t.id); + const selectTheme = async (th: ThemeDefinition) => { + await window.api.installTheme(th); + await window.api.setActiveTheme(th.id); await refreshThemes(); }; const selectLang = async (l: LanguageDefinition) => { await window.api.installLanguage(l); - await window.api.setActiveLanguage(l.id); + await setLanguage(l.id); await refreshLanguages(); }; @@ -58,34 +62,38 @@ export function AppearanceSection() { setTimeout(() => setDevSynced(false), 2000); }; - const allThemes = mergeById(availableThemes, remoteThemes, (t) => t.id); + const allThemes = mergeById(availableThemes, remoteThemes, (th) => th.id); const allLangs = mergeById(availableLanguages, remoteLangs, (l) => l.id); return ( <> -
+
-

Select a visual theme

+

{t('settings.themeHint')}

{themeFetch === 'error' && ( -

Failed to fetch themes.

+

+ {t('appearance.fetchThemesFailed')} +

)}
- {allThemes.map((t) => ( + {allThemes.map((th) => ( ))}
-
+
-

Select a display language

+

{t('settings.languageHint')}

{langFetch === 'error' && ( -

Failed to fetch languages.

+

+ {t('appearance.fetchLangsFailed')} +

)}
{allLangs.map((l) => (
{isDev && ( -
+
-

Sync local project files

-

- Load themes and languages from /themes and /languages in the project root -

+

{t('appearance.syncTitle')}

+

{t('appearance.syncHint')}

@@ -159,7 +173,6 @@ export function AppearanceSection() { ); } -/** Merge local + remote lists by ID, preferring remote version if newer */ function mergeById(local: T[], remote: T[], getId: (item: T) => string): T[] { const map = new Map(); for (const item of local) map.set(getId(item), item); diff --git a/src/renderer/components/settings/sections/ConsoleSection.tsx b/src/renderer/components/settings/sections/ConsoleSection.tsx index a278684..6f54b36 100644 --- a/src/renderer/components/settings/sections/ConsoleSection.tsx +++ b/src/renderer/components/settings/sections/ConsoleSection.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Toggle } from '../../common/Toggle'; import { Section, Row, NumInput } from '../SettingsRow'; +import { useTranslation } from '../../../i18n/I18nProvider'; import { AppSettings } from '../../../../main/shared/types/App.types'; interface Props { @@ -9,9 +10,11 @@ interface Props { } export function ConsoleSection({ draft, set }: Props) { + const { t } = useTranslation(); + return ( -
- +
+
- + set({ consoleLineNumbers: v })} /> - + set({ consoleTimestamps: v })} /> - + set({ consoleWordWrap: v })} /> - + set({ consoleMaxLines: v })} /> - + -
- +
+ set({ launchOnStartup: v })} /> - + set({ startMinimized: v })} disabled={!draft.launchOnStartup} /> - + set({ minimizeToTray: v })} />
diff --git a/src/renderer/i18n/I18nProvider.tsx b/src/renderer/i18n/I18nProvider.tsx index e25413e..f769313 100644 --- a/src/renderer/i18n/I18nProvider.tsx +++ b/src/renderer/i18n/I18nProvider.tsx @@ -1,10 +1,11 @@ -import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'; import type { LanguageDefinition } from '../../main/shared/types/Language.types'; import { ENGLISH } from '../../main/shared/config/DefaultLanguage.config'; +import type { TranslationKey } from './TranslationKeys'; interface I18nContextValue { language: LanguageDefinition; - t: (key: string, params?: Record) => string; + t: (key: TranslationKey, params?: Record) => string; setLanguage: (id: string) => Promise; availableLanguages: LanguageDefinition[]; refreshLanguages: () => Promise; @@ -15,16 +16,25 @@ const I18nContext = createContext(null); export function I18nProvider({ children }: { children: React.ReactNode }) { const [language, setLang] = useState(ENGLISH); const [available, setAvailable] = useState([ENGLISH]); + const langRef = useRef(ENGLISH); + + useEffect(() => { + langRef.current = language; + }, [language]); useEffect(() => { if (!window.api) return; - window.api.getActiveLanguage().then(setLang); + window.api.getActiveLanguage().then((l) => { + setLang(l); + langRef.current = l; + }); window.api.getLanguageState().then((s) => setAvailable(s.languages)); }, []); const t = useCallback( - (key: string, params?: Record) => { - let str = language.strings[key] ?? ENGLISH.strings[key] ?? key; + (key: TranslationKey, params?: Record) => { + const strings = language.strings; + let str = strings[key] ?? ENGLISH.strings[key] ?? key; if (params) { for (const [k, v] of Object.entries(params)) { str = str.replace(`{${k}}`, v); @@ -32,12 +42,13 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { } return str; }, - [language] + [language], ); const setLanguage = useCallback(async (id: string) => { if (!window.api) return; const lang = await window.api.setActiveLanguage(id); + langRef.current = lang; setLang(lang); }, []); @@ -45,10 +56,15 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { if (!window.api) return; const state = await window.api.getLanguageState(); setAvailable(state.languages); + const active = state.languages.find((l) => l.id === state.activeLanguageId) ?? ENGLISH; + langRef.current = active; + setLang(active); }, []); return ( - + {children} ); diff --git a/src/renderer/i18n/TranslationKeys.ts b/src/renderer/i18n/TranslationKeys.ts new file mode 100644 index 0000000..64cd441 --- /dev/null +++ b/src/renderer/i18n/TranslationKeys.ts @@ -0,0 +1,3 @@ +import { ENGLISH_STRINGS } from '../../main/shared/config/DefaultLanguage.config'; + +export type TranslationKey = keyof typeof ENGLISH_STRINGS;