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>
<OutputType>WinExe</OutputType>
@@ -8,7 +8,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>BSO.Launcher</RootNamespace>
<AssemblyName>BSO-Launcher</AssemblyName>
<Version>0.3.0</Version>
<Version>0.3.1</Version>
<Company>MBM.wtf</Company>
<Product>BSO Launcher</Product>
<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
`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
- 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
+7 -4
View File
@@ -32,6 +32,7 @@ public sealed class MainForm : Form
ClientSize = new Size(1120, 700);
MinimumSize = new Size(900, 600);
StartPosition = FormStartPosition.CenterScreen;
Lang.Set(settings.Language);
LauncherLog.Info("BSO Launcher " + SelfUpdate.CurrentVersion + " gestartet" + (Api.Dev ? " (-dev)" : "") + ", Website " + Api.Base);
if (Api.Base != Api.BaseUrl) Text += " (DEV)";
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
@@ -120,7 +121,7 @@ public sealed class MainForm : Form
}
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);
if (answer == DialogResult.Yes) OpenUrl("https://developer.microsoft.com/microsoft-edge/webview2/");
Close();
@@ -189,7 +190,7 @@ public sealed class MainForm : Form
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)
{
@@ -220,7 +221,7 @@ public sealed class MainForm : Form
catch (Exception e)
{
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,
loggedIn = settings.Token != null,
master = Api.Base,
language = Lang.Current,
dev = Api.Dev || Api.Base != Api.BaseUrl
};
@@ -328,11 +330,12 @@ public sealed class MainForm : Form
settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect;
settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen;
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();
return null;
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 };
string folder = dialog.SelectedPath;
// 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)
{
ApplicationConfiguration.Initialize();
Lang.Set(Settings.Load().Language);
// Deinstallieren (aus "Apps & Features")
if (args.Contains("--uninstall"))
@@ -33,7 +34,7 @@ static class Program
}
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;
}
SelfUpdate.CleanupOldVersion();
+12 -12
View File
@@ -45,7 +45,7 @@ public static class Installer
/// <summary>Installieren. progress(0..1, Text).</summary>
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"))
{
try
@@ -58,19 +58,19 @@ public static class Installer
catch { }
}
progress(0.3, "Dateien kopieren …");
progress(0.3, Lang.T("Dateien kopieren …"));
Directory.CreateDirectory(InstallDir);
string tmp = InstalledExe + ".new";
File.Copy(Self, tmp, true);
File.Move(tmp, InstalledExe, true);
progress(0.6, "Verknuepfungen anlegen …");
progress(0.6, Lang.T("Verknuepfungen anlegen …"));
CreateShortcut(StartMenuShortcut);
CreateShortcut(DesktopShortcut);
progress(0.8, "Registrieren …");
progress(0.8, Lang.T("Registrieren …"));
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>
@@ -141,13 +141,13 @@ public static class Installer
if (!quiet)
{
var answer = MessageBox.Show(
"BSO komplett deinstallieren?\n\nDas entfernt den Launcher, BSO aus dem Bus-Simulator-2012-Ordner (dinput8.dll, BSO-Ordner) " +
"und deine Anmeldung im Launcher.",
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."),
AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
if (answer != DialogResult.Yes) return false;
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;
}
}
@@ -162,9 +162,9 @@ public static class Installer
// Die laufende .exe laesst sich nicht selbst loeschen: aufraeumen, sobald dieser Prozess beendet ist
RemoveAfterExit(dir, Settings.Folder);
if (!quiet)
MessageBox.Show(problems.Count == 0
MessageBox.Show(Lang.T(problems.Count == 0
? "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);
return true;
}
@@ -198,7 +198,7 @@ public sealed class InstallForm : Form
status.Font = new Font("Segoe UI", 9.5f);
status.TextAlign = ContentAlignment.MiddleCenter;
status.SetBounds(20, 178, 380, 24);
status.Text = "Vorbereiten …";
status.Text = Lang.T("Vorbereiten …");
Controls.Add(status);
Shown += async (_, _) => await Run();
}
@@ -238,7 +238,7 @@ public sealed class InstallForm : Form
{
error = e.Message;
status.ForeColor = Color.FromArgb(0xff, 0x8a, 0x8d);
status.Text = "Installation fehlgeschlagen: " + e.Message;
status.Text = Lang.T("Installation fehlgeschlagen: " + e.Message);
Invalidate();
await Task.Delay(6000);
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 KeepOpen { get; set; }
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]
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); }
.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 */
.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; }
+104 -81
View File
@@ -1,5 +1,6 @@
"use strict";
// 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
@@ -46,9 +47,9 @@ function h(tag, attrs, ...children) {
}
function toast(text, error) {
const t = h("div", { class: "toast" + (error ? " error" : "") }, text);
$("toasts").append(t);
setTimeout(() => t.remove(), error ? 7000 : 4000);
const el = h("div", { class: "toast" + (error ? " error" : "") }, text);
$("toasts").append(el);
setTimeout(() => el.remove(), error ? 7000 : 4000);
}
/** Knopf waehrend einer Aktion sperren; Fehler als Meldung. */
@@ -59,7 +60,19 @@ async function busy(button, action) {
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
@@ -78,7 +91,7 @@ function setAuthMode(register) {
$("tabRegister").classList.toggle("on", register);
$("emailRow").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";
$("authError").classList.add("hidden");
}
@@ -91,7 +104,7 @@ $("authForm").addEventListener("submit", async (e) => {
const error = $("authError");
error.classList.add("hidden");
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");
return;
}
@@ -101,7 +114,7 @@ $("authForm").addEventListener("submit", async (e) => {
{ username: $("username").value, password: $("password").value, email: $("email").value });
$("password").value = $("password2").value = "";
showApp(r.username);
if (registerMode) toast("Willkommen bei BSO, " + r.username + "!");
if (registerMode) toast(t("Willkommen bei BSO, {0}!", r.username));
} catch (err) {
error.textContent = err.message;
error.classList.remove("hidden");
@@ -139,14 +152,14 @@ function showPage(name) {
page = name;
document.querySelectorAll("#nav [data-page]").forEach((b) => b.classList.toggle("on", b.dataset.page === name));
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);
}
// Anmeldung abgelaufen (z.B. beim Holen des Spiel-Tickets)
on("loggedOut", () => showAuth());
// Fortschritt (Downloads)
// Fortschritt (Downloads) - der Text kommt schon uebersetzt vom Launcher
let progressBox = null;
on("progress", (m) => {
if (!progressBox) return;
@@ -162,31 +175,31 @@ async function renderHome(root) {
if (page !== "home") return;
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.addEventListener("click", () => busy(play, async () => {
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 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 = [
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("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
];
if (s.launcherUpdate) {
const b = h("button", { class: "btn primary small", type: "button" }, "Jetzt aktualisieren");
b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast("Launcher startet neu …"); }));
children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, "Launcher " + s.launcherUpdate), " ist da."), b));
const b = h("button", { class: "btn primary small", type: "button" }, t("Jetzt aktualisieren"));
b.addEventListener("click", () => busy(b, async () => { await call("installLauncherUpdate"); toast(t("Launcher startet neu …")); }));
children.push(h("div", { class: "banner" }, h("span", {}, h("b", {}, t("Launcher {0}", s.launcherUpdate)), t(" ist da.")), b));
}
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)));
root.replaceChildren(...children);
@@ -197,26 +210,26 @@ function card(title, state, value, ...extra) {
}
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"); }));
return card("Bus-Simulator 2012", s.game.found ? "ok" : "bad", s.game.found ? "Gefunden" : "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."),
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" }, t("Ueber Steam installiert? Sonst den Ordner von Hand waehlen.")),
h("div", { class: "row" }, choose));
}
function modCard(s) {
const m = s.mod;
const state = m.installed && !m.updateAvailable ? "ok" : m.latest ? "warn" : "bad";
const value = m.installed ? `Version ${m.installed}` : "Nicht installiert";
const info = m.updateAvailable ? `Neu: ${m.latest}` : m.latest ? "Aktuell" : "Keine Version auf dem Server";
const value = m.installed ? t("Version {0}", m.installed) : t("Nicht installiert");
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)];
// Immer da: holt die neueste Mod von der Website (bei SPIELEN passiert das sonst automatisch, wenn eingeschaltet)
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);
b.addEventListener("click", () => busy(b, async () => {
const r = await call("installMod");
toast("BSO " + r.version + " installiert");
toast(t("BSO {0} installiert", r.version));
showPage("home");
}));
extra.push(h("div", { class: "row" }, b));
@@ -225,11 +238,11 @@ function modCard(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)."));
const b = h("button", { class: "btn small", type: "button" }, "Herunterladen");
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" }, t("Herunterladen"));
b.addEventListener("click", () => call("openDotnet"));
return card(".NET 10 Runtime", "bad", "Fehlt",
h("span", { class: "muted small" }, "BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren."),
return card(".NET 10 Runtime", "bad", t("Fehlt"),
h("span", { class: "muted small" }, t("BSO braucht die .NET 10 Runtime (x64) - \".NET Runtime\" fuer Windows x64 installieren.")),
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 auto = h("input", { type: "checkbox" });
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);
auto.addEventListener("change", save);
return card("Server", "ok", "Mitspielen",
h("label", {}, h("span", {}, "Adresse"), input),
h("label", { class: "switch" }, h("span", {}, "Beim Start verbinden", h("small", {}, "sonst im Spiel mit F8")), auto));
return card("Server", "ok", t("Mitspielen"),
h("label", {}, h("span", {}, t("Adresse")), input),
h("label", { class: "switch" }, h("span", {}, t("Beim Start verbinden"), h("small", {}, t("sonst im Spiel mit F8"))), auto));
}
// ---------------------------------------------------------------- Freunde & Lobby
@@ -258,8 +271,8 @@ async function loadSocial() {
$("friendsBadge").textContent = n;
$("friendsBadge").classList.toggle("hidden", n === 0);
// 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 r of friends.incoming) notice("f" + r.id, `${r.username} möchte dein Freund sein`);
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, t("{0} möchte dein Freund sein", r.username));
}
function notice(key, text) {
@@ -277,7 +290,7 @@ setInterval(async () => {
}, 3000);
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(); }
catch (e) { $("socialRoot").replaceChildren(h("p", { class: "error" }, e.message)); return; }
if (page === "friends") drawSocial();
@@ -305,48 +318,48 @@ function lobbyCard() {
const { lobby, invites } = social.lobby;
const card = h("div", { class: "card" });
for (const i of invites)
card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), " lädt dich in die Lobby ein"),
action("Beitreten", "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action("Ablehnen", "", "lobbyDecline", { lobbyId: i.lobbyId })));
card.append(h("div", { class: "invite" }, h("span", {}, h("b", {}, i.owner), t(" lädt dich in die Lobby ein")),
action(t("Beitreten"), "primary", "lobbyJoin", { lobbyId: i.lobbyId }), action(t("Ablehnen"), "", "lobbyDecline", { lobbyId: i.lobbyId })));
if (!lobby) {
card.append(h("h3", {}, "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("div", { class: "row" }, action("Lobby erstellen", "primary", "lobbyCreate")));
card.append(h("h3", {}, t("Lobby")),
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(t("Lobby erstellen"), "primary", "lobbyCreate")));
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.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" },
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`),
lobby.isOwner ? (lobby.serverOnline ? action("Server stoppen", "", "lobbyStop") : action("Server starten", "primary", "lobbyStart")) : null);
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(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" },
h("span", { class: "dot " + (m.online ? "ok" : "") }),
h("span", { class: "name" }, m.username),
m.isOwner ? h("span", { class: "crown", title: "Ersteller" }, "★") : null,
lobby.isOwner && !m.isOwner ? action("Entfernen", "ghost", "lobbyKick", { accountId: m.accountId }) : null)));
m.isOwner ? h("span", { class: "crown", title: t("Ersteller") }, "★") : 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,
h("div", { class: "section-title" }, `Mitglieder (${lobby.members.length})`), members,
lobby.invited.length ? h("p", { class: "muted small" }, `${lobby.invited.length} Einladung(en) offen`) : null,
card.append(h("h3", {}, lobby.isOwner ? t("Deine Lobby") : t("Lobby von {0}", lobby.owner)), state,
h("div", { class: "section-title" }, t("Mitglieder ({0})", lobby.members.length)), members,
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,
action(lobby.isOwner ? "Lobby auflösen" : "Lobby verlassen", "danger", "lobbyLeave")));
action(lobby.isOwner ? t("Lobby auflösen") : t("Lobby verlassen"), "danger", "lobbyLeave")));
return card;
}
function friendsCard() {
const { friends, incoming, outgoing } = social.friends;
const lobby = social.lobby.lobby;
const name = h("input", { placeholder: "Benutzername", maxlength: "32", spellcheck: "false" });
const add = h("button", { class: "btn small primary", type: "button" }, "Hinzufügen");
const name = h("input", { placeholder: t("Benutzername"), maxlength: "32", spellcheck: "false" });
const add = h("button", { class: "btn small primary", type: "button" }, t("Hinzufügen"));
const send = () => busy(add, async () => {
if (!name.value.trim()) return;
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 = "";
await loadSocial();
drawSocial();
@@ -354,41 +367,44 @@ function friendsCard() {
add.addEventListener("click", 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)
card.append(h("div", { class: "section-title" }, "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 })))));
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(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)`));
if (!friends.length) card.append(h("p", { class: "muted small" }, "Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu."));
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" }, t("Noch keine Freunde – füge jemanden über seinen BSO-Benutzernamen hinzu.")));
card.append(h("div", { class: "people" }, ...friends.map((f) => {
const inLobby = lobby && lobby.members.some((m) => m.accountId === f.accountId);
const invited = lobby && lobby.invited.includes(f.accountId);
const remove = h("button", { class: "x", type: "button", title: "Freund entfernen" }, "");
remove.addEventListener("click", () => { if (confirm(`${f.username} als Freund entfernen?`)) busy(remove, async () => { await call("friendRemove", { id: f.id }); await loadSocial(); drawSocial(); }); });
const remove = h("button", { class: "x", type: "button", title: t("Freund entfernen") }, "");
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" },
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,
inLobby ? h("span", { class: "muted small" }, "in der Lobby") : 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" }, t("in der Lobby")) : null,
remove);
})));
if (outgoing.length)
card.append(h("div", { class: "section-title" }, "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 })))));
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" }, t("wartet …")), action(t("Zurückziehen"), "ghost", "friendRemove", { id: r.id })))));
return card;
}
// ---------------------------------------------------------------- Neuigkeiten, Einstellungen
async function renderNews(root) {
let list;
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;
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" },
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) {
@@ -397,34 +413,41 @@ async function renderSettings(root) {
const toggle = (label, hint, key, value) => {
const box = h("input", { type: "checkbox" });
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);
};
const folder = h("button", { class: "btn small", type: "button" }, "Spielordner waehlen");
folder.addEventListener("click", () => busy(folder, async () => { if ((await call("chooseGameFolder")).changed) { toast("Spielordner gespeichert"); showPage("settings"); } }));
const log = h("button", { class: "btn small", type: "button" }, "Protokoll-Ordner oeffnen");
const folder = h("button", { class: "btn small", type: "button" }, t("Spielordner waehlen"));
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" }, t("Protokoll-Ordner oeffnen"));
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")));
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" },
toggle("BSO automatisch aktualisieren", "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("BSO automatisch aktualisieren"), t("Vor jedem Start die neueste Mod-Version installieren"), "autoUpdateMod", s?.settings.autoUpdateMod ?? true),
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("h3", {}, "Spiel"),
h("span", { class: "mono muted" }, s?.game.folder || "Nicht gefunden"),
h("h3", {}, t("Spiel")),
h("span", { class: "mono muted" }, s?.game.folder || t("Nicht gefunden")),
h("div", { class: "row" }, folder)),
h("div", { class: "card" },
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))));
renderLangSwitch(setLang);
}
// ---------------------------------------------------------------- Start
(async () => {
const init = await call("init");
lang = init.language === "en" ? "en" : "de";
translateStatic();
renderLangSwitch(setLang);
setAuthMode(false);
$("meVersion").textContent = "Launcher " + init.version;
if (init.dev) {
// -dev: gegen die lokale Test-Website - gut sichtbar, damit niemand das verwechselt
@@ -433,9 +456,9 @@ async function renderSettings(root) {
$("devBadge").classList.remove("hidden");
$("meVersion").textContent += " · DEV";
}
$("authServer").textContent = "Server: " + init.master.replace(/^https?:\/\//, "");
$("authServer").textContent = t("Server: {0}", init.master.replace(/^https?:\/\//, ""));
if (!init.loggedIn) { showAuth(); return; }
const me = await call("me");
if (me.loggedIn) showApp(me.username);
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">
<title>BSO Launcher</title>
<link rel="stylesheet" href="app.css">
<script src="i18n.js" defer></script>
<script src="app.js" defer></script>
</head>
<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">
<div class="titlebar-drag" id="titlebarDrag">
<img src="logo.png" alt="">
<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 class="window-buttons">
<button type="button" data-window="minimize" title="Minimieren" aria-label="Minimieren">&#xE921;</button>
<button type="button" data-window="maximize" id="maximizeButton" title="Maximieren" aria-label="Maximieren">&#xE922;</button>
<button type="button" data-window="close" class="close" title="Schliessen" aria-label="Schliessen">&#xE8BB;</button>
<button type="button" data-window="minimize" data-t-title="Minimieren">&#xE921;</button>
<button type="button" data-window="maximize" id="maximizeButton" data-t-title="Maximieren">&#xE922;</button>
<button type="button" data-window="close" class="close" data-t-title="Schliessen">&#xE8BB;</button>
</div>
</header>
@@ -33,26 +34,27 @@
</div>
</div>
<div class="tabs">
<button type="button" class="tab on" id="tabLogin">Anmelden</button>
<button type="button" class="tab" id="tabRegister">Konto erstellen</button>
<button type="button" class="tab on" id="tabLogin" data-t>Anmelden</button>
<button type="button" class="tab" id="tabRegister" data-t>Konto erstellen</button>
</div>
<label><span>Benutzername</span>
<label><span data-t>Benutzername</span>
<input id="username" autocomplete="username" maxlength="32" required>
</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">
</label>
<label><span>Passwort</span>
<label><span data-t>Passwort</span>
<input id="password" type="password" autocomplete="current-password" maxlength="200" required>
</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">
</label>
<p id="authError" class="error hidden"></p>
<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>
</form>
<div class="auth-lang lang-switch" data-lang-switch></div>
</main>
<!-- Launcher -->
@@ -66,16 +68,17 @@
</div>
</div>
<nav id="nav">
<button type="button" data-page="home" class="on"><span class="nav-icon">&#xE768;</span>Spielen</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="news"><span class="nav-icon">&#xE789;</span>Neuigkeiten</button>
<button type="button" data-page="settings"><span class="nav-icon">&#xE713;</span>Einstellungen</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><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><span data-t>Neuigkeiten</span></button>
<button type="button" data-page="settings"><span class="nav-icon">&#xE713;</span><span data-t>Einstellungen</span></button>
</nav>
<div class="sidebar-foot">
<div class="lang-switch" data-lang-switch></div>
<div class="me-card">
<span class="avatar" id="meAvatar">?</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>
</aside>