Sprache Deutsch/Englisch

Umschalter in den Einstellungen und auf der Anmeldeseite; Meldungen des Launchers (Lang.cs)
und der Website (Accept-Language) in der gewaehlten Sprache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 19:44:07 +01:00
co-authored by Claude Opus 5.5
parent 3281468bee
commit 07da29ab94
11 changed files with 333 additions and 117 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
@@ -8,7 +8,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>BSO.Launcher</RootNamespace> <RootNamespace>BSO.Launcher</RootNamespace>
<AssemblyName>BSO-Launcher</AssemblyName> <AssemblyName>BSO-Launcher</AssemblyName>
<Version>0.3.0</Version> <Version>0.3.1</Version>
<Company>MBM.wtf</Company> <Company>MBM.wtf</Company>
<Product>BSO Launcher</Product> <Product>BSO Launcher</Product>
<Description>BSO Launcher - Bus Simulator Online</Description> <Description>BSO Launcher - Bus Simulator Online</Description>
+3
View File
@@ -3,6 +3,9 @@
Neue Version: hier einen Abschnitt `## <Version>` anlegen, `<Version>` in `BSO.Launcher.csproj` erhöhen, dann Neue Version: hier einen Abschnitt `## <Version>` anlegen, `<Version>` in `BSO.Launcher.csproj` erhöhen, dann
`dotnet publish -c Release -o publish -p:BsoRelease=true` – der Abschnitt wird der Changelog auf der Website und im Launcher. `dotnet publish -c Release -o publish -p:BsoRelease=true` – der Abschnitt wird der Changelog auf der Website und im Launcher.
## 0.3.1
- Sprache: Deutsch oder Englisch (Einstellungen oder Umschalter auf der Anmeldeseite) – auch Meldungen und Fehler der Website
## 0.3.0 ## 0.3.0
- Freunde: per Benutzername hinzufügen, Anfragen annehmen, sehen wer online ist - Freunde: per Benutzername hinzufügen, Anfragen annehmen, sehen wer online ist
- Lobby: Freunde einladen, Server starten, zusammen spielen – der Server läuft auf dem BSO-Relay, keine Portfreigabe nötig - Lobby: Freunde einladen, Server starten, zusammen spielen – der Server läuft auf dem BSO-Relay, keine Portfreigabe nötig
+7 -4
View File
@@ -32,6 +32,7 @@ public sealed class MainForm : Form
ClientSize = new Size(1120, 700); ClientSize = new Size(1120, 700);
MinimumSize = new Size(900, 600); MinimumSize = new Size(900, 600);
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
Lang.Set(settings.Language);
LauncherLog.Info("BSO Launcher " + SelfUpdate.CurrentVersion + " gestartet" + (Api.Dev ? " (-dev)" : "") + ", Website " + Api.Base); LauncherLog.Info("BSO Launcher " + SelfUpdate.CurrentVersion + " gestartet" + (Api.Dev ? " (-dev)" : "") + ", Website " + Api.Base);
if (Api.Base != Api.BaseUrl) Text += " (DEV)"; if (Api.Base != Api.BaseUrl) Text += " (DEV)";
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { } try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
@@ -120,7 +121,7 @@ public sealed class MainForm : Form
} }
catch (Exception e) catch (Exception e)
{ {
var answer = MessageBox.Show("Die Microsoft Edge WebView2 Runtime fehlt (" + e.Message + ").\n\nDownload-Seite oeffnen?", var answer = MessageBox.Show(Lang.T("Die Microsoft Edge WebView2 Runtime fehlt (" + e.Message + ").\n\nDownload-Seite oeffnen?"),
Installer.AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Error); Installer.AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Error);
if (answer == DialogResult.Yes) OpenUrl("https://developer.microsoft.com/microsoft-edge/webview2/"); if (answer == DialogResult.Yes) OpenUrl("https://developer.microsoft.com/microsoft-edge/webview2/");
Close(); Close();
@@ -189,7 +190,7 @@ public sealed class MainForm : Form
else web.CoreWebView2?.PostWebMessageAsJson(json); else web.CoreWebView2?.PostWebMessageAsJson(json);
} }
void Progress(string task, double value, string text) => Emit(new { @event = "progress", task, value, text }); void Progress(string task, double value, string text) => Emit(new { @event = "progress", task, value, text = Lang.T(text) });
async Task OnMessage(string raw) async Task OnMessage(string raw)
{ {
@@ -220,7 +221,7 @@ public sealed class MainForm : Form
catch (Exception e) catch (Exception e)
{ {
LauncherLog.Error(e.Message); LauncherLog.Error(e.Message);
Emit(new { id, ok = false, error = e.Message }); Emit(new { id, ok = false, error = Lang.T(e.Message) });
} }
} }
@@ -246,6 +247,7 @@ public sealed class MainForm : Form
username = settings.Username, username = settings.Username,
loggedIn = settings.Token != null, loggedIn = settings.Token != null,
master = Api.Base, master = Api.Base,
language = Lang.Current,
dev = Api.Dev || Api.Base != Api.BaseUrl dev = Api.Dev || Api.Base != Api.BaseUrl
}; };
@@ -328,11 +330,12 @@ public sealed class MainForm : Form
settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect; settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect;
settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen; settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen;
settings.AutoUpdateMod = BoolArg(args, "autoUpdateMod") ?? settings.AutoUpdateMod; settings.AutoUpdateMod = BoolArg(args, "autoUpdateMod") ?? settings.AutoUpdateMod;
if (Arg(args, "language") is "de" or "en") { settings.Language = Arg(args, "language"); Lang.Set(settings.Language); }
settings.Save(); settings.Save();
return null; return null;
case "chooseGameFolder": case "chooseGameFolder":
{ {
using var dialog = new FolderBrowserDialog { Description = "Ordner \"Bus-Simulator 2012\" waehlen", UseDescriptionForTitle = true }; using var dialog = new FolderBrowserDialog { Description = Lang.T("Ordner \"Bus-Simulator 2012\" waehlen"), UseDescriptionForTitle = true };
if (dialog.ShowDialog(this) != DialogResult.OK) return new { changed = false }; if (dialog.ShowDialog(this) != DialogResult.OK) return new { changed = false };
string folder = dialog.SelectedPath; string folder = dialog.SelectedPath;
// Auch Bin_High_Win64 selbst gewaehlt? Dann den Ordner darueber nehmen // Auch Bin_High_Win64 selbst gewaehlt? Dann den Ordner darueber nehmen
+2 -1
View File
@@ -6,6 +6,7 @@ static class Program
static int Main(string[] args) static int Main(string[] args)
{ {
ApplicationConfiguration.Initialize(); ApplicationConfiguration.Initialize();
Lang.Set(Settings.Load().Language);
// Deinstallieren (aus "Apps & Features") // Deinstallieren (aus "Apps & Features")
if (args.Contains("--uninstall")) if (args.Contains("--uninstall"))
@@ -33,7 +34,7 @@ static class Program
} }
if (!first) if (!first)
{ {
MessageBox.Show("Der BSO Launcher laeuft schon.", Installer.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Show(Lang.T("Der BSO Launcher laeuft schon."), Installer.AppName, MessageBoxButtons.OK, MessageBoxIcon.Information);
return 0; return 0;
} }
SelfUpdate.CleanupOldVersion(); SelfUpdate.CleanupOldVersion();
+12 -12
View File
@@ -45,7 +45,7 @@ public static class Installer
/// <summary>Installieren. progress(0..1, Text).</summary> /// <summary>Installieren. progress(0..1, Text).</summary>
public static void Install(Action<double, string> progress) public static void Install(Action<double, string> progress)
{ {
progress(0.1, "Laufenden Launcher schliessen …"); progress(0.1, Lang.T("Laufenden Launcher schliessen …"));
foreach (var p in Process.GetProcessesByName("BSO-Launcher")) foreach (var p in Process.GetProcessesByName("BSO-Launcher"))
{ {
try try
@@ -58,19 +58,19 @@ public static class Installer
catch { } catch { }
} }
progress(0.3, "Dateien kopieren …"); progress(0.3, Lang.T("Dateien kopieren …"));
Directory.CreateDirectory(InstallDir); Directory.CreateDirectory(InstallDir);
string tmp = InstalledExe + ".new"; string tmp = InstalledExe + ".new";
File.Copy(Self, tmp, true); File.Copy(Self, tmp, true);
File.Move(tmp, InstalledExe, true); File.Move(tmp, InstalledExe, true);
progress(0.6, "Verknuepfungen anlegen …"); progress(0.6, Lang.T("Verknuepfungen anlegen …"));
CreateShortcut(StartMenuShortcut); CreateShortcut(StartMenuShortcut);
CreateShortcut(DesktopShortcut); CreateShortcut(DesktopShortcut);
progress(0.8, "Registrieren …"); progress(0.8, Lang.T("Registrieren …"));
Register(); Register();
progress(1.0, "Fertig"); progress(1.0, Lang.T("Fertig"));
} }
/// <summary>Eintrag unter "Apps &amp; Features" (bzw. dessen Version nach einem Selbst-Update).</summary> /// <summary>Eintrag unter "Apps &amp; Features" (bzw. dessen Version nach einem Selbst-Update).</summary>
@@ -141,13 +141,13 @@ public static class Installer
if (!quiet) if (!quiet)
{ {
var answer = MessageBox.Show( var answer = MessageBox.Show(
"BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) " + Lang.T("BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) " +
"und deine Anmeldung im Launcher.", "und deine Anmeldung im Launcher."),
AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (answer != DialogResult.Yes) return false; if (answer != DialogResult.Yes) return false;
while (Game.IsRunning()) while (Game.IsRunning())
{ {
if (MessageBox.Show("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung.", AppName, if (MessageBox.Show(Lang.T("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung."), AppName,
MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning) != DialogResult.Retry) return false; MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning) != DialogResult.Retry) return false;
} }
} }
@@ -162,9 +162,9 @@ public static class Installer
// Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist // Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist
RemoveAfterExit(dir, Settings.Folder); RemoveAfterExit(dir, Settings.Folder);
if (!quiet) if (!quiet)
MessageBox.Show(problems.Count == 0 MessageBox.Show(Lang.T(problems.Count == 0
? "BSO wurde komplett deinstalliert." ? "BSO wurde komplett deinstalliert."
: "BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n" + string.Join("\n", problems), : "BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n" + string.Join("\n", problems)),
AppName, MessageBoxButtons.OK, problems.Count == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning); AppName, MessageBoxButtons.OK, problems.Count == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning);
return true; return true;
} }
@@ -198,7 +198,7 @@ public sealed class InstallForm : Form
status.Font = new Font("Segoe UI", 9.5f); status.Font = new Font("Segoe UI", 9.5f);
status.TextAlign = ContentAlignment.MiddleCenter; status.TextAlign = ContentAlignment.MiddleCenter;
status.SetBounds(20, 178, 380, 24); status.SetBounds(20, 178, 380, 24);
status.Text = "Vorbereiten …"; status.Text = Lang.T("Vorbereiten …");
Controls.Add(status); Controls.Add(status);
Shown += async (_, _) => await Run(); Shown += async (_, _) => await Run();
} }
@@ -238,7 +238,7 @@ public sealed class InstallForm : Form
{ {
error = e.Message; error = e.Message;
status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d); status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d);
status.Text = "Installation fehlgeschlagen: " + e.Message; status.Text = Lang.T("Installation fehlgeschlagen: " + e.Message);
Invalidate(); Invalidate();
await Task.Delay(6000); await Task.Delay(6000);
Close(); Close();
+83
View File
@@ -0,0 +1,83 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace BSO.Launcher;
/// <summary>
/// Sprache der Meldungen, die vom Launcher selbst kommen (Fehler, Fortschritt, Dialoge). Im Code steht Deutsch; T() sucht
/// die englische Fassung - auch fuer Texte mit eingesetzten Werten ({0} im Muster). Die Oberflaeche hat ihre eigene Liste
/// (ui/i18n.js), Fehler der Website kommen dank Accept-Language schon in der richtigen Sprache.
/// </summary>
public static class Lang
{
/// <summary>"de" oder "en" - aus den Einstellungen, beim ersten Start aus der Windows-Sprache.</summary>
public static string Current { get; set; } = Default;
public static string Default => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "de" ? "de" : "en";
static readonly (string de, string en)[] Texts =
{
("Der BSO Launcher laeuft schon.", "The BSO Launcher is already running."),
("Die Microsoft Edge WebView2 Runtime fehlt ({0}).\n\nDownload-Seite oeffnen?", "The Microsoft Edge WebView2 Runtime is missing ({0}).\n\nOpen the download page?"),
("Laeuft schon - bitte warten", "Already in progress - please wait"),
("Kein Launcher-Update gefunden", "No launcher update found"),
("Launcher-Update wird geladen …", "Downloading launcher update …"),
("In diesem Ordner ist kein Bus-Simulator 2012 (Bin_High_Win64\\BusSimulator2012.exe fehlt)", "There is no Bus-Simulator 2012 in this folder (Bin_High_Win64\\BusSimulator2012.exe is missing)"),
("Unbekannter Befehl: {0}", "Unknown command: {0}"),
("Bitte anmelden", "Please sign in"),
("Anmeldung abgelaufen - bitte neu anmelden", "Session expired - please sign in again"),
("Fehlgeschlagen", "Failed"),
("Bus-Simulator 2012 nicht gefunden - in den Einstellungen den Spielordner waehlen", "Bus-Simulator 2012 not found - choose the game folder in the settings"),
("Noch keine BSO-Version auf dem Server", "No BSO version on the server yet"),
("BSO {0} wird geladen …", "Downloading BSO {0} …"),
("Installieren …", "Installing …"),
("Fuer BSO fehlt die .NET 10 Runtime (x64) - unter \"Status\" herunterladen", "BSO needs the .NET 10 Runtime (x64) - download it on the Play page"),
("Bus-Simulator 2012 laeuft schon", "Bus-Simulator 2012 is already running"),
("BSO ist nicht installiert und der BSO-Server ist nicht erreichbar", "BSO is not installed and the BSO server is not reachable"),
("{0} {1} ist nicht von BSO signiert - nicht installiert", "{0} {1} is not signed by BSO - not installed"),
("Ungueltige Pruefsumme", "Invalid checksum"),
("Download abgebrochen (keine Verbindung) - nochmal versuchen, er macht dort weiter", "Download interrupted (no connection) - try again, it continues where it stopped"),
("Der Download ist beschaedigt (Pruefsumme passt nicht) - bitte nochmal versuchen", "The download is damaged (checksum mismatch) - please try again"),
("BSO-Server nicht erreichbar ({0})", "BSO server not reachable ({0})"),
("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Mod-Dateien gesperrt", "Close Bus-Simulator 2012 first - the mod files are locked while it runs"),
("Ungueltiger Pfad im Mod-Paket: {0}", "Invalid path in the mod package: {0}"),
("Laufenden Launcher schliessen …", "Closing the running launcher …"),
("Dateien kopieren …", "Copying files …"),
("Verknuepfungen anlegen …", "Creating shortcuts …"),
("Registrieren …", "Registering …"),
("Fertig", "Done"),
("Vorbereiten …", "Preparing …"),
("Installation fehlgeschlagen: {0}", "Installation failed: {0}"),
("BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) und deine Anmeldung im Launcher.",
"Uninstall BSO completely?\n\nThis removes the launcher, BSO from the Bus-Simulator 2012 folder (dinput8.dll, BSO folder) and your launcher sign-in."),
("Erst Bus-Simulator 2012 schliessen - solange es laeuft, sind die Dateien in Benutzung.", "Close Bus-Simulator 2012 first - its files are in use while it runs."),
("BSO wurde komplett deinstalliert.", "BSO was uninstalled completely."),
("BSO wurde deinstalliert. Einiges liess sich nicht entfernen:\n\n{0}", "BSO was uninstalled. Some things could not be removed:\n\n{0}"),
("Neuigkeiten nicht verfuegbar", "News not available"),
("Ordner \"Bus-Simulator 2012\" waehlen", "Choose the \"Bus-Simulator 2012\" folder"),
("Konto konnte nicht geprueft werden: {0}", "Account could not be checked: {0}")
};
static readonly List<(Regex pattern, string en)> patterns = Texts.Select(x => (new Regex("^" + Regex.Replace(Regex.Escape(x.de), @"\\\{(\d)}", m => $"(?<a{m.Groups[1].Value}>.*?)") + "$", RegexOptions.Singleline), x.en)).ToList();
/// <summary>Deutschen Text (auch mit eingesetzten Werten) in die aktuelle Sprache. Unbekannt -> unveraendert.</summary>
public static string T(string de)
{
if (Current != "en" || string.IsNullOrEmpty(de)) return de;
foreach ((Regex pattern, string en) in patterns)
{
var m = pattern.Match(de);
if (!m.Success) continue;
return Regex.Replace(en, @"\{(\d)}", x => m.Groups["a" + x.Groups[1].Value].Value);
}
return de;
}
/// <summary>Sprache wechseln - auch fuer die Anfragen an die Website (Accept-Language: deren Fehler kommen passend).</summary>
public static void Set(string? lang)
{
Current = lang == "de" || lang == "en" ? lang : Default;
Api.Http.DefaultRequestHeaders.AcceptLanguage.Clear();
Api.Http.DefaultRequestHeaders.AcceptLanguage.ParseAdd(Current);
}
}
+2
View File
@@ -23,6 +23,8 @@ public sealed class Settings
public bool AutoConnect { get; set; } = true; public bool AutoConnect { get; set; } = true;
public bool KeepOpen { get; set; } public bool KeepOpen { get; set; }
public bool AutoUpdateMod { get; set; } = true; public bool AutoUpdateMod { get; set; } = true;
/// <summary>"de" oder "en" - null = nach der Windows-Sprache (Lang.Default).</summary>
public string? Language { get; set; }
[System.Text.Json.Serialization.JsonIgnore] [System.Text.Json.Serialization.JsonIgnore]
public string? Token public string? Token
+9
View File
@@ -146,6 +146,15 @@ nav button.on { background: var(--panel-2); color: var(--text); }
.dot.bad { background: var(--danger); } .dot.bad { background: var(--danger); }
.row { display: flex; gap: 8px; align-items: center; } .row { display: flex; gap: 8px; align-items: center; }
/* ---------------------------------------------------------------- Sprache */
.lang-switch { display: inline-flex; padding: 3px; border-radius: 9px; background: var(--bg); border: 1px solid var(--border); gap: 2px; }
.lang-switch button { border: 0; background: transparent; color: var(--muted); font: inherit; font-size: 12px; font-weight: 800; letter-spacing: .04em;
padding: 4px 10px; border-radius: 7px; cursor: pointer; }
.lang-switch button.on { background: var(--panel-2); color: var(--text); }
.sidebar-foot { display: grid; gap: 10px; justify-items: start; }
.sidebar-foot .me-card { justify-self: stretch; }
.auth-lang { position: fixed; top: calc(var(--titlebar) + 14px); right: 14px; }
/* ---------------------------------------------------------------- Freunde & Lobby */ /* ---------------------------------------------------------------- Freunde & Lobby */
.nav-badge { margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--danger); color: #fff; .nav-badge { margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--danger); color: #fff;
font-size: 11px; font-weight: 800; display: grid; place-items: center; } font-size: 11px; font-weight: 800; display: grid; place-items: center; }
+104 -81
View File
@@ -1,5 +1,6 @@
"use strict"; "use strict";
// BSO Launcher - Oberflaeche. Spricht ueber window.chrome.webview mit dem Launcher (C#), nie direkt mit dem Netz. // BSO Launcher - Oberflaeche. Spricht ueber window.chrome.webview mit dem Launcher (C#), nie direkt mit dem Netz.
// Texte auf Deutsch im Code, t() uebersetzt (i18n.js).
// ---------------------------------------------------------------- Verbindung zum Launcher // ---------------------------------------------------------------- Verbindung zum Launcher
@@ -46,9 +47,9 @@ function h(tag, attrs, ...children) {
} }
function toast(text, error) { function toast(text, error) {
const t = h("div", { class: "toast" + (error ? " error" : "") }, text); const el = h("div", { class: "toast" + (error ? " error" : "") }, text);
$("toasts").append(t); $("toasts").append(el);
setTimeout(() => t.remove(), error ? 7000 : 4000); setTimeout(() => el.remove(), error ? 7000 : 4000);
} }
/** Knopf waehrend einer Aktion sperren; Fehler als Meldung. */ /** Knopf waehrend einer Aktion sperren; Fehler als Meldung. */
@@ -59,7 +60,19 @@ async function busy(button, action) {
finally { button.disabled = false; } finally { button.disabled = false; }
} }
const date = (iso) => new Date(iso).toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" }); const date = (iso) => new Date(iso).toLocaleDateString(lang === "en" ? "en-GB" : "de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
// ---------------------------------------------------------------- Sprache
/** Sprache wechseln: im Launcher speichern (auch fuer seine eigenen Meldungen), alles neu beschriften. */
async function setLang(l) {
lang = l;
await call("setSettings", { language: l });
translateStatic();
renderLangSwitch(setLang);
setAuthMode(registerMode);
if (!$("app").classList.contains("hidden")) showPage(page);
}
// ---------------------------------------------------------------- Fenster // ---------------------------------------------------------------- Fenster
@@ -78,7 +91,7 @@ function setAuthMode(register) {
$("tabRegister").classList.toggle("on", register); $("tabRegister").classList.toggle("on", register);
$("emailRow").classList.toggle("hidden", !register); $("emailRow").classList.toggle("hidden", !register);
$("password2Row").classList.toggle("hidden", !register); $("password2Row").classList.toggle("hidden", !register);
$("authButton").textContent = register ? "Konto erstellen" : "Anmelden"; $("authButton").textContent = register ? t("Konto erstellen") : t("Anmelden");
$("password").autocomplete = register ? "new-password" : "current-password"; $("password").autocomplete = register ? "new-password" : "current-password";
$("authError").classList.add("hidden"); $("authError").classList.add("hidden");
} }
@@ -91,7 +104,7 @@ $("authForm").addEventListener("submit", async (e) => {
const error = $("authError"); const error = $("authError");
error.classList.add("hidden"); error.classList.add("hidden");
if (registerMode && $("password").value !== $("password2").value) { if (registerMode && $("password").value !== $("password2").value) {
error.textContent = "Die Passwoerter sind nicht gleich"; error.textContent = t("Die Passwoerter sind nicht gleich");
error.classList.remove("hidden"); error.classList.remove("hidden");
return; return;
} }
@@ -101,7 +114,7 @@ $("authForm").addEventListener("submit", async (e) => {
{ username: $("username").value, password: $("password").value, email: $("email").value }); { username: $("username").value, password: $("password").value, email: $("email").value });
$("password").value = $("password2").value = ""; $("password").value = $("password2").value = "";
showApp(r.username); showApp(r.username);
if (registerMode) toast("Willkommen bei BSO, " + r.username + "!"); if (registerMode) toast(t("Willkommen bei BSO, {0}!", r.username));
} catch (err) { } catch (err) {
error.textContent = err.message; error.textContent = err.message;
error.classList.remove("hidden"); error.classList.remove("hidden");
@@ -139,14 +152,14 @@ function showPage(name) {
page = name; page = name;
document.querySelectorAll("#nav [data-page]").forEach((b) => b.classList.toggle("on", b.dataset.page === name)); document.querySelectorAll("#nav [data-page]").forEach((b) => b.classList.toggle("on", b.dataset.page === name));
const root = $("page"); const root = $("page");
root.replaceChildren(h("p", { class: "muted" }, "Laden …")); root.replaceChildren(h("p", { class: "muted" }, t("Laden …")));
({ home: renderHome, friends: renderFriends, news: renderNews, settings: renderSettings })[name](root); ({ home: renderHome, friends: renderFriends, news: renderNews, settings: renderSettings })[name](root);
} }
// Anmeldung abgelaufen (z.B. beim Holen des Spiel-Tickets) // Anmeldung abgelaufen (z.B. beim Holen des Spiel-Tickets)
on("loggedOut", () => showAuth()); on("loggedOut", () => showAuth());
// Fortschritt (Downloads) // Fortschritt (Downloads) - der Text kommt schon uebersetzt vom Launcher
let progressBox = null; let progressBox = null;
on("progress", (m) => { on("progress", (m) => {
if (!progressBox) return; if (!progressBox) return;
@@ -162,31 +175,31 @@ async function renderHome(root) {
if (page !== "home") return; if (page !== "home") return;
progressBox = h("div", { class: "progress hidden" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div"))); progressBox = h("div", { class: "progress hidden" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
const play = h("button", { class: "btn primary play", type: "button" }, s.game.running ? "LAEUFT" : "SPIELEN"); const play = h("button", { class: "btn primary play", type: "button" }, s.game.running ? t("LAEUFT") : t("SPIELEN"));
play.disabled = s.game.running; play.disabled = s.game.running;
play.addEventListener("click", () => busy(play, async () => { play.addEventListener("click", () => busy(play, async () => {
await call("launch"); await call("launch");
toast("Bus-Simulator 2012 startet …"); toast(t("Bus-Simulator 2012 startet …"));
})); }));
const ready = s.game.found && s.dotnet && (s.mod.installed || s.mod.latest); const ready = s.game.found && s.dotnet && (s.mod.installed || s.mod.latest);
const line = s.mod.installed ? `BSO ${s.mod.installed} installiert · Server ${s.settings.server}` : "BSO wird beim ersten Start installiert"; const line = s.mod.installed ? t("BSO {0} installiert · Server {1}", s.mod.installed, s.settings.server) : t("BSO wird beim ersten Start installiert");
const children = [ const children = [
h("div", { class: "hero" }, h("div", { class: "hero" },
h("div", {}, h("h1", {}, "Bus Simulator Online"), h("p", {}, "Multiplayer fuer Bus-Simulator 2012")), h("div", {}, h("h1", {}, "Bus Simulator Online"), h("p", {}, t("Multiplayer fuer Bus-Simulator 2012"))),
h("img", { src: "logo.svg", alt: "" }), h("img", { src: "logo.svg", alt: "" }),
h("div", { class: "hero-actions" }, play, h("span", { class: "muted small" }, ready ? line : "Vor dem Start fehlt noch etwas - siehe unten"))), h("div", { class: "hero-actions" }, play, h("span", { class: "muted small" }, ready ? line : t("Vor dem Start fehlt noch etwas - siehe unten")))),
progressBox progressBox
]; ];
if (s.launcherUpdate) { if (s.launcherUpdate) {
const b = h("button", { class: "btn primary small", type: "button" }, "Jetzt aktualisieren"); const b = h("button", { class: "btn primary small", type: "button" }, t("Jetzt aktualisieren"));
b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast("Launcher startet neu …"); })); b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast(t("Launcher startet neu …")); }));
children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, "Launcher " + s.launcherUpdate), " ist da."), b)); children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, t("Launcher {0}", s.launcherUpdate)), t(" ist da.")), b));
} }
if (!s.masterReachable) if (!s.masterReachable)
children.push(h("div", { class: "banner warn" }, h("span", {}, "BSO-Server nicht erreichbar - Updates werden spaeter geprueft."))); children.push(h("div", { class: "banner warn" }, h("span", {}, t("BSO-Server nicht erreichbar - Updates werden spaeter geprueft."))));
children.push(h("div", { class: "cards" }, gameCard(s), modCard(s), dotnetCard(s), serverCard(s))); children.push(h("div", { class: "cards" }, gameCard(s), modCard(s), dotnetCard(s), serverCard(s)));
root.replaceChildren(...children); root.replaceChildren(...children);
@@ -197,26 +210,26 @@ function card(title, state, value, ...extra) {
} }
function gameCard(s) { function gameCard(s) {
const choose = h("button", { class: "btn small", type: "button" }, s.game.found ? "Anderen Ordner waehlen" : "Ordner waehlen"); const choose = h("button", { class: "btn small", type: "button" }, s.game.found ? t("Anderen Ordner waehlen") : t("Ordner waehlen"));
choose.addEventListener("click", () => busy(choose, async () => { if ((await call("chooseGameFolder")).changed) showPage("home"); })); choose.addEventListener("click", () => busy(choose, async () => { if ((await call("chooseGameFolder")).changed) showPage("home"); }));
return card("Bus-Simulator 2012", s.game.found ? "ok" : "bad", s.game.found ? "Gefunden" : "Nicht gefunden", return card("Bus-Simulator 2012", s.game.found ? "ok" : "bad", s.game.found ? t("Gefunden") : t("Nicht gefunden"),
s.game.found ? h("span", { class: "mono muted" }, s.game.folder) : h("span", { class: "muted small" }, "Ueber Steam installiert? Sonst den Ordner von Hand waehlen."), s.game.found ? h("span", { class: "mono muted" }, s.game.folder) : h("span", { class: "muted small" }, t("Ueber Steam installiert? Sonst den Ordner von Hand waehlen.")),
h("div", { class: "row" }, choose)); h("div", { class: "row" }, choose));
} }
function modCard(s) { function modCard(s) {
const m = s.mod; const m = s.mod;
const state = m.installed && !m.updateAvailable ? "ok" : m.latest ? "warn" : "bad"; const state = m.installed && !m.updateAvailable ? "ok" : m.latest ? "warn" : "bad";
const value = m.installed ? `Version ${m.installed}` : "Nicht installiert"; const value = m.installed ? t("Version {0}", m.installed) : t("Nicht installiert");
const info = m.updateAvailable ? `Neu: ${m.latest}` : m.latest ? "Aktuell" : "Keine Version auf dem Server"; const info = m.updateAvailable ? t("Neu: {0}", m.latest) : m.latest ? t("Aktuell") : t("Keine Version auf dem Server");
const extra = [h("span", { class: "muted small" }, info)]; const extra = [h("span", { class: "muted small" }, info)];
// Immer da: holt die neueste Mod von der Website (bei SPIELEN passiert das sonst automatisch, wenn eingeschaltet) // Immer da: holt die neueste Mod von der Website (bei SPIELEN passiert das sonst automatisch, wenn eingeschaltet)
if (m.latest && s.game.found) { if (m.latest && s.game.found) {
const label = !m.installed ? "Mod herunterladen" : m.updateAvailable ? "Mod aktualisieren" : "Neu herunterladen"; const label = !m.installed ? t("Mod herunterladen") : m.updateAvailable ? t("Mod aktualisieren") : t("Neu herunterladen");
const b = h("button", { class: "btn small" + (!m.installed || m.updateAvailable ? " primary" : ""), type: "button" }, label); const b = h("button", { class: "btn small" + (!m.installed || m.updateAvailable ? " primary" : ""), type: "button" }, label);
b.addEventListener("click", () => busy(b, async () => { b.addEventListener("click", () => busy(b, async () => {
const r = await call("installMod"); const r = await call("installMod");
toast("BSO " + r.version + " installiert"); toast(t("BSO {0} installiert", r.version));
showPage("home"); showPage("home");
})); }));
extra.push(h("div", { class: "row" }, b)); extra.push(h("div", { class: "row" }, b));
@@ -225,11 +238,11 @@ function modCard(s) {
} }
function dotnetCard(s) { function dotnetCard(s) {
if (s.dotnet) return card(".NET 10 Runtime", "ok", "Installiert", h("span", { class: "muted small" }, "Die Mod laeuft mit .NET 10 (x64).")); if (s.dotnet) return card(".NET 10 Runtime", "ok", t("Installiert"), h("span", { class: "muted small" }, t("Die Mod laeuft mit .NET 10 (x64).")));
const b = h("button", { class: "btn small", type: "button" }, "Herunterladen"); const b = h("button", { class: "btn small", type: "button" }, t("Herunterladen"));
b.addEventListener("click", () => call("openDotnet")); b.addEventListener("click", () => call("openDotnet"));
return card(".NET 10 Runtime", "bad", "Fehlt", return card(".NET 10 Runtime", "bad", t("Fehlt"),
h("span", { class: "muted small" }, "BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren."), h("span", { class: "muted small" }, t("BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren.")),
h("div", { class: "row" }, b)); h("div", { class: "row" }, b));
} }
@@ -237,12 +250,12 @@ function serverCard(s) {
const input = h("input", { value: s.settings.server, placeholder: "host:7777", spellcheck: "false" }); const input = h("input", { value: s.settings.server, placeholder: "host:7777", spellcheck: "false" });
const auto = h("input", { type: "checkbox" }); const auto = h("input", { type: "checkbox" });
auto.checked = s.settings.autoConnect; auto.checked = s.settings.autoConnect;
const save = () => call("setSettings", { server: input.value.trim(), autoConnect: auto.checked }).then(() => toast("Gespeichert")); const save = () => call("setSettings", { server: input.value.trim(), autoConnect: auto.checked }).then(() => toast(t("Gespeichert")));
input.addEventListener("change", save); input.addEventListener("change", save);
auto.addEventListener("change", save); auto.addEventListener("change", save);
return card("Server", "ok", "Mitspielen", return card("Server", "ok", t("Mitspielen"),
h("label", {}, h("span", {}, "Adresse"), input), h("label", {}, h("span", {}, t("Adresse")), input),
h("label", { class: "switch" }, h("span", {}, "Beim Start verbinden", h("small", {}, "sonst im Spiel mit F8")), auto)); h("label", { class: "switch" }, h("span", {}, t("Beim Start verbinden"), h("small", {}, t("sonst im Spiel mit F8"))), auto));
} }
// ---------------------------------------------------------------- Freunde & Lobby // ---------------------------------------------------------------- Freunde & Lobby
@@ -258,8 +271,8 @@ async function loadSocial() {
$("friendsBadge").textContent = n; $("friendsBadge").textContent = n;
$("friendsBadge").classList.toggle("hidden", n === 0); $("friendsBadge").classList.toggle("hidden", n === 0);
// Neue Einladungen/Anfragen melden (auch auf anderen Seiten) // Neue Einladungen/Anfragen melden (auch auf anderen Seiten)
for (const i of lobby.invites) notice("l" + i.lobbyId, `${i.owner} lädt dich in die Lobby ein`); for (const i of lobby.invites) notice("l" + i.lobbyId, t("{0} lädt dich in die Lobby ein", i.owner));
for (const r of friends.incoming) notice("f" + r.id, `${r.username} möchte dein Freund sein`); for (const r of friends.incoming) notice("f" + r.id, t("{0} möchte dein Freund sein", r.username));
} }
function notice(key, text) { function notice(key, text) {
@@ -277,7 +290,7 @@ setInterval(async () => {
}, 3000); }, 3000);
async function renderFriends(root) { async function renderFriends(root) {
root.replaceChildren(h("h1", {}, "Freunde & Lobby"), h("div", { class: "social", id: "socialRoot" }, h("p", { class: "muted" }, "Laden …"))); root.replaceChildren(h("h1", {}, t("Freunde & Lobby")), h("div", { class: "social", id: "socialRoot" }, h("p", { class: "muted" }, t("Laden …"))));
try { await loadSocial(); } try { await loadSocial(); }
catch (e) { $("socialRoot").replaceChildren(h("p", { class: "error" }, e.message)); return; } catch (e) { $("socialRoot").replaceChildren(h("p", { class: "error" }, e.message)); return; }
if (page === "friends") drawSocial(); if (page === "friends") drawSocial();
@@ -305,48 +318,48 @@ function lobbyCard() {
const { lobby, invites } = social.lobby; const { lobby, invites } = social.lobby;
const card = h("div", { class: "card" }); const card = h("div", { class: "card" });
for (const i of invites) for (const i of invites)
card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), " lädt dich in die Lobby ein"), card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), t(" lädt dich in die Lobby ein")),
action("Beitreten", "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action("Ablehnen", "", "lobbyDecline", { lobbyId: i.lobbyId }))); action(t("Beitreten"), "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action(t("Ablehnen"), "", "lobbyDecline", { lobbyId: i.lobbyId })));
if (!lobby) { if (!lobby) {
card.append(h("h3", {}, "Lobby"), card.append(h("h3", {}, t("Lobby")),
h("p", { class: "muted small" }, "Erstelle eine Lobby und lade Freunde ein. Der Server läuft auf dem BSO-Relay – niemand muss etwas hosten oder Ports freigeben."), h("p", { class: "muted small" }, t("Erstelle eine Lobby und lade Freunde ein. Der Server läuft auf dem BSO-Relay – niemand muss etwas hosten oder Ports freigeben.")),
h("div", { class: "row" }, action("Lobby erstellen", "primary", "lobbyCreate"))); h("div", { class: "row" }, action(t("Lobby erstellen"), "primary", "lobbyCreate")));
return card; return card;
} }
const play = h("button", { class: "btn primary play", type: "button" }, "SPIELEN"); const play = h("button", { class: "btn primary play", type: "button" }, t("SPIELEN"));
play.disabled = !lobby.serverOnline; play.disabled = !lobby.serverOnline;
play.addEventListener("click", () => busy(play, async () => { await call("playLobby"); toast("Bus-Simulator 2012 startet und verbindet mit der Lobby …"); })); play.addEventListener("click", () => busy(play, async () => { await call("playLobby"); toast(t("Bus-Simulator 2012 startet und verbindet mit der Lobby …")); }));
const state = h("div", { class: "server-state" }, const state = h("div", { class: "server-state" },
h("span", { class: "dot " + (lobby.serverOnline ? "ok" : "bad") }), h("span", { class: "dot " + (lobby.serverOnline ? "ok" : "bad") }),
h("span", { style: "flex:1" }, lobby.serverOnline ? "Server läuft" : lobby.isOwner ? "Server aus – starte ihn, dann können alle spielen" : `Server aus – warte, bis ${lobby.owner} ihn startet`), h("span", { style: "flex:1" }, lobby.serverOnline ? t("Server läuft") : lobby.isOwner ? t("Server aus – starte ihn, dann können alle spielen") : t("Server aus – warte, bis {0} ihn startet", lobby.owner)),
lobby.isOwner ? (lobby.serverOnline ? action("Server stoppen", "", "lobbyStop") : action("Server starten", "primary", "lobbyStart")) : null); lobby.isOwner ? (lobby.serverOnline ? action(t("Server stoppen"), "", "lobbyStop") : action(t("Server starten"), "primary", "lobbyStart")) : null);
const members = h("div", { class: "people" }, ...lobby.members.map((m) => h("div", { class: "person" }, const members = h("div", { class: "people" }, ...lobby.members.map((m) => h("div", { class: "person" },
h("span", { class: "dot " + (m.online ? "ok" : "") }), h("span", { class: "dot " + (m.online ? "ok" : "") }),
h("span", { class: "name" }, m.username), h("span", { class: "name" }, m.username),
m.isOwner ? h("span", { class: "crown", title: "Ersteller" }, "★") : null, m.isOwner ? h("span", { class: "crown", title: t("Ersteller") }, "★") : null,
lobby.isOwner && !m.isOwner ? action("Entfernen", "ghost", "lobbyKick", { accountId: m.accountId }) : null))); lobby.isOwner && !m.isOwner ? action(t("Entfernen"), "ghost", "lobbyKick", { accountId: m.accountId }) : null)));
card.append(h("h3", {}, lobby.isOwner ? "Deine Lobby" : `Lobby von ${lobby.owner}`), state, card.append(h("h3", {}, lobby.isOwner ? t("Deine Lobby") : t("Lobby von {0}", lobby.owner)), state,
h("div", { class: "section-title" }, `Mitglieder (${lobby.members.length})`), members, h("div", { class: "section-title" }, t("Mitglieder ({0})", lobby.members.length)), members,
lobby.invited.length ? h("p", { class: "muted small" }, `${lobby.invited.length} Einladung(en) offen`) : null, lobby.invited.length ? h("p", { class: "muted small" }, t("{0} Einladung(en) offen", lobby.invited.length)) : null,
h("div", { class: "row", style: "justify-content:space-between;margin-top:6px" }, play, h("div", { class: "row", style: "justify-content:space-between;margin-top:6px" }, play,
action(lobby.isOwner ? "Lobby auflösen" : "Lobby verlassen", "danger", "lobbyLeave"))); action(lobby.isOwner ? t("Lobby auflösen") : t("Lobby verlassen"), "danger", "lobbyLeave")));
return card; return card;
} }
function friendsCard() { function friendsCard() {
const { friends, incoming, outgoing } = social.friends; const { friends, incoming, outgoing } = social.friends;
const lobby = social.lobby.lobby; const lobby = social.lobby.lobby;
const name = h("input", { placeholder: "Benutzername", maxlength: "32", spellcheck: "false" }); const name = h("input", { placeholder: t("Benutzername"), maxlength: "32", spellcheck: "false" });
const add = h("button", { class: "btn small primary", type: "button" }, "Hinzufügen"); const add = h("button", { class: "btn small primary", type: "button" }, t("Hinzufügen"));
const send = () => busy(add, async () => { const send = () => busy(add, async () => {
if (!name.value.trim()) return; if (!name.value.trim()) return;
await call("friendRequest", { username: name.value.trim() }); await call("friendRequest", { username: name.value.trim() });
toast("Anfrage an " + name.value.trim() + " geschickt"); toast(t("Anfrage an {0} geschickt", name.value.trim()));
name.value = ""; name.value = "";
await loadSocial(); await loadSocial();
drawSocial(); drawSocial();
@@ -354,41 +367,44 @@ function friendsCard() {
add.addEventListener("click", send); add.addEventListener("click", send);
name.addEventListener("keydown", (e) => { if (e.key === "Enter") send(); }); name.addEventListener("keydown", (e) => { if (e.key === "Enter") send(); });
const card = h("div", { class: "card" }, h("h3", {}, "Freunde"), h("div", { class: "row" }, name, add)); const card = h("div", { class: "card" }, h("h3", {}, t("Freunde")), h("div", { class: "row" }, name, add));
if (incoming.length) if (incoming.length)
card.append(h("div", { class: "section-title" }, "Anfragen"), h("div", { class: "people" }, ...incoming.map((r) => h("div", { class: "person" }, card.append(h("div", { class: "section-title" }, t("Anfragen")), h("div", { class: "people" }, ...incoming.map((r) => h("div", { class: "person" },
h("span", { class: "name" }, r.username), action("Annehmen", "primary", "friendAccept", { id: r.id }), action("Ablehnen", "", "friendRemove", { id: r.id }))))); h("span", { class: "name" }, r.username), action(t("Annehmen"), "primary", "friendAccept", { id: r.id }), action(t("Ablehnen"), "", "friendRemove", { id: r.id })))));
card.append(h("div", { class: "section-title" }, `Freunde (${friends.filter((f) => f.online).length} online)`)); card.append(h("div", { class: "section-title" }, t("Freunde ({0} online)", friends.filter((f) => f.online).length)));
if (!friends.length) card.append(h("p", { class: "muted small" }, "Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu.")); if (!friends.length) card.append(h("p", { class: "muted small" }, t("Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu.")));
card.append(h("div", { class: "people" }, ...friends.map((f) => { card.append(h("div", { class: "people" }, ...friends.map((f) => {
const inLobby = lobby && lobby.members.some((m) => m.accountId === f.accountId); const inLobby = lobby && lobby.members.some((m) => m.accountId === f.accountId);
const invited = lobby && lobby.invited.includes(f.accountId); const invited = lobby && lobby.invited.includes(f.accountId);
const remove = h("button", { class: "x", type: "button", title: "Freund entfernen" }, ""); const remove = h("button", { class: "x", type: "button", title: t("Freund entfernen") }, "");
remove.addEventListener("click", () => { if (confirm(`${f.username} als Freund entfernen?`)) busy(remove, async () => { await call("friendRemove", { id: f.id }); await loadSocial(); drawSocial(); }); }); remove.addEventListener("click", () => { if (confirm(t("{0} als Freund entfernen?", f.username))) busy(remove, async () => { await call("friendRemove", { id: f.id }); await loadSocial(); drawSocial(); }); });
return h("div", { class: "person" }, return h("div", { class: "person" },
h("span", { class: "dot " + (f.online ? "ok" : "") }), h("span", { class: "name" }, f.username), h("span", { class: "dot " + (f.online ? "ok" : "") }), h("span", { class: "name" }, f.username),
lobby && lobby.isOwner && !inLobby ? (invited ? h("span", { class: "muted small" }, "eingeladen") : action("Einladen", "", "lobbyInvite", { accountId: f.accountId })) : null, lobby && lobby.isOwner && !inLobby ? (invited ? h("span", { class: "muted small" }, t("eingeladen")) : action(t("Einladen"), "", "lobbyInvite", { accountId: f.accountId })) : null,
inLobby ? h("span", { class: "muted small" }, "in der Lobby") : null, inLobby ? h("span", { class: "muted small" }, t("in der Lobby")) : null,
remove); remove);
}))); })));
if (outgoing.length) if (outgoing.length)
card.append(h("div", { class: "section-title" }, "Gesendet"), h("div", { class: "people" }, ...outgoing.map((r) => h("div", { class: "person" }, card.append(h("div", { class: "section-title" }, t("Gesendet")), h("div", { class: "people" }, ...outgoing.map((r) => h("div", { class: "person" },
h("span", { class: "name" }, r.username), h("span", { class: "muted small" }, "wartet …"), action("Zurückziehen", "ghost", "friendRemove", { id: r.id }))))); h("span", { class: "name" }, r.username), h("span", { class: "muted small" }, t("wartet …")), action(t("Zurückziehen"), "ghost", "friendRemove", { id: r.id })))));
return card; return card;
} }
// ---------------------------------------------------------------- Neuigkeiten, Einstellungen
async function renderNews(root) { async function renderNews(root) {
let list; let list;
try { list = await call("releases"); } try { list = await call("releases"); }
catch (e) { root.replaceChildren(h("h1", {}, "Neuigkeiten"), h("p", { class: "error" }, e.message)); return; } catch (e) { root.replaceChildren(h("h1", {}, t("Neuigkeiten")), h("p", { class: "error" }, e.message)); return; }
if (page !== "news") return; if (page !== "news") return;
const names = { mod: "Mod", launcher: "Launcher", server: "Server" }; const names = { mod: "Mod", launcher: "Launcher", server: "Server" };
root.replaceChildren(h("h1", {}, "Neuigkeiten"), list = list.filter((r) => r.product !== "server"); // Server laufen jetzt im BSO-Relay
root.replaceChildren(h("h1", {}, t("Neuigkeiten")),
...(list.length ? list.map((r) => h("article", { class: "release" }, ...(list.length ? list.map((r) => h("article", { class: "release" },
h("header", {}, h("span", { class: "tag" }, names[r.product] || r.product), h("h2", {}, r.version), h("span", { class: "muted small" }, date(r.date))), h("header", {}, h("span", { class: "tag" }, names[r.product] || r.product), h("h2", {}, r.version), h("span", { class: "muted small" }, date(r.date))),
h("pre", {}, r.changelog || "Keine Beschreibung"))) : [h("p", { class: "muted" }, "Noch keine Versionen veroeffentlicht.")])); h("pre", {}, r.changelog || t("Keine Beschreibung")))) : [h("p", { class: "muted" }, t("Noch keine Versionen veroeffentlicht."))]));
} }
async function renderSettings(root) { async function renderSettings(root) {
@@ -397,34 +413,41 @@ async function renderSettings(root) {
const toggle = (label, hint, key, value) => { const toggle = (label, hint, key, value) => {
const box = h("input", { type: "checkbox" }); const box = h("input", { type: "checkbox" });
box.checked = value; box.checked = value;
box.addEventListener("change", () => call("setSettings", { [key]: box.checked }).then(() => toast("Gespeichert"))); box.addEventListener("change", () => call("setSettings", { [key]: box.checked }).then(() => toast(t("Gespeichert"))));
return h("label", { class: "switch" }, h("span", {}, label, h("small", {}, hint)), box); return h("label", { class: "switch" }, h("span", {}, label, h("small", {}, hint)), box);
}; };
const folder = h("button", { class: "btn small", type: "button" }, "Spielordner waehlen"); const folder = h("button", { class: "btn small", type: "button" }, t("Spielordner waehlen"));
folder.addEventListener("click", () => busy(folder, async () => { if ((await call("chooseGameFolder")).changed) { toast("Spielordner gespeichert"); showPage("settings"); } })); folder.addEventListener("click", () => busy(folder, async () => { if ((await call("chooseGameFolder")).changed) { toast(t("Spielordner gespeichert")); showPage("settings"); } }));
const log = h("button", { class: "btn small", type: "button" }, "Protokoll-Ordner oeffnen"); const log = h("button", { class: "btn small", type: "button" }, t("Protokoll-Ordner oeffnen"));
log.addEventListener("click", () => call("openLog")); log.addEventListener("click", () => call("openLog"));
const uninstall = h("button", { class: "btn small danger", type: "button" }, "BSO deinstallieren"); const uninstall = h("button", { class: "btn small danger", type: "button" }, t("BSO deinstallieren"));
uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall"))); uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall")));
const langBox = h("div", { class: "lang-switch", "data-lang-switch": true });
root.replaceChildren(h("h1", {}, "Einstellungen"), h("div", { class: "settings" }, root.replaceChildren(h("h1", {}, t("Einstellungen")), h("div", { class: "settings" },
h("div", { class: "card" }, h("div", { class: "card" },
toggle("BSO automatisch aktualisieren", "Vor jedem Start die neueste Mod-Version installieren", "autoUpdateMod", s?.settings.autoUpdateMod ?? true), toggle(t("BSO automatisch aktualisieren"), t("Vor jedem Start die neueste Mod-Version installieren"), "autoUpdateMod", s?.settings.autoUpdateMod ?? true),
toggle("Launcher offen lassen", "Sonst schliesst er sich nach dem Spielstart", "keepOpen", s?.settings.keepOpen ?? false)), toggle(t("Launcher offen lassen"), t("Sonst schliesst er sich nach dem Spielstart"), "keepOpen", s?.settings.keepOpen ?? false),
h("div", { class: "switch" }, h("span", {}, t("Sprache")), langBox)),
h("div", { class: "card" }, h("div", { class: "card" },
h("h3", {}, "Spiel"), h("h3", {}, t("Spiel")),
h("span", { class: "mono muted" }, s?.game.folder || "Nicht gefunden"), h("span", { class: "mono muted" }, s?.game.folder || t("Nicht gefunden")),
h("div", { class: "row" }, folder)), h("div", { class: "row" }, folder)),
h("div", { class: "card" }, h("div", { class: "card" },
h("h3", {}, "Launcher"), h("h3", {}, "Launcher"),
h("span", { class: "muted small" }, `Version ${init.version} · Server ${init.master}`), h("span", { class: "muted small" }, t("Version {0} · Server {1}", init.version, init.master)),
h("div", { class: "row" }, log, uninstall)))); h("div", { class: "row" }, log, uninstall))));
renderLangSwitch(setLang);
} }
// ---------------------------------------------------------------- Start // ---------------------------------------------------------------- Start
(async () => { (async () => {
const init = await call("init"); const init = await call("init");
lang = init.language === "en" ? "en" : "de";
translateStatic();
renderLangSwitch(setLang);
setAuthMode(false);
$("meVersion").textContent = "Launcher " + init.version; $("meVersion").textContent = "Launcher " + init.version;
if (init.dev) { if (init.dev) {
// -dev: gegen die lokale Test-Website - gut sichtbar, damit niemand das verwechselt // -dev: gegen die lokale Test-Website - gut sichtbar, damit niemand das verwechselt
@@ -433,9 +456,9 @@ async function renderSettings(root) {
$("devBadge").classList.remove("hidden"); $("devBadge").classList.remove("hidden");
$("meVersion").textContent += " · DEV"; $("meVersion").textContent += " · DEV";
} }
$("authServer").textContent = "Server: " + init.master.replace(/^https?:\/\//, ""); $("authServer").textContent = t("Server: {0}", init.master.replace(/^https?:\/\//, ""));
if (!init.loggedIn) { showAuth(); return; } if (!init.loggedIn) { showAuth(); return; }
const me = await call("me"); const me = await call("me");
if (me.loggedIn) showApp(me.username); if (me.loggedIn) showApp(me.username);
else showAuth(); else showAuth();
})(); })();
+89
View File
@@ -0,0 +1,89 @@
"use strict";
// Sprachen der Oberflaeche. Deutsch ist der Text im Code selbst, hier steht nur Englisch dazu.
// t("Text mit {0}", wert) - fehlt eine Uebersetzung, bleibt der deutsche Text stehen.
const EN = {
// Fenster, Anmeldung
"Minimieren": "Minimize", "Maximieren": "Maximize", "Schliessen": "Close",
"Anmelden": "Sign in", "Konto erstellen": "Create account", "Benutzername": "Username", "E-Mail (optional)": "Email (optional)",
"Passwort": "Password", "Passwort wiederholen": "Repeat password", "Abmelden": "Sign out",
"Dein Benutzername ist auch dein Name im Spiel. Passwoerter liegen nur verschluesselt auf dem BSO-Server.":
"Your username is also your name in the game. Passwords are only stored encrypted on the BSO server.",
"Die Passwoerter sind nicht gleich": "The passwords don't match",
"Willkommen bei BSO, {0}!": "Welcome to BSO, {0}!",
"Server: {0}": "Server: {0}",
// Navigation
"Spielen": "Play", "Freunde": "Friends", "Neuigkeiten": "News", "Einstellungen": "Settings", "Laden …": "Loading …",
// Spielen
"SPIELEN": "PLAY", "LAEUFT": "RUNNING", "Bus-Simulator 2012 startet …": "Bus-Simulator 2012 is starting …",
"BSO {0} installiert · Server {1}": "BSO {0} installed · Server {1}", "BSO wird beim ersten Start installiert": "BSO will be installed on first start",
"Multiplayer fuer Bus-Simulator 2012": "Multiplayer for Bus-Simulator 2012",
"Vor dem Start fehlt noch etwas - siehe unten": "Something is still missing - see below",
"Jetzt aktualisieren": "Update now", "Launcher startet neu …": "Launcher is restarting …", "Launcher {0}": "Launcher {0}", " ist da.": " is available.",
"BSO-Server nicht erreichbar - Updates werden spaeter geprueft.": "BSO server not reachable - updates will be checked later.",
"Anderen Ordner waehlen": "Choose another folder", "Ordner waehlen": "Choose folder", "Gefunden": "Found", "Nicht gefunden": "Not found",
"Ueber Steam installiert? Sonst den Ordner von Hand waehlen.": "Installed via Steam? Otherwise choose the folder manually.",
"Version {0}": "Version {0}", "Nicht installiert": "Not installed", "Neu: {0}": "New: {0}", "Aktuell": "Up to date",
"Keine Version auf dem Server": "No version on the server", "Mod herunterladen": "Download mod", "Mod aktualisieren": "Update mod",
"Neu herunterladen": "Download again", "BSO {0} installiert": "BSO {0} installed",
"Installiert": "Installed", "Die Mod laeuft mit .NET 10 (x64).": "The mod runs on .NET 10 (x64).", "Herunterladen": "Download", "Fehlt": "Missing",
"BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren.": "BSO needs the .NET 10 Runtime (x64) - install \".NET Runtime\" for Windows x64.",
"Mitspielen": "Play together", "Adresse": "Address", "Gespeichert": "Saved", "Beim Start verbinden": "Connect on start", "sonst im Spiel mit F8": "otherwise in game with F8",
// Freunde & Lobby
"Freunde & Lobby": "Friends & lobby", "{0} lädt dich in die Lobby ein": "{0} invites you to their lobby", " lädt dich in die Lobby ein": " invites you to their lobby",
"{0} möchte dein Freund sein": "{0} wants to be your friend", "Beitreten": "Join", "Ablehnen": "Decline", "Lobby": "Lobby",
"Erstelle eine Lobby und lade Freunde ein. Der Server läuft auf dem BSO-Relay – niemand muss etwas hosten oder Ports freigeben.":
"Create a lobby and invite friends. The server runs on the BSO relay – nobody has to host anything or forward ports.",
"Lobby erstellen": "Create lobby", "Bus-Simulator 2012 startet und verbindet mit der Lobby …": "Bus-Simulator 2012 is starting and connecting to the lobby …",
"Server läuft": "Server is running", "Server aus – starte ihn, dann können alle spielen": "Server is off – start it so everyone can play",
"Server aus – warte, bis {0} ihn startet": "Server is off – wait until {0} starts it", "Server stoppen": "Stop server", "Server starten": "Start server",
"Ersteller": "Owner", "Entfernen": "Remove", "Deine Lobby": "Your lobby", "Lobby von {0}": "{0}'s lobby", "Mitglieder ({0})": "Members ({0})",
"{0} Einladung(en) offen": "{0} invitation(s) pending", "Lobby auflösen": "Close lobby", "Lobby verlassen": "Leave lobby",
"Hinzufügen": "Add", "Anfrage an {0} geschickt": "Request sent to {0}", "Anfragen": "Requests", "Annehmen": "Accept",
"Freunde ({0} online)": "Friends ({0} online)", "Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu.": "No friends yet – add someone by their BSO username.",
"Freund entfernen": "Remove friend", "{0} als Freund entfernen?": "Remove {0} as a friend?", "eingeladen": "invited", "Einladen": "Invite",
"in der Lobby": "in the lobby", "Gesendet": "Sent", "wartet …": "pending …", "Zurückziehen": "Withdraw",
// Neuigkeiten
"Keine Beschreibung": "No description", "Noch keine Versionen veroeffentlicht.": "No versions published yet.",
// Einstellungen
"Spielordner waehlen": "Choose game folder", "Spielordner gespeichert": "Game folder saved", "Protokoll-Ordner oeffnen": "Open log folder",
"BSO deinstallieren": "Uninstall BSO", "BSO automatisch aktualisieren": "Update BSO automatically",
"Vor jedem Start die neueste Mod-Version installieren": "Install the newest mod version before every start",
"Launcher offen lassen": "Keep launcher open", "Sonst schliesst er sich nach dem Spielstart": "Otherwise it closes after the game starts",
"Spiel": "Game", "Version {0} · Server {1}": "Version {0} · Server {1}", "Sprache": "Language"
};
let lang = "de";
function t(de, ...args) {
let s = lang === "en" && EN[de] != null ? EN[de] : de;
args.forEach((a, i) => { s = s.split("{" + i + "}").join(String(a)); });
return s;
}
/** Feste Texte in index.html: data-t (Text), data-t-title (Tooltip), data-t-placeholder - Original bleibt in data-de. */
function translateStatic() {
document.querySelectorAll("[data-t]").forEach((el) => {
if (el.dataset.de == null) el.dataset.de = el.textContent;
el.textContent = t(el.dataset.de);
});
document.querySelectorAll("[data-t-title]").forEach((el) => {
el.title = t(el.dataset.tTitle);
el.setAttribute("aria-label", el.title);
});
document.documentElement.lang = lang;
}
/** Umschalter DE | EN (in jedes Element mit data-lang-switch). */
function renderLangSwitch(onChange) {
document.querySelectorAll("[data-lang-switch]").forEach((box) => {
box.replaceChildren(...["de", "en"].map((l) => {
const b = document.createElement("button");
b.type = "button";
b.textContent = l.toUpperCase();
b.className = l === lang ? "on" : "";
b.addEventListener("click", () => onChange(l));
return b;
}));
});
}
+20 -17
View File
@@ -5,20 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>BSO Launcher</title> <title>BSO Launcher</title>
<link rel="stylesheet" href="app.css"> <link rel="stylesheet" href="app.css">
<script src="i18n.js" defer></script>
<script src="app.js" defer></script> <script src="app.js" defer></script>
</head> </head>
<body> <body>
<!-- Eigene Titelleiste (das Fenster hat keine von Windows) --> <!-- Eigene Titelleiste (das Fenster hat keine von Windows). data-t = wird uebersetzt (i18n.js) -->
<header class="titlebar"> <header class="titlebar">
<div class="titlebar-drag" id="titlebarDrag"> <div class="titlebar-drag" id="titlebarDrag">
<img src="logo.png" alt=""> <img src="logo.png" alt="">
<span>BSO Launcher</span> <span>BSO Launcher</span>
<span class="dev-badge hidden" id="devBadge" title="Mit -dev gestartet"></span> <span class="dev-badge hidden" id="devBadge"></span>
</div> </div>
<div class="window-buttons"> <div class="window-buttons">
<button type="button" data-window="minimize" title="Minimieren" aria-label="Minimieren">&#xE921;</button> <button type="button" data-window="minimize" data-t-title="Minimieren">&#xE921;</button>
<button type="button" data-window="maximize" id="maximizeButton" title="Maximieren" aria-label="Maximieren">&#xE922;</button> <button type="button" data-window="maximize" id="maximizeButton" data-t-title="Maximieren">&#xE922;</button>
<button type="button" data-window="close" class="close" title="Schliessen" aria-label="Schliessen">&#xE8BB;</button> <button type="button" data-window="close" class="close" data-t-title="Schliessen">&#xE8BB;</button>
</div> </div>
</header> </header>
@@ -33,26 +34,27 @@
</div> </div>
</div> </div>
<div class="tabs"> <div class="tabs">
<button type="button" class="tab on" id="tabLogin">Anmelden</button> <button type="button" class="tab on" id="tabLogin" data-t>Anmelden</button>
<button type="button" class="tab" id="tabRegister">Konto erstellen</button> <button type="button" class="tab" id="tabRegister" data-t>Konto erstellen</button>
</div> </div>
<label><span>Benutzername</span> <label><span data-t>Benutzername</span>
<input id="username" autocomplete="username" maxlength="32" required> <input id="username" autocomplete="username" maxlength="32" required>
</label> </label>
<label id="emailRow" class="hidden"><span>E-Mail (optional)</span> <label id="emailRow" class="hidden"><span data-t>E-Mail (optional)</span>
<input id="email" type="email" autocomplete="email" maxlength="190"> <input id="email" type="email" autocomplete="email" maxlength="190">
</label> </label>
<label><span>Passwort</span> <label><span data-t>Passwort</span>
<input id="password" type="password" autocomplete="current-password" maxlength="200" required> <input id="password" type="password" autocomplete="current-password" maxlength="200" required>
</label> </label>
<label id="password2Row" class="hidden"><span>Passwort wiederholen</span> <label id="password2Row" class="hidden"><span data-t>Passwort wiederholen</span>
<input id="password2" type="password" autocomplete="new-password" maxlength="200"> <input id="password2" type="password" autocomplete="new-password" maxlength="200">
</label> </label>
<p id="authError" class="error hidden"></p> <p id="authError" class="error hidden"></p>
<button class="btn primary wide" id="authButton" type="submit">Anmelden</button> <button class="btn primary wide" id="authButton" type="submit">Anmelden</button>
<p class="muted small center">Dein Benutzername ist auch dein Name im Spiel. Passwoerter liegen nur verschluesselt auf dem BSO-Server.</p> <p class="muted small center" data-t>Dein Benutzername ist auch dein Name im Spiel. Passwoerter liegen nur verschluesselt auf dem BSO-Server.</p>
<p class="muted small center" id="authServer"></p> <p class="muted small center" id="authServer"></p>
</form> </form>
<div class="auth-lang lang-switch" data-lang-switch></div>
</main> </main>
<!-- Launcher --> <!-- Launcher -->
@@ -66,16 +68,17 @@
</div> </div>
</div> </div>
<nav id="nav"> <nav id="nav">
<button type="button" data-page="home" class="on"><span class="nav-icon">&#xE768;</span>Spielen</button> <button type="button" data-page="home" class="on"><span class="nav-icon">&#xE768;</span><span data-t>Spielen</span></button>
<button type="button" data-page="friends"><span class="nav-icon">&#xE716;</span>Freunde<span class="nav-badge hidden" id="friendsBadge"></span></button> <button type="button" data-page="friends"><span class="nav-icon">&#xE716;</span><span data-t>Freunde</span><span class="nav-badge hidden" id="friendsBadge"></span></button>
<button type="button" data-page="news"><span class="nav-icon">&#xE789;</span>Neuigkeiten</button> <button type="button" data-page="news"><span class="nav-icon">&#xE789;</span><span data-t>Neuigkeiten</span></button>
<button type="button" data-page="settings"><span class="nav-icon">&#xE713;</span>Einstellungen</button> <button type="button" data-page="settings"><span class="nav-icon">&#xE713;</span><span data-t>Einstellungen</span></button>
</nav> </nav>
<div class="sidebar-foot"> <div class="sidebar-foot">
<div class="lang-switch" data-lang-switch></div>
<div class="me-card"> <div class="me-card">
<span class="avatar" id="meAvatar">?</span> <span class="avatar" id="meAvatar">?</span>
<span class="me-text"><b id="meName"></b><small id="meVersion"></small></span> <span class="me-text"><b id="meName"></b><small id="meVersion"></small></span>
<button class="icon-btn" id="logout" type="button" title="Abmelden" aria-label="Abmelden">&#xF3B1;</button> <button class="icon-btn" id="logout" type="button" data-t-title="Abmelden">&#xF3B1;</button>
</div> </div>
</div> </div>
</aside> </aside>