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:
+2
-2
@@ -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,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
@@ -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
@@ -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
@@ -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 & Features" (bzw. dessen Version nach einem Selbst-Update).</summary>
|
/// <summary>Eintrag unter "Apps & 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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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,7 +456,7 @@ 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);
|
||||||
|
|||||||
+89
@@ -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
@@ -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"></button>
|
<button type="button" data-window="minimize" data-t-title="Minimieren"></button>
|
||||||
<button type="button" data-window="maximize" id="maximizeButton" title="Maximieren" aria-label="Maximieren"></button>
|
<button type="button" data-window="maximize" id="maximizeButton" data-t-title="Maximieren"></button>
|
||||||
<button type="button" data-window="close" class="close" title="Schliessen" aria-label="Schliessen"></button>
|
<button type="button" data-window="close" class="close" data-t-title="Schliessen"></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"></span>Spielen</button>
|
<button type="button" data-page="home" class="on"><span class="nav-icon"></span><span data-t>Spielen</span></button>
|
||||||
<button type="button" data-page="friends"><span class="nav-icon"></span>Freunde<span class="nav-badge hidden" id="friendsBadge"></span></button>
|
<button type="button" data-page="friends"><span class="nav-icon"></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"></span>Neuigkeiten</button>
|
<button type="button" data-page="news"><span class="nav-icon"></span><span data-t>Neuigkeiten</span></button>
|
||||||
<button type="button" data-page="settings"><span class="nav-icon"></span>Einstellungen</button>
|
<button type="button" data-page="settings"><span class="nav-icon"></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"></button>
|
<button class="icon-btn" id="logout" type="button" data-t-title="Abmelden"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
Reference in New Issue
Block a user