Added internationalization support (i18n) (#74)

* - Implemented i18n.
- Updated UI elements to use localized strings for various messages and confirmations.
- Added language selection functionality in settings with appropriate event handling.
- Created English localization file with translations for all new strings.
- Updated backend to save and load user-selected language preferences.

* Add Spanish localization for the GUI

* Add Portuguese (Brazil) localization for the GUI

* update main branch to release/v2.0.2b (#86)

* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b

* Update localization for game repair and GPU settings

Added new localization entries for game repair and GPU preferences.

* Update spanish localization for game repair and GPU settings

* Update portuguese (brazil) for game repair and GPU settings

* Update localization for system logs in English, Spanish, and Portuguese

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
This commit is contained in:
xSamiVS
2026-01-21 14:41:12 +01:00
committed by GitHub
parent 4ac12e0e24
commit 9ef05e8322
17 changed files with 1195 additions and 213 deletions

View File

@@ -33,27 +33,27 @@
<div class="sidebar-nav"> <div class="sidebar-nav">
<div class="nav-item active" data-page="play"> <div class="nav-item active" data-page="play">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span class="nav-tooltip">Play</span> <span class="nav-tooltip" data-i18n="nav.play">Play</span>
</div> </div>
<div class="nav-item" data-page="mods"> <div class="nav-item" data-page="mods">
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
<span class="nav-tooltip">Mods</span> <span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
</div> </div>
<div class="nav-item" data-page="news"> <div class="nav-item" data-page="news">
<i class="fas fa-newspaper"></i> <i class="fas fa-newspaper"></i>
<span class="nav-tooltip">News</span> <span class="nav-tooltip" data-i18n="nav.news">News</span>
</div> </div>
<div class="nav-item" data-page="chat"> <div class="nav-item" data-page="chat">
<i class="fas fa-comments"></i> <i class="fas fa-comments"></i>
<span class="nav-tooltip">Players Chat</span> <span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span>
</div> </div>
<div class="nav-item" data-page="settings"> <div class="nav-item" data-page="settings">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
<span class="nav-tooltip">Settings</span> <span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
</div> </div>
<div class="nav-item" data-page="skins"> <div class="nav-item" data-page="skins">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
<span class="nav-tooltip">Skins</span> <span class="nav-tooltip" data-i18n="nav.skins">Skins</span>
</div> </div>
<div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()"> <div class="nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()">
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
@@ -68,7 +68,7 @@
<header class="header"> <header class="header">
<div id="playersOnlineCounter" class="players-counter"> <div id="playersOnlineCounter" class="players-counter">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<span class="counter-label">Players:</span> <span class="counter-label" data-i18n="header.playersLabel">Players:</span>
<span id="onlineCount" class="counter-value">0</span> <span id="onlineCount" class="counter-value">0</span>
</div> </div>
@@ -85,7 +85,7 @@
<div class="profile-divider"></div> <div class="profile-divider"></div>
<div class="profile-action" onclick="openProfileManager()"> <div class="profile-action" onclick="openProfileManager()">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
<span>Manage Profiles</span> <span data-i18n="header.manageProfiles">Manage Profiles</span>
</div> </div>
</div> </div>
</div> </div>
@@ -105,7 +105,7 @@
HY<span class="title-accent">TALE</span> HY<span class="title-accent">TALE</span>
</h1> </h1>
<div class="game-tags"> <div class="game-tags">
<span class="tag">FREE TO PLAY</span> <span class="tag" data-i18n="header.f2p">FREE TO PLAY</span>
</div> </div>
</div> </div>
@@ -116,27 +116,27 @@
<h1 class="install-title"> <h1 class="install-title">
HYTA<span class="title-accent">LE</span> HYTA<span class="title-accent">LE</span>
</h1> </h1>
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p> <p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
</div> </div>
<div class="install-form"> <div class="install-form">
<div class="form-group"> <div class="form-group">
<label class="form-label">Player Name</label> <label class="form-label" data-i18n="install.playerName">Player Name</label>
<input type="text" id="installPlayerName" placeholder="Enter your name" <input type="text" id="installPlayerName" data-i18n-placeholder="install.playerNamePlaceholder"
class="form-input" value="Player" /> class="form-input" value="Player" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-group"> <label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox"> <input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label">Custom Installation</span> <span class="checkbox-label" data-i18n="install.customInstallation">Custom Installation</span>
</label> </label>
<div id="installCustomOptions" class="custom-options"> <div id="installCustomOptions" class="custom-options">
<div class="form-subgroup"> <div class="form-subgroup">
<label class="form-label">Installation Folder</label> <label class="form-label" data-i18n="install.installationFolder">Installation Folder</label>
<div class="input-with-button"> <div class="input-with-button">
<input type="text" id="installPath" placeholder="Default location" <input type="text" id="installPath" data-i18n-placeholder="install.pathPlaceholder"
class="form-input" readonly /> class="form-input" readonly />
<button onclick="browseInstallPath()" class="browse-btn"> <button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
@@ -148,7 +148,7 @@
<button id="installBtn" class="install-button" onclick="installGame()"> <button id="installBtn" class="install-button" onclick="installGame()">
<i class="fas fa-download mr-2"></i> <i class="fas fa-download mr-2"></i>
<span id="installText">INSTALL HYTALE</span> <span id="installText" data-i18n="install.installButton">INSTALL HYTALE</span>
</button> </button>
</div> </div>
</div> </div>
@@ -161,14 +161,14 @@
<div class="play-header"> <div class="play-header">
<h2 class="play-title"> <h2 class="play-title">
<i class="fas fa-play-circle mr-2"></i> <i class="fas fa-play-circle mr-2"></i>
READY TO PLAY <span data-i18n="play.ready">READY TO PLAY</span>
</h2> </h2>
<p class="play-subtitle">Launch Hytale and enter the adventure</p> <p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the adventure</p>
</div> </div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()"> <button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span>PLAY HYTALE</span> <span data-i18n="play.playButton">PLAY HYTALE</span>
</button> </button>
</div> </div>
</div> </div>
@@ -177,10 +177,10 @@
<div class="news-header"> <div class="news-header">
<h2 class="news-title"> <h2 class="news-title">
<i class="fas fa-star mr-2"></i> <i class="fas fa-star mr-2"></i>
LATEST NEWS <span data-i18n="play.latestNews">LATEST NEWS</span>
</h2> </h2>
<button class="view-all-btn" onclick="navigateToPage('news')"> <button class="view-all-btn" onclick="navigateToPage('news')">
VIEW ALL <i class="fas fa-arrow-right ml-1"></i> <span data-i18n="play.viewAll">VIEW ALL</span> <i class="fas fa-arrow-right ml-1"></i>
</button> </button>
</div> </div>
<div id="newsGrid" class="news-grid-horizontal"></div> <div id="newsGrid" class="news-grid-horizontal"></div>
@@ -191,12 +191,12 @@
<div class="mods-header"> <div class="mods-header">
<div class="mods-search-container"> <div class="mods-search-container">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" /> <input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder" class="mods-search" />
</div> </div>
<div class="mods-actions"> <div class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary"> <button id="myModsBtn" class="mods-btn-primary">
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
MY MODS <span data-i18n="mods.myMods">MY MODS</span>
</button> </button>
</div> </div>
</div> </div>
@@ -207,13 +207,13 @@
<div class="mods-pagination"> <div class="mods-pagination">
<button id="prevPage" class="pagination-btn"> <button id="prevPage" class="pagination-btn">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
PREVIOUS <span data-i18n="mods.previous">PREVIOUS</span>
</button> </button>
<span class="pagination-info"> <span class="pagination-info">
Page <span id="currentPage">1</span> of <span id="totalPages">1</span> <span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span data-i18n="mods.of">of</span> <span id="totalPages">1</span>
</span> </span>
<button id="nextPage" class="pagination-btn"> <button id="nextPage" class="pagination-btn">
NEXT <span data-i18n="mods.next">NEXT</span>
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</button> </button>
</div> </div>
@@ -223,7 +223,7 @@
<div class="news-header"> <div class="news-header">
<h2 class="news-title"> <h2 class="news-title">
<i class="fas fa-newspaper mr-2"></i> <i class="fas fa-newspaper mr-2"></i>
ALL NEWS <span data-i18n="news.title">ALL NEWS</span>
</h2> </h2>
</div> </div>
<div id="allNewsGrid" class="news-grid-full"></div> <div id="allNewsGrid" class="news-grid-full"></div>
@@ -234,16 +234,16 @@
<div class="chat-header"> <div class="chat-header">
<h2 class="chat-title"> <h2 class="chat-title">
<i class="fas fa-comments mr-2"></i> <i class="fas fa-comments mr-2"></i>
PLAYERS CHAT <span data-i18n="chat.title">PLAYERS CHAT</span>
</h2> </h2>
<div class="chat-header-actions"> <div class="chat-header-actions">
<button id="chatColorBtn" class="chat-color-btn"> <button id="chatColorBtn" class="chat-color-btn">
<i class="fas fa-palette"></i> <i class="fas fa-palette"></i>
<span>Color</span> <span data-i18n="chat.pickColor">Color</span>
</button> </button>
<div class="chat-online-badge"> <div class="chat-online-badge">
<i class="fas fa-circle"></i> <i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> online <span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span>
</div> </div>
</div> </div>
</div> </div>
@@ -256,7 +256,7 @@
<div class="chat-footer"> <div class="chat-footer">
<div class="chat-input-container"> <div class="chat-input-container">
<textarea id="chatInput" class="chat-input" <textarea id="chatInput" class="chat-input"
placeholder="Type your message... (Links are automatically censored)" rows="1" data-i18n-placeholder="chat.inputPlaceholder" rows="1"
maxlength="500"></textarea> maxlength="500"></textarea>
<button id="chatSendBtn" class="chat-send-btn"> <button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
@@ -266,7 +266,7 @@
<span class="chat-char-counter" id="chatCharCounter">0/500</span> <span class="chat-char-counter" id="chatCharCounter">0/500</span>
<span class="chat-warning-text"> <span class="chat-warning-text">
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
Secure chat - Links are censored <span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
</span> </span>
</div> </div>
</div> </div>
@@ -278,26 +278,25 @@
<div class="settings-header"> <div class="settings-header">
<h2 class="settings-title"> <h2 class="settings-title">
<i class="fas fa-cog mr-2"></i> <i class="fas fa-cog mr-2"></i>
SETTINGS <span data-i18n="settings.title">SETTINGS</span>
</h2> </h2>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-gamepad"></i> <i class="fas fa-gamepad"></i>
Game Options <span data-i18n="settings.game">Game Options</span>
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Player Name</label> <label class="settings-input-label" data-i18n="settings.playerName">Player Name</label>
<input type="text" id="settingsPlayerName" class="settings-input" <input type="text" id="settingsPlayerName" class="settings-input"
placeholder="Enter your player name" maxlength="16" /> data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
This name will be used in-game (1-16 characters) <span data-i18n="settings.playerNameHint">This name will be used in-game (1-16 characters)</span>
</p> </p>
</div> </div>
</div> </div>
@@ -308,8 +307,8 @@
onclick="openGameLocation()"> onclick="openGameLocation()">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<div class="btn-content"> <div class="btn-content">
<div class="btn-title">Open Game Location</div> <div class="btn-title" data-i18n="settings.openGameLocation">Open Game Location</div>
<div class="btn-description">Open the game installation folder</div> <div class="btn-description" data-i18n="settings.openGameLocationDesc">Open the game installation folder</div>
</div> </div>
</button> </button>
</div> </div>
@@ -321,8 +320,8 @@
onclick="repairGame()"> onclick="repairGame()">
<i class="fas fa-tools"></i> <i class="fas fa-tools"></i>
<div class="btn-content"> <div class="btn-content">
<div class="btn-title">Repair Game</div> <div class="btn-title" data-i18n="settings.repairGame">Repair Game</div>
<div class="btn-description">Reinstall game files (preserves data) <div class="btn-description" data-i18n="settings.reinstallGame">Reinstall game files (preserves data)
</div> </div>
</div> </div>
</button> </button>
@@ -330,21 +329,18 @@
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">GPU Preference</label> <label class="settings-input-label" data-i18n="settings.gpuPreference">GPU Preference</label>
<div class="segmented-control"> <div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" <input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
checked> <label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<label for="gpu-auto">Auto</label> <input type="radio" id="gpu-integrated" name="gpuPreference" value="integrated">
<input type="radio" id="gpu-integrated" name="gpuPreference" <label for="gpu-integrated" data-i18n="settings.gpuIntegrated">Integrated</label>
value="integrated"> <input type="radio" id="gpu-dedicated" name="gpuPreference" value="dedicated">
<label for="gpu-integrated">Integrated</label> <label for="gpu-dedicated" data-i18n="settings.gpuDedicated">Dedicated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated">Dedicated</label>
</div> </div>
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
Select your preferred GPU (Linux: affects DRI_PRIME) <span data-i18n="settings.gpuHint">Select your preferred GPU (Linux: affects DRI_PRIME)</span>
</p> </p>
<div id="gpu-detection-info" class="gpu-detection-info"></div> <div id="gpu-detection-info" class="gpu-detection-info"></div>
</div> </div>
@@ -354,15 +350,15 @@
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i> <i class="fas fa-fingerprint"></i>
Player UUID Management <span data-i18n="settings.account">Player UUID Management</span>
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Current UUID</label> <label class="settings-input-label" data-i18n="settings.currentUUID">Current UUID</label>
<div class="uuid-display-container"> <div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input" <input type="text" id="currentUuid" class="settings-input uuid-input"
readonly placeholder="Loading UUID..." /> readonly data-i18n-placeholder="settings.uuidPlaceholder" />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID"> <button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
</button> </button>
@@ -373,7 +369,7 @@
</div> </div>
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
Your unique player identifier for this username <span data-i18n="settings.uuidHint">Your unique player identifier for this username</span>
</p> </p>
</div> </div>
</div> </div>
@@ -383,9 +379,8 @@
<button id="manageUuidsBtn" class="settings-action-btn"> <button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
<div class="btn-content"> <div class="btn-content">
<div class="btn-title">Manage All UUIDs</div> <div class="btn-title" data-i18n="settings.manageUUIDs">Manage All UUIDs</div>
<div class="btn-description">View and manage all player UUIDs <div class="btn-description" data-i18n="settings.manageUUIDsDesc">View and manage all player UUIDs</div>
</div>
</div> </div>
</button> </button>
</div> </div>
@@ -395,7 +390,7 @@
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fab fa-discord"></i> <i class="fab fa-discord"></i>
Discord Integration <span data-i18n="settings.discord">Discord Integration</span>
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
@@ -403,8 +398,8 @@
<input type="checkbox" id="discordRPCCheck" checked /> <input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span> <span class="checkmark"></span>
<div class="checkbox-content"> <div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div> <div class="checkbox-title" data-i18n="settings.enableRPC">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord <div class="checkbox-description" data-i18n="settings.discordDescription">Show your launcher activity on Discord
</div> </div>
</div> </div>
</label> </label>
@@ -414,7 +409,7 @@
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-coffee"></i> <i class="fas fa-coffee"></i>
Java Runtime <span data-i18n="settings.java">Java Runtime</span>
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
@@ -422,8 +417,8 @@
<input type="checkbox" id="customJavaCheck" /> <input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span> <span class="checkmark"></span>
<div class="checkbox-content"> <div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div> <div class="checkbox-title" data-i18n="settings.useCustomJava">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with <div class="checkbox-description" data-i18n="settings.javaDescription">Override the bundled Java runtime with
your own installation</div> your own installation</div>
</div> </div>
</label> </label>
@@ -431,23 +426,38 @@
<div id="customJavaOptions" class="custom-java-options" style="display: none;"> <div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label> <label class="settings-input-label" data-i18n="settings.javaPath">Java Executable Path</label>
<div class="settings-input-with-button"> <div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input" <input type="text" id="customJavaPath" class="settings-input"
placeholder="Select Java path..." readonly /> data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
<button id="browseJavaBtn" class="settings-browse-btn"> <button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
Browse <span data-i18n="settings.javaBrowse">Browse</span>
</button> </button>
</div> </div>
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux) <span data-i18n="settings.javaHint">Select the Java installation folder (supports Windows, Mac, Linux)</span>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-language"></i>
<span data-i18n="settings.language">Language</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select Language</label>
<select id="languageSelect" class="settings-input">
<!-- Options populated by i18n.js -->
</select>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -455,8 +465,8 @@
<div id="skins-page" class="page"> <div id="skins-page" class="page">
<div class="placeholder-content"> <div class="placeholder-content">
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i> <i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2>Skins</h2> <h2 data-i18n="skins.title">Skins</h2>
<p>Skin customization coming soon...</p> <p data-i18n="skins.comingSoon">Skin customization coming soon...</p>
</div> </div>
</div> </div>
@@ -465,22 +475,22 @@
<div class="logs-header"> <div class="logs-header">
<h2 class="logs-title"> <h2 class="logs-title">
<i class="fas fa-terminal"></i> <i class="fas fa-terminal"></i>
SYSTEM LOGS <span data-i18n="settings.logs">SYSTEM LOGS</span>
</h2> </h2>
<div class="logs-actions"> <div class="logs-actions">
<button class="logs-action-btn" onclick="copyLogs()"> <button class="logs-action-btn" onclick="copyLogs()">
<i class="fas fa-copy"></i> Copy <i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
</button> </button>
<button class="logs-action-btn" onclick="refreshLogs()"> <button class="logs-action-btn" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> Refresh <i class="fas fa-sync-alt"></i> <span data-i18n="settings.logsRefresh">Refresh</span>
</button> </button>
<button class="logs-action-btn" onclick="openLogsFolder()"> <button class="logs-action-btn" onclick="openLogsFolder()">
<i class="fas fa-folder-open"></i> Open Folder <i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open Folder</span>
</button> </button>
</div> </div>
</div> </div>
<div id="logsTerminal" class="logs-terminal"> <div id="logsTerminal" class="logs-terminal">
<div class="text-gray-500 text-center mt-10">Loading logs...</div> <div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading logs...</div>
</div> </div>
</div> </div>
</div> </div>
@@ -493,7 +503,7 @@
<div class="mods-modal-header"> <div class="mods-modal-header">
<h2 class="mods-modal-title"> <h2 class="mods-modal-title">
<i class="fas fa-box mr-2"></i> <i class="fas fa-box mr-2"></i>
MY MODS <span data-i18n="mods.modalTitle">MY MODS</span>
</h2> </h2>
<button id="closeMyModsModal" class="mods-modal-close"> <button id="closeMyModsModal" class="mods-modal-close">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -509,7 +519,7 @@
<div id="progressOverlay" class="progress-overlay" style="display: none;"> <div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content"> <div class="progress-content">
<div class="progress-info"> <div class="progress-info">
<span id="progressText">Initializing...</span> <span id="progressText" data-i18n="progress.initializing">Initializing...</span>
<span id="progressPercent">0%</span> <span id="progressPercent">0%</span>
</div> </div>
<div class="progress-bar-container"> <div class="progress-bar-container">
@@ -527,29 +537,29 @@
<div class="chat-username-modal-header"> <div class="chat-username-modal-header">
<h2 class="chat-username-modal-title"> <h2 class="chat-username-modal-title">
<i class="fas fa-comments mr-2"></i> <i class="fas fa-comments mr-2"></i>
Join Chat <span data-i18n="chat.joinChat">Join Chat</span>
</h2> </h2>
</div> </div>
<div class="chat-username-modal-body"> <div class="chat-username-modal-body">
<p class="chat-username-modal-description"> <p class="chat-username-modal-description" data-i18n="chat.chooseUsername">
Choose a username to join the Players Chat Choose a username to join the Players Chat
</p> </p>
<div class="chat-username-input-group"> <div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label">Username</label> <label for="chatUsernameInput" class="chat-username-label" data-i18n="chat.username">Username</label>
<input type="text" id="chatUsernameInput" class="chat-username-input" <input type="text" id="chatUsernameInput" class="chat-username-input"
placeholder="Enter your username..." maxlength="20" autocomplete="off" /> data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span> <span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, - and _ only</span>
<span id="chatUsernameError" class="chat-username-error"></span> <span id="chatUsernameError" class="chat-username-error"></span>
</div> </div>
</div> </div>
<div class="chat-username-modal-footer"> <div class="chat-username-modal-footer">
<button id="chatUsernameCancel" class="chat-username-btn-cancel"> <button id="chatUsernameCancel" class="chat-username-btn-cancel">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel <span data-i18n="common.cancel">Cancel</span>
</button> </button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit"> <button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
Join Chat <span data-i18n="chat.joinButton">Join Chat</span>
</button> </button>
</div> </div>
</div> </div>
@@ -561,7 +571,7 @@
<div class="uuid-modal-header"> <div class="uuid-modal-header">
<h2 class="uuid-modal-title"> <h2 class="uuid-modal-title">
<i class="fas fa-fingerprint mr-2"></i> <i class="fas fa-fingerprint mr-2"></i>
UUID Management <span data-i18n="uuid.modalTitle">UUID Management</span>
</h2> </h2>
<button id="uuidModalClose" class="modal-close-btn"> <button id="uuidModalClose" class="modal-close-btn">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -570,7 +580,7 @@
<div class="uuid-modal-body"> <div class="uuid-modal-body">
<div class="uuid-current-section"> <div class="uuid-current-section">
<h3 class="uuid-section-title">Current User UUID</h3> <h3 class="uuid-section-title" data-i18n="uuid.currentUserUUID">Current User UUID</h3>
<div class="uuid-current-display"> <div class="uuid-current-display">
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly /> <input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID"> <button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
@@ -585,34 +595,34 @@
<div class="uuid-list-section"> <div class="uuid-list-section">
<div class="uuid-list-header"> <div class="uuid-list-header">
<h3 class="uuid-section-title">All Player UUIDs</h3> <h3 class="uuid-section-title" data-i18n="uuid.allPlayerUUIDs">All Player UUIDs</h3>
<button id="generateNewUuidBtn" class="uuid-generate-btn"> <button id="generateNewUuidBtn" class="uuid-generate-btn">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Generate New UUID <span data-i18n="uuid.generateNew">Generate New UUID</span>
</button> </button>
</div> </div>
<div id="uuidList" class="uuid-list"> <div id="uuidList" class="uuid-list">
<div class="uuid-loading"> <div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i> <i class="fas fa-spinner fa-spin"></i>
Loading UUIDs... <span data-i18n="uuid.loadingUUIDs">Loading UUIDs...</span>
</div> </div>
</div> </div>
</div> </div>
<div class="uuid-custom-section"> <div class="uuid-custom-section">
<h3 class="uuid-section-title">Set Custom UUID</h3> <h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
<div class="uuid-custom-form"> <div class="uuid-custom-form">
<input type="text" id="customUuidInput" class="uuid-input" <input type="text" id="customUuidInput" class="uuid-input"
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" data-i18n-placeholder="uuid.customPlaceholder"
maxlength="36" /> maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn"> <button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
Set UUID <span data-i18n="uuid.setUUID">Set UUID</span>
</button> </button>
</div> </div>
<p class="uuid-custom-hint"> <p class="uuid-custom-hint">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
Warning: Setting a custom UUID will change your current player identity <span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player identity</span>
</p> </p>
</div> </div>
</div> </div>
@@ -625,7 +635,7 @@
<div class="profile-modal-header"> <div class="profile-modal-header">
<h2 class="profile-modal-title"> <h2 class="profile-modal-title">
<i class="fas fa-users-cog mr-2"></i> <i class="fas fa-users-cog mr-2"></i>
Manage Profiles <span data-i18n="profiles.modalTitle">Manage Profiles</span>
</h2> </h2>
<button class="modal-close-btn" onclick="closeProfileManager()"> <button class="modal-close-btn" onclick="closeProfileManager()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -636,10 +646,10 @@
<!-- Populated by JS --> <!-- Populated by JS -->
</div> </div>
<div class="profile-create-section"> <div class="profile-create-section">
<input type="text" id="newProfileName" placeholder="New Profile Name" class="profile-input" <input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder" class="profile-input"
maxlength="20"> maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()"> <button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> Create Profile <i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
</button> </button>
</div> </div>
</div> </div>
@@ -674,10 +684,10 @@
<div id="discordNotification" class="discord-notification"> <div id="discordNotification" class="discord-notification">
<div class="notification-content"> <div class="notification-content">
<i class="fab fa-discord"></i> <i class="fab fa-discord"></i>
<span class="notification-text">Join our Discord community!</span> <span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
<button class="notification-action" <button class="notification-action"
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')"> onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
Join Discord <span data-i18n="discord.joinButton">Join Discord</span>
</button> </button>
</div> </div>
<button class="notification-close" onclick="closeDiscordNotification()"> <button class="notification-close" onclick="closeDiscordNotification()">
@@ -691,7 +701,7 @@
<div class="chat-color-modal-header"> <div class="chat-color-modal-header">
<h3 class="chat-color-modal-title"> <h3 class="chat-color-modal-title">
<i class="fas fa-palette"></i> <i class="fas fa-palette"></i>
Customize Username Color <span data-i18n="chat.colorModal.title">Customize Username Color</span>
</h3> </h3>
<button class="modal-close-btn" onclick="closeChatColorModal()"> <button class="modal-close-btn" onclick="closeChatColorModal()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -699,7 +709,7 @@
</div> </div>
<div class="chat-color-modal-body"> <div class="chat-color-modal-body">
<div id="solidColorSection" class="color-section"> <div id="solidColorSection" class="color-section">
<h4>Choose a solid color:</h4> <h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
<div class="predefined-colors"> <div class="predefined-colors">
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div> <div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div> <div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
@@ -711,23 +721,24 @@
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div> <div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
</div> </div>
<div class="custom-color-input"> <div class="custom-color-input">
<label for="customColor">Custom color:</label> <label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
<input type="color" id="customColor" value="#3498db"> <input type="color" id="customColor" value="#3498db">
</div> </div>
</div> </div>
<div class="color-preview"> <div class="color-preview">
<h4>Preview:</h4> <h4 data-i18n="chat.colorModal.preview">Preview:</h4>
<div id="colorPreview" class="preview-username">YourUsername</div> <div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">YourUsername</div>
</div> </div>
</div> </div>
<div class="chat-color-modal-footer"> <div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button> <button class="btn-secondary" onclick="closeChatColorModal()"><span data-i18n="common.cancel">Cancel</span></button>
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button> <button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply Color</span></button>
</div> </div>
</div> </div>
</div> </div>
<script src="js/i18n.js"></script>
<script type="module" src="js/update.js"></script> <script type="module" src="js/update.js"></script>
</body> </body>

89
GUI/js/i18n.js Normal file
View File

@@ -0,0 +1,89 @@
// Minimal i18n system - optimized async loading
const i18n = (() => {
let currentLang = 'en';
let translations = {};
const availableLanguages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'pt-BR', name: 'Português (Brasil)' }
];
// Load single language file
async function loadLanguage(lang) {
if (translations[lang]) return true;
try {
const response = await fetch(`locales/${lang}.json`);
if (response.ok) {
translations[lang] = await response.json();
return true;
}
} catch (e) {
console.warn(`Failed to load language: ${lang}`);
}
return false;
}
// Get translation by key
function t(key) {
const keys = key.split('.');
let value = translations[currentLang];
for (const k of keys) {
if (value && value[k] !== undefined) {
value = value[k];
} else {
return key;
}
}
return value;
}
// Set language
async function setLanguage(lang) {
await loadLanguage(lang);
if (translations[lang]) {
currentLang = lang;
updateDOM();
window.electronAPI?.saveLanguage(lang);
}
}
// Update all elements with data-i18n attribute
function updateDOM() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
el.textContent = t(key);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.placeholder = t(key);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.title = t(key);
});
}
// Initialize - load saved language only
async function init(savedLang) {
const lang = savedLang || 'en';
await loadLanguage(lang);
currentLang = lang;
updateDOM();
}
return {
init,
t,
setLanguage,
getAvailableLanguages: () => availableLanguages,
getCurrentLanguage: () => currentLang
};
})();
// Make i18n globally available
window.i18n = i18n;

View File

@@ -51,7 +51,7 @@ export async function installGame() {
isDownloading = true; isDownloading = true;
if (installBtn) { if (installBtn) {
installBtn.disabled = true; installBtn.disabled = true;
installText.textContent = 'INSTALLING...'; installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
} }
try { try {
@@ -59,8 +59,9 @@ export async function installGame() {
const result = await window.electronAPI.installGame(playerName, '', installPath); const result = await window.electronAPI.installGame(playerName, '', installPath);
if (result.success) { if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' }); window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true); window.LauncherUI.showLauncherOrInstall(true);
@@ -76,8 +77,9 @@ export async function installGame() {
simulateInstallation(playerName); simulateInstallation(playerName);
} }
} catch (error) { } catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` }); window.LauncherUI.updateProgress({ message: errorMsg });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
resetInstallButton(); resetInstallButton();
@@ -92,10 +94,13 @@ function simulateInstallation(playerName) {
progress += Math.random() * 3; progress += Math.random() * 3;
if (progress > 100) progress = 100; if (progress > 100) progress = 100;
const installingMsg = window.i18n ? window.i18n.t('progress.installingGameFiles') : 'Installing game files...';
const completeMsg = window.i18n ? window.i18n.t('progress.installComplete') : 'Installation complete!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ window.LauncherUI.updateProgress({
percent: progress, percent: progress,
message: progress < 100 ? 'Installing game files...' : 'Installation complete!', message: progress < 100 ? installingMsg : completeMsg,
speed: 1024 * 1024 * (5 + Math.random() * 10), speed: 1024 * 1024 * (5 + Math.random() * 10),
downloaded: progress * 1024 * 1024 * 20, downloaded: progress * 1024 * 1024 * 20,
total: 1024 * 1024 * 2000 total: 1024 * 1024 * 2000
@@ -104,9 +109,10 @@ function simulateInstallation(playerName) {
if (progress >= 100) { if (progress >= 100) {
clearInterval(interval); clearInterval(interval);
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
setTimeout(() => { setTimeout(() => {
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' }); window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true); window.LauncherUI.showLauncherOrInstall(true);

View File

@@ -160,7 +160,8 @@ window.deleteProfile = async (id) => {
window.switchProfile = async (id) => { window.switchProfile = async (id) => {
try { try {
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Switching Profile...' }); const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
await window.electronAPI.profile.activate(id); await window.electronAPI.profile.activate(id);
@@ -178,7 +179,8 @@ window.switchProfile = async (id) => {
if (dropdown) dropdown.classList.remove('show'); if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Profile Switched!' }); const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
window.LauncherUI.updateProgress({ message: switchedMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 1000); setTimeout(() => window.LauncherUI.hideProgress(), 1000);
} }
@@ -221,7 +223,8 @@ export async function launch() {
} }
try { try {
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' }); const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
if (window.electronAPI && window.electronAPI.launchGame) { if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference); const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
@@ -261,7 +264,12 @@ export async function launch() {
} }
} }
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') { function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
// Apply defaults with i18n support
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
const existingModal = document.querySelector('.custom-confirm-modal'); const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) { if (existingModal) {
existingModal.remove(); existingModal.remove();
@@ -383,22 +391,28 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
} }
export async function uninstallGame() { export async function uninstallGame() {
const message = window.i18n ? window.i18n.t('confirm.uninstallGameMessage') : 'Are you sure you want to uninstall Hytale? All game files will be deleted.';
const title = window.i18n ? window.i18n.t('confirm.uninstallGameTitle') : 'Uninstall Game';
const confirmBtn = window.i18n ? window.i18n.t('confirm.uninstallGameButton') : 'Uninstall';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm( showCustomConfirm(
'Are you sure you want to uninstall Hytale? All game files will be deleted.', message,
'Uninstall Game', title,
async () => { async () => {
await performUninstall(); await performUninstall();
}, },
null, null,
'Uninstall', confirmBtn,
'Cancel' cancelBtn
); );
} }
async function performUninstall() { async function performUninstall() {
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' }); const uninstallingMsg = window.i18n ? window.i18n.t('progress.uninstallingGame') : 'Uninstalling game...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: uninstallingMsg });
if (uninstallBtn) uninstallBtn.disabled = true; if (uninstallBtn) uninstallBtn.disabled = true;
try { try {
@@ -406,8 +420,9 @@ async function performUninstall() {
const result = await window.electronAPI.uninstallGame(); const result = await window.electronAPI.uninstallGame();
if (result.success) { if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' }); window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false); window.LauncherUI.showLauncherOrInstall(false);
@@ -417,9 +432,10 @@ async function performUninstall() {
throw new Error(result.error || 'Uninstall failed'); throw new Error(result.error || 'Uninstall failed');
} }
} else { } else {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
setTimeout(() => { setTimeout(() => {
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' }); window.LauncherUI.updateProgress({ message: successMsg });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false); window.LauncherUI.showLauncherOrInstall(false);
@@ -428,8 +444,9 @@ async function performUninstall() {
}, 2000); }, 2000);
} }
} catch (error) { } catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.uninstallFailed').replace('{error}', error.message) : `Uninstall failed: ${error.message}`;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` }); window.LauncherUI.updateProgress({ message: errorMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 3000); setTimeout(() => window.LauncherUI.hideProgress(), 3000);
} }
} finally { } finally {
@@ -484,7 +501,7 @@ function resetPlayButton() {
isDownloading = false; isDownloading = false;
if (playBtn) { if (playBtn) {
playBtn.disabled = false; playBtn.disabled = false;
playText.textContent = 'PLAY'; playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
} }
} }

View File

@@ -113,10 +113,15 @@ function displayInstalledMods(mods) {
modsContainer.innerHTML = ` modsContainer.innerHTML = `
<div class=\"empty-installed-mods\"> <div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i> <i class=\"fas fa-box-open\"></i>
<h4>No Mods Installed</h4> <h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
<p>Add mods from CurseForge or import local files</p> <p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
</div> </div>
`; `;
if (window.i18n) {
const container = modsContainer.querySelector('.empty-installed-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
}
return; return;
} }
@@ -138,9 +143,9 @@ function displayInstalledMods(mods) {
function createInstalledModCard(mod) { function createInstalledModCard(mod) {
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500'; const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED'; const statusText = mod.enabled ? (window.i18n ? window.i18n.t('mods.active') : 'ACTIVE') : (window.i18n ? window.i18n.t('mods.disabled') : 'DISABLED');
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable'; const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE'; const toggleBtnText = mod.enabled ? (window.i18n ? window.i18n.t('mods.disable') : 'DISABLE') : (window.i18n ? window.i18n.t('mods.enable') : 'ENABLE');
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play'; const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
return ` return `
@@ -154,7 +159,7 @@ function createInstalledModCard(mod) {
<h4 class="installed-mod-name">${mod.name}</h4> <h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span> <span class="installed-mod-version">v${mod.version}</span>
</div> </div>
<p class="installed-mod-description">${mod.description || 'No description available'}</p> <p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
</div> </div>
<div class="installed-mod-actions"> <div class="installed-mod-actions">
@@ -163,7 +168,7 @@ function createInstalledModCard(mod) {
${statusText} ${statusText}
</div> </div>
<div class="installed-mod-buttons"> <div class="installed-mod-buttons">
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod"> <button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="${window.i18n ? window.i18n.t('mods.delete') : 'Delete mod'}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}"> <button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
@@ -180,7 +185,7 @@ async function loadBrowseMods() {
const browseContainer = document.getElementById('browseModsList'); const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return; if (!browseContainer) return;
browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>'; browseContainer.innerHTML = `<div class="loading-mods"><div class="loading-spinner"></div><span>${window.i18n ? window.i18n.t('mods.loadingMods') : 'Loading mods from CurseForge...'}</span></div>`;
try { try {
if (!API_KEY || API_KEY.length < 10) { if (!API_KEY || API_KEY.length < 10) {
@@ -264,10 +269,15 @@ function displayBrowseMods(mods) {
browseContainer.innerHTML = ` browseContainer.innerHTML = `
<div class=\"empty-browse-mods\"> <div class=\"empty-browse-mods\">
<i class=\"fas fa-search\"></i> <i class=\"fas fa-search\"></i>
<h4>No Mods Found</h4> <h4 data-i18n="mods.noModsFound">No Mods Found</h4>
<p>Try adjusting your search</p> <p data-i18n="mods.noModsFoundDesc">Try adjusting your search</p>
</div> </div>
`; `;
if (window.i18n) {
const container = browseContainer.querySelector('.empty-browse-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.noModsFound');
container.querySelector('p').textContent = window.i18n.t('mods.noModsFoundDesc');
}
return; return;
} }
@@ -324,16 +334,16 @@ function createBrowseModCard(mod) {
<div class=\"mod-actions\"> <div class=\"mod-actions\">
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\"> <button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
<i class=\"fas fa-external-link-alt\"></i> <i class=\"fas fa-external-link-alt\"></i>
VIEW ${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
</button> </button>
${!isInstalled ? ${!isInstalled ?
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\"> `<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
<i class=\"fas fa-download\"></i> <i class="fas fa-download"></i>
INSTALL ${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
</button>` : </button>` :
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled> `<button class="mod-btn-toggle bg-white/10 text-white" disabled>
<i class=\"fas fa-check\"></i> <i class="fas fa-check"></i>
INSTALLED ${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
</button>` </button>`
} }
</div> </div>
@@ -343,7 +353,8 @@ function createBrowseModCard(mod) {
async function downloadAndInstallMod(modInfo) { async function downloadAndInstallMod(modInfo) {
try { try {
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`); const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
window.LauncherUI?.showProgress(downloadMsg);
const result = await window.electronAPI?.downloadMod(modInfo); const result = await window.electronAPI?.downloadMod(modInfo);
@@ -367,20 +378,23 @@ async function downloadAndInstallMod(modInfo) {
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success'); const successMsg = window.i18n ? window.i18n.t('notifications.modsInstalledSuccess').replace('{name}', modInfo.name) : `${modInfo.name} installed successfully! 🎉`;
showNotification(successMsg, 'success');
} else { } else {
throw new Error(result?.error || 'Failed to download mod'); throw new Error(result?.error || 'Failed to download mod');
} }
} catch (error) { } catch (error) {
console.error('Error downloading mod:', error); console.error('Error downloading mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification('Failed to download mod: ' + error.message, 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.modsDownloadFailed').replace('{error}', error.message) : 'Failed to download mod: ' + error.message;
showNotification(errorMsg, 'error');
} }
} }
async function toggleMod(modId) { async function toggleMod(modId) {
try { try {
window.LauncherUI?.showProgress('Toggling mod...'); const toggleMsg = window.i18n ? window.i18n.t('notifications.modsTogglingMod') : 'Toggling mod...';
window.LauncherUI?.showProgress(toggleMsg);
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.toggleMod(modId, modsPath); const result = await window.electronAPI?.toggleMod(modId, modsPath);
@@ -394,7 +408,8 @@ async function toggleMod(modId) {
} catch (error) { } catch (error) {
console.error('Error toggling mod:', error); console.error('Error toggling mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification('Failed to toggle mod: ' + error.message, 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.modsToggleFailed').replace('{error}', error.message) : 'Failed to toggle mod: ' + error.message;
showNotification(errorMsg, 'error');
} }
} }
@@ -402,11 +417,16 @@ async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId); const mod = installedMods.find(m => m.id === modId);
if (!mod) return; if (!mod) return;
const confirmMsg = window.i18n ?
window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') :
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`;
showConfirmModal( showConfirmModal(
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`, confirmMsg,
async () => { async () => {
try { try {
window.LauncherUI?.showProgress('Deleting mod...'); const deleteMsg = window.i18n ? window.i18n.t('notifications.modsDeletingMod') : 'Deleting mod...';
window.LauncherUI?.showProgress(deleteMsg);
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.uninstallMod(modId, modsPath); const result = await window.electronAPI?.uninstallMod(modId, modsPath);
@@ -415,14 +435,16 @@ async function deleteMod(modId) {
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification(`"${mod.name}" deleted successfully`, 'success'); const successMsg = window.i18n ? window.i18n.t('notifications.modsDeletedSuccess').replace('{name}', mod.name) : `"${mod.name}" deleted successfully`;
showNotification(successMsg, 'success');
} else { } else {
throw new Error(result?.error || 'Failed to delete mod'); throw new Error(result?.error || 'Failed to delete mod');
} }
} catch (error) { } catch (error) {
console.error('Error deleting mod:', error); console.error('Error deleting mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification('Failed to delete mod: ' + error.message, 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.modsDeleteFailed').replace('{error}', error.message) : 'Failed to delete mod: ' + error.message;
showNotification(errorMsg, 'error');
} }
} }
); );
@@ -571,7 +593,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);"> <div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;"> <div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i> <i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Confirm Deletion</h3> <h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${window.i18n ? window.i18n.t('mods.confirmDeletion') : 'Confirm Deletion'}</h3>
</div> </div>
</div> </div>
<div style="padding: 24px; color: #e5e7eb;"> <div style="padding: 24px; color: #e5e7eb;">
@@ -587,7 +609,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;
">Cancel</button> ">${window.i18n ? window.i18n.t('common.cancel') : 'Cancel'}</button>
<button class="mod-confirm-delete" style=" <button class="mod-confirm-delete" style="
background: #ef4444; background: #ef4444;
color: white; color: white;
@@ -597,7 +619,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;
">Delete</button> ">${window.i18n ? window.i18n.t('common.delete') : 'Delete'}</button>
</div> </div>
`; `;
@@ -715,7 +737,8 @@ function viewModPage(modId) {
} }
} else { } else {
console.error('Mod not found with ID:', modId); console.error('Mod not found with ID:', modId);
showNotification('Mod information not found', 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.modsModNotFound') : 'Mod information not found';
showNotification(errorMsg, 'error');
} }
} }

View File

@@ -8,17 +8,55 @@ import './chat.js';
import './settings.js'; import './settings.js';
import './logs.js'; import './logs.js';
window.closeDiscordNotification = function () { // Initialize i18n immediately (before DOMContentLoaded)
const notification = document.getElementById('discordNotification'); let i18nInitialized = false;
if (notification) { (async () => {
notification.classList.add('hidden'); const savedLang = await window.electronAPI?.loadLanguage();
setTimeout(() => { await i18n.init(savedLang);
notification.style.display = 'none'; i18nInitialized = true;
}, 300);
// Update language selector if DOM is already loaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
updateLanguageSelector();
} }
}; })();
function updateLanguageSelector() {
const langSelect = document.getElementById('languageSelect');
if (langSelect) {
// Clear existing options
langSelect.innerHTML = '';
const languages = i18n.getAvailableLanguages();
const currentLang = i18n.getCurrentLanguage();
languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang.code;
option.textContent = lang.name;
if (lang.code === currentLang) {
option.selected = true;
}
langSelect.appendChild(option);
});
// Handle language change (add listener only once)
if (!langSelect.hasAttribute('data-listener-added')) {
langSelect.addEventListener('change', async (e) => {
await i18n.setLanguage(e.target.value);
});
langSelect.setAttribute('data-listener-added', 'true');
}
}
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Populate language selector (wait for i18n if needed)
if (i18nInitialized) {
updateLanguageSelector();
}
// Discord notification
const notification = document.getElementById('discordNotification'); const notification = document.getElementById('discordNotification');
if (notification) { if (notification) {
const dismissed = localStorage.getItem('discordNotificationDismissed'); const dismissed = localStorage.getItem('discordNotificationDismissed');
@@ -32,8 +70,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
const originalClose = window.closeDiscordNotification; window.closeDiscordNotification = function() {
window.closeDiscordNotification = function () { const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
localStorage.setItem('discordNotificationDismissed', 'true'); localStorage.setItem('discordNotificationDismissed', 'true');
originalClose();
}; };

View File

@@ -22,7 +22,12 @@ let uuidList;
let customUuidInput; let customUuidInput;
let setCustomUuidBtn; let setCustomUuidBtn;
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') { function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
// Apply defaults with i18n support
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
const existingModal = document.querySelector('.custom-confirm-modal'); const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) { if (existingModal) {
existingModal.remove(); existingModal.remove();
@@ -313,9 +318,11 @@ async function saveDiscordRPC() {
// Feedback visuel pour l'utilisateur // Feedback visuel pour l'utilisateur
if (enabled) { if (enabled) {
showNotification('Discord Rich Presence enabled', 'success'); const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
showNotification(msg, 'success');
} else { } else {
showNotification('Discord Rich Presence disabled', 'success'); const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
showNotification(msg, 'success');
} }
} else { } else {
throw new Error('Failed to save Discord RPC setting'); throw new Error('Failed to save Discord RPC setting');
@@ -323,7 +330,8 @@ async function saveDiscordRPC() {
} }
} catch (error) { } catch (error) {
console.error('Error saving Discord RPC setting:', error); console.error('Error saving Discord RPC setting:', error);
showNotification('Failed to save Discord setting', 'error'); const msg = window.i18n ? window.i18n.t('notifications.discordSaveFailed') : 'Failed to save Discord setting';
showNotification(msg, 'error');
} }
} }
@@ -347,16 +355,19 @@ async function savePlayerName() {
const playerName = settingsPlayerName.value.trim(); const playerName = settingsPlayerName.value.trim();
if (!playerName) { if (!playerName) {
showNotification('Please enter a valid player name', 'error'); const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
showNotification(msg, 'error');
return; return;
} }
await window.electronAPI.saveUsername(playerName); await window.electronAPI.saveUsername(playerName);
showNotification('Player name saved successfully', 'success'); const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
showNotification(successMsg, 'success');
} catch (error) { } catch (error) {
console.error('Error saving player name:', error); console.error('Error saving player name:', error);
showNotification('Failed to save player name', 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
showNotification(errorMsg, 'error');
} }
} }
@@ -507,34 +518,43 @@ async function copyCurrentUuid() {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value; const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
if (uuid && navigator.clipboard) { if (uuid && navigator.clipboard) {
await navigator.clipboard.writeText(uuid); await navigator.clipboard.writeText(uuid);
showNotification('UUID copied to clipboard!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
showNotification(msg, 'success');
} }
} catch (error) { } catch (error) {
console.error('Error copying UUID:', error); console.error('Error copying UUID:', error);
showNotification('Failed to copy UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
showNotification(msg, 'error');
} }
} }
async function regenerateCurrentUuid() { async function regenerateCurrentUuid() {
try { try {
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) { if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm( showCustomConfirm(
'Are you sure you want to generate a new UUID? This will change your player identity.', message,
'Generate New UUID', title,
async () => { async () => {
await performRegenerateUuid(); await performRegenerateUuid();
}, },
null, null,
'Generate', confirmBtn,
'Cancel' cancelBtn
); );
} else { } else {
console.error('electronAPI.resetCurrentUserUuid not available'); console.error('electronAPI.resetCurrentUserUuid not available');
showNotification('UUID regeneration not available', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidRegenNotAvailable') : 'UUID regeneration not available';
showNotification(msg, 'error');
} }
} catch (error) { } catch (error) {
console.error('Error in regenerateCurrentUuid:', error); console.error('Error in regenerateCurrentUuid:', error);
showNotification('Failed to regenerate UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed') : 'Failed to regenerate UUID';
showNotification(msg, 'error');
} }
} }
@@ -544,7 +564,8 @@ async function performRegenerateUuid() {
if (result.success && result.uuid) { if (result.success && result.uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid; if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid; if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
showNotification('New UUID generated successfully!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
showNotification(msg, 'success');
if (uuidModal && uuidModal.style.display !== 'none') { if (uuidModal && uuidModal.style.display !== 'none') {
await loadAllUuids(); await loadAllUuids();
@@ -554,7 +575,8 @@ async function performRegenerateUuid() {
} }
} catch (error) { } catch (error) {
console.error('Error regenerating UUID:', error); console.error('Error regenerating UUID:', error);
showNotification(`Failed to regenerate UUID: ${error.message}`, 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed').replace('{error}', error.message) : `Failed to regenerate UUID: ${error.message}`;
showNotification(msg, 'error');
} }
} }
@@ -647,19 +669,22 @@ async function generateNewUuid() {
const newUuid = await window.electronAPI.generateNewUuid(); const newUuid = await window.electronAPI.generateNewUuid();
if (newUuid) { if (newUuid) {
if (customUuidInput) customUuidInput.value = newUuid; if (customUuidInput) customUuidInput.value = newUuid;
showNotification('New UUID generated!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
showNotification(msg, 'success');
} }
} }
} catch (error) { } catch (error) {
console.error('Error generating new UUID:', error); console.error('Error generating new UUID:', error);
showNotification('Failed to generate new UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
showNotification(msg, 'error');
} }
} }
async function setCustomUuid() { async function setCustomUuid() {
try { try {
if (!customUuidInput || !customUuidInput.value.trim()) { if (!customUuidInput || !customUuidInput.value.trim()) {
showNotification('Please enter a UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidRequired') : 'Please enter a UUID';
showNotification(msg, 'error');
return; return;
} }
@@ -667,23 +692,30 @@ async function setCustomUuid() {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(uuid)) { if (!uuidRegex.test(uuid)) {
showNotification('Invalid UUID format', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
showNotification(msg, 'error');
return; return;
} }
const message = window.i18n ? window.i18n.t('confirm.setCustomUuidMessage') : 'Are you sure you want to set this custom UUID? This will change your player identity.';
const title = window.i18n ? window.i18n.t('confirm.setCustomUuidTitle') : 'Set Custom UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.setCustomUuidButton') : 'Set UUID';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm( showCustomConfirm(
'Are you sure you want to set this custom UUID? This will change your player identity.', message,
'Set Custom UUID', title,
async () => { async () => {
await performSetCustomUuid(uuid); await performSetCustomUuid(uuid);
}, },
null, null,
'Set UUID', confirmBtn,
'Cancel' cancelBtn
); );
} catch (error) { } catch (error) {
console.error('Error in setCustomUuid:', error); console.error('Error in setCustomUuid:', error);
showNotification('Failed to set custom UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed') : 'Failed to set custom UUID';
showNotification(msg, 'error');
} }
} }
@@ -698,7 +730,8 @@ async function setCustomUuid() {
if (modalCurrentUuid) modalCurrentUuid.value = uuid; if (modalCurrentUuid) modalCurrentUuid.value = uuid;
if (customUuidInput) customUuidInput.value = ''; if (customUuidInput) customUuidInput.value = '';
showNotification('Custom UUID set successfully!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
showNotification(msg, 'success');
await loadAllUuids(); await loadAllUuids();
} else { } else {
@@ -707,7 +740,8 @@ async function setCustomUuid() {
} }
} catch (error) { } catch (error) {
console.error('Error setting custom UUID:', error); console.error('Error setting custom UUID:', error);
showNotification(`Failed to set custom UUID: ${error.message}`, 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
showNotification(msg, 'error');
} }
} }
@@ -715,29 +749,37 @@ window.copyUuid = async function(uuid) {
try { try {
if (navigator.clipboard) { if (navigator.clipboard) {
await navigator.clipboard.writeText(uuid); await navigator.clipboard.writeText(uuid);
showNotification('UUID copied to clipboard!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
showNotification(msg, 'success');
} }
} catch (error) { } catch (error) {
console.error('Error copying UUID:', error); console.error('Error copying UUID:', error);
showNotification('Failed to copy UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
showNotification(msg, 'error');
} }
}; };
window.deleteUuid = async function(username) { window.deleteUuid = async function(username) {
try { try {
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm( showCustomConfirm(
`Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`, message,
'Delete UUID', title,
async () => { async () => {
await performDeleteUuid(username); await performDeleteUuid(username);
}, },
null, null,
'Delete', confirmBtn,
'Cancel' cancelBtn
); );
} catch (error) { } catch (error) {
console.error('Error in deleteUuid:', error); console.error('Error in deleteUuid:', error);
showNotification('Failed to delete UUID', 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID';
showNotification(msg, 'error');
} }
}; };
@@ -747,7 +789,8 @@ async function performDeleteUuid(username) {
const result = await window.electronAPI.deleteUuidForUser(username); const result = await window.electronAPI.deleteUuidForUser(username);
if (result.success) { if (result.success) {
showNotification('UUID deleted successfully!', 'success'); const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
showNotification(msg, 'success');
await loadAllUuids(); await loadAllUuids();
} else { } else {
throw new Error(result.error || 'Failed to delete UUID'); throw new Error(result.error || 'Failed to delete UUID');
@@ -755,7 +798,8 @@ async function performDeleteUuid(username) {
} }
} catch (error) { } catch (error) {
console.error('Error deleting UUID:', error); console.error('Error deleting UUID:', error);
showNotification(`Failed to delete UUID: ${error.message}`, 'error'); const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
showNotification(msg, 'error');
} }
} }

View File

@@ -366,7 +366,7 @@ function lockPlayButton(locked) {
if (!playButton.getAttribute('data-original-text')) { if (!playButton.getAttribute('data-original-text')) {
playButton.setAttribute('data-original-text', spanElement.textContent); playButton.setAttribute('data-original-text', spanElement.textContent);
} }
spanElement.textContent = 'CHECKING...'; spanElement.textContent = window.i18n ? window.i18n.t('play.checking') : 'CHECKING...';
} }
console.log('Play button locked'); console.log('Play button locked');
@@ -377,9 +377,9 @@ function lockPlayButton(locked) {
playButton.removeAttribute('data-locked'); playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span'); const spanElement = playButton.querySelector('span');
const originalText = playButton.getAttribute('data-original-text'); if (spanElement) {
if (spanElement && originalText) { // Use i18n to get the current translation instead of restoring saved text
spanElement.textContent = originalText; spanElement.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
playButton.removeAttribute('data-original-text'); playButton.removeAttribute('data-original-text');
} }
@@ -393,7 +393,8 @@ async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame; const existingGame = window.firstLaunchExistingGame;
if (!existingGame) { if (!existingGame) {
showNotification('Error: Game data not found', 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.gameDataNotFound') : 'Error: Game data not found';
showNotification(errorMsg, 'error');
return; return;
} }
@@ -410,7 +411,8 @@ async function acceptFirstLaunchUpdate() {
try { try {
showProgress(); showProgress();
updateProgress({ message: 'Starting mandatory game update...', percent: 0 }); const updateMsg = window.i18n ? window.i18n.t('progress.startingUpdate') : 'Starting mandatory game update...';
updateProgress({ message: updateMsg, percent: 0 });
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame); const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
@@ -424,10 +426,12 @@ async function acceptFirstLaunchUpdate() {
if (result.success) { if (result.success) {
hideProgress(); hideProgress();
showNotification('Game updated successfully! 🎉', 'success'); const successMsg = window.i18n ? window.i18n.t('notifications.gameUpdatedSuccess') : 'Game updated successfully! 🎉';
showNotification(successMsg, 'success');
} else { } else {
hideProgress(); hideProgress();
showNotification(`Update failed: ${result.error}`, 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.updateFailed').replace('{error}', result.error) : `Update failed: ${result.error}`;
showNotification(errorMsg, 'error');
} }
} catch (error) { } catch (error) {
if (modal) { if (modal) {
@@ -435,7 +439,8 @@ async function acceptFirstLaunchUpdate() {
} }
lockPlayButton(false); lockPlayButton(false);
hideProgress(); hideProgress();
showNotification(`Update error: ${error.message}`, 'error'); const errorMsg = window.i18n ? window.i18n.t('notifications.updateError').replace('{error}', error.message) : `Update error: ${error.message}`;
showNotification(errorMsg, 'error');
} }
} }

234
GUI/locales/en.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Play",
"mods": "Mods",
"news": "News",
"chat": "Players Chat",
"settings": "Settings",
"skins": "Skins"
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
"playerName": "Player Name",
"playerNamePlaceholder": "Enter your name",
"customInstallation": "Custom Installation",
"installationFolder": "Installation Folder",
"pathPlaceholder": "Default location",
"browse": "Browse",
"installButton": "INSTALL HYTALE",
"installing": "INSTALLING..."
},
"play": {
"ready": "READY TO PLAY",
"subtitle": "Launch Hytale and enter the adventure",
"playButton": "PLAY HYTALE",
"latestNews": "LATEST NEWS",
"viewAll": "VIEW ALL",
"checking": "CHECKING...",
"play": "PLAY"
},
"mods": {
"searchPlaceholder": "Search mods...",
"myMods": "MY MODS",
"previous": "PREVIOUS",
"next": "NEXT",
"page": "Page",
"of": "of",
"modalTitle": "MY MODS",
"noModsFound": "No Mods Found",
"noModsFoundDesc": "Try adjusting your search",
"noModsInstalled": "No Mods Installed",
"noModsInstalledDesc": "Add mods from CurseForge or import local files",
"view": "VIEW",
"install": "INSTALL",
"installed": "INSTALLED",
"enable": "ENABLE",
"disable": "DISABLE",
"active": "ACTIVE",
"disabled": "DISABLED",
"delete": "Delete mod",
"noDescription": "No description available",
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
"confirmDeleteDesc": "This action cannot be undone.",
"confirmDeletion": "Confirm Deletion"
},
"news": {
"title": "ALL NEWS",
"readMore": "Read More"
},
"chat": {
"title": "PLAYERS CHAT",
"pickColor": "Color",
"inputPlaceholder": "Type your message...",
"send": "Send",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Secure chat - Links are censored",
"joinChat": "Join Chat",
"chooseUsername": "Choose a username to join the Players Chat",
"username": "Username",
"usernamePlaceholder": "Enter your username...",
"usernameHint": "3-20 characters, letters, numbers, - and _ only",
"joinButton": "Join Chat",
"colorModal": {
"title": "Customize Username Color",
"chooseSolid": "Choose a solid color:",
"customColor": "Custom color:",
"preview": "Preview:",
"previewUsername": "Username",
"apply": "Apply Color"
}
},
"settings": {
"title": "SETTINGS",
"java": "Java Runtime",
"useCustomJava": "Use Custom Java Path",
"javaDescription": "Override the bundled Java runtime with your own installation",
"javaPath": "Java Executable Path",
"javaPathPlaceholder": "Select Java path...",
"javaBrowse": "Browse",
"javaHint": "Select the Java installation folder (supports Windows, Mac, Linux)",
"discord": "Discord Integration",
"enableRPC": "Enable Discord Rich Presence",
"discordDescription": "Show your launcher activity on Discord",
"game": "Game Options",
"playerName": "Player Name",
"playerNamePlaceholder": "Enter your player name",
"playerNameHint": "This name will be used in-game (1-16 characters)",
"openGameLocation": "Open Game Location",
"openGameLocationDesc": "Open the game installation folder",
"account": "Player UUID Management",
"currentUUID": "Current UUID",
"uuidPlaceholder": "Loading UUID...",
"copyUUID": "Copy UUID",
"regenerateUUID": "Regenerate UUID",
"uuidHint": "Your unique player identifier for this username",
"manageUUIDs": "Manage All UUIDs",
"manageUUIDsDesc": "View and manage all player UUIDs",
"language": "Language",
"selectLanguage": "Select Language",
"repairGame": "Repair Game",
"reinstallGame": "Reinstall game files (preserves data)",
"gpuPreference": "GPU Preference",
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrated",
"gpuDedicated": "Dedicated",
"logs": "SYSTEM LOGS",
"logsCopy": "Copy",
"logsRefresh": "Refresh",
"logsFolder": "Open Folder",
"logsLoading": "Loading logs..."
},
"uuid": {
"modalTitle": "UUID Management",
"currentUserUUID": "Current User UUID",
"allPlayerUUIDs": "All Player UUIDs",
"generateNew": "Generate New UUID",
"loadingUUIDs": "Loading UUIDs...",
"setCustomUUID": "Set Custom UUID",
"customPlaceholder": "Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Set UUID",
"warning": "Warning: Setting a custom UUID will change your current player identity",
"copyTooltip": "Copy UUID",
"regenerateTooltip": "Generate New UUID"
},
"profiles": {
"modalTitle": "Manage Profiles",
"newProfilePlaceholder": "New Profile Name",
"createProfile": "Create Profile"
},
"discord": {
"notificationText": "Join our Discord community!",
"joinButton": "Join Discord"
},
"skins": {
"title": "Skins",
"comingSoon": "Skin customization coming soon..."
},
"common": {
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"close": "Close",
"delete": "Delete",
"edit": "Edit",
"loading": "Loading...",
"apply": "Apply"
},
"notifications": {
"gameDataNotFound": "Error: Game data not found",
"gameUpdatedSuccess": "Game updated successfully! 🎉",
"updateFailed": "Update failed: {error}",
"updateError": "Update error: {error}",
"discordEnabled": "Discord Rich Presence enabled",
"discordDisabled": "Discord Rich Presence disabled",
"discordSaveFailed": "Failed to save Discord setting",
"playerNameRequired": "Please enter a valid player name",
"playerNameSaved": "Player name saved successfully",
"playerNameSaveFailed": "Failed to save player name",
"uuidCopied": "UUID copied to clipboard!",
"uuidCopyFailed": "Failed to copy UUID",
"uuidRegenNotAvailable": "UUID regeneration not available",
"uuidRegenFailed": "Failed to regenerate UUID",
"uuidGenerated": "New UUID generated successfully!",
"uuidGeneratedShort": "New UUID generated!",
"uuidGenerateFailed": "Failed to generate new UUID",
"uuidRequired": "Please enter a UUID",
"uuidInvalidFormat": "Invalid UUID format",
"uuidSetFailed": "Failed to set custom UUID",
"uuidSetSuccess": "Custom UUID set successfully!",
"uuidDeleteFailed": "Failed to delete UUID",
"uuidDeleteSuccess": "UUID deleted successfully!",
"modsDownloading": "Downloading {name}...",
"modsTogglingMod": "Toggling mod...",
"modsDeletingMod": "Deleting mod...",
"modsLoadingMods": "Loading mods from CurseForge...",
"modsInstalledSuccess": "{name} installed successfully! 🎉",
"modsDeletedSuccess": "{name} deleted successfully",
"modsDownloadFailed": "Failed to download mod: {error}",
"modsToggleFailed": "Failed to toggle mod: {error}",
"modsDeleteFailed": "Failed to delete mod: {error}",
"modsModNotFound": "Mod information not found"
},
"confirm": {
"defaultTitle": "Confirm action",
"regenerateUuidTitle": "Generate new UUID",
"regenerateUuidMessage": "Are you sure you want to generate a new UUID? This will change your player identity.",
"regenerateUuidButton": "Generate",
"setCustomUuidTitle": "Set custom UUID",
"setCustomUuidMessage": "Are you sure you want to set this custom UUID? This will change your player identity.",
"setCustomUuidButton": "Set UUID",
"deleteUuidTitle": "Delete UUID",
"deleteUuidMessage": "Are you sure you want to delete the UUID for \"{username}\"? This action cannot be undone.",
"deleteUuidButton": "Delete",
"uninstallGameTitle": "Uninstall game",
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
"uninstallGameButton": "Uninstall"
},
"progress": {
"initializing": "Initializing...",
"downloading": "Downloading...",
"installing": "Installing...",
"extracting": "Extracting...",
"verifying": "Verifying...",
"switchingProfile": "Switching profile...",
"profileSwitched": "Profile switched!",
"startingGame": "Starting game...",
"launching": "LAUNCHING...",
"uninstallingGame": "Uninstalling game...",
"gameUninstalled": "Game uninstalled successfully!",
"uninstallFailed": "Uninstall failed: {error}",
"startingUpdate": "Starting mandatory game update...",
"installationComplete": "Installation completed successfully!",
"installationFailed": "Installation failed: {error}",
"installingGameFiles": "Installing game files...",
"installComplete": "Installation complete!"
}
}

234
GUI/locales/es.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Jugar",
"mods": "Mods",
"news": "Noticias",
"chat": "Chat de Jugadores",
"settings": "Configuración",
"skins": "Aspectos"
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "LAUNCHER GRATUITO",
"playerName": "Nombre del Jugador",
"playerNamePlaceholder": "Ingresa tu nombre",
"customInstallation": "Instalación Personalizada",
"installationFolder": "Carpeta de Instalación",
"pathPlaceholder": "Ubicación predeterminada",
"browse": "Examinar",
"installButton": "INSTALAR HYTALE",
"installing": "INSTALANDO..."
},
"play": {
"ready": "LISTO PARA JUGAR",
"subtitle": "Inicia Hytale y entra en la aventura",
"playButton": "JUGAR HYTALE",
"latestNews": "ÚLTIMAS NOTICIAS",
"viewAll": "VER TODO",
"checking": "VERIFICANDO...",
"play": "JUGAR"
},
"mods": {
"searchPlaceholder": "Buscar mods...",
"myMods": "MIS MODS",
"previous": "ANTERIOR",
"next": "SIGUIENTE",
"page": "Página",
"of": "de",
"modalTitle": "MIS MODS",
"noModsFound": "No se encontraron mods",
"noModsFoundDesc": "Intenta ajustar tu búsqueda",
"noModsInstalled": "No hay mods instalados",
"noModsInstalledDesc": "Añade mods desde CurseForge o importa archivos locales",
"view": "VER",
"install": "INSTALAR",
"installed": "INSTALADO",
"enable": "ACTIVAR",
"disable": "DESACTIVAR",
"active": "ACTIVO",
"disabled": "DESACTIVADO",
"delete": "Eliminar mod",
"noDescription": "Sin descripción disponible",
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
"confirmDeletion": "Confirmar eliminación"
},
"news": {
"title": "TODAS LAS NOTICIAS",
"readMore": "Leer más"
},
"chat": {
"title": "CHAT DE JUGADORES",
"pickColor": "Color",
"inputPlaceholder": "Escribe tu mensaje...",
"send": "Enviar",
"online": "en línea",
"charCounter": "{current}/{max}",
"secureChat": "Chat seguro - Los enlaces están censurados",
"joinChat": "Unirse al chat",
"chooseUsername": "Elige un nombre de usuario para unirte al chat de jugadores",
"username": "Nombre de usuario",
"usernamePlaceholder": "Ingresa tu nombre de usuario...",
"usernameHint": "3-20 caracteres, letras, números, - y _ solamente",
"joinButton": "Unirse al Chat",
"colorModal": {
"title": "Personalizar color del nombre",
"chooseSolid": "Elige un color sólido:",
"customColor": "Color personalizado:",
"preview": "Vista previa:",
"previewUsername": "Nombre de usuario",
"apply": "Aplicar color"
}
},
"settings": {
"title": "CONFIGURACIÓN",
"java": "Entorno Java",
"useCustomJava": "Usar ruta de Java personalizada",
"javaDescription": "Reemplaza el entorno Java incluido con tu propia instalación",
"javaPath": "Ruta del ejecutable Java",
"javaPathPlaceholder": "Selecciona la ruta de Java...",
"javaBrowse": "Examinar",
"javaHint": "Selecciona la carpeta de instalación de Java (compatible con Windows, Mac, Linux)",
"discord": "Integración con Discord",
"enableRPC": "Habilitar Discord Rich Presence",
"discordDescription": "Muestra tu actividad del launcher en Discord",
"game": "Opciones del juego",
"playerName": "Nombre del jugador",
"playerNamePlaceholder": "Ingresa tu nombre de jugador",
"playerNameHint": "Este nombre se usará en el juego (1-16 caracteres)",
"openGameLocation": "Abrir ubicación del juego",
"openGameLocationDesc": "Abre la carpeta de instalación del juego",
"account": "Gestión de UUID del jugador",
"currentUUID": "UUID actual",
"uuidPlaceholder": "Cargando UUID...",
"copyUUID": "Copiar UUID",
"regenerateUUID": "Regenerar UUID",
"uuidHint": "Tu identificador único de jugador para este nombre de usuario",
"manageUUIDs": "Gestionar todos los UUIDs",
"manageUUIDsDesc": "Ver y gestionar todos los UUIDs de jugadores",
"language": "Idioma",
"selectLanguage": "Seleccionar idioma",
"repairGame": "Reparar juego",
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
"gpuPreference": "Preferencia de GPU",
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
"logs": "REGISTROS DEL SISTEMA",
"logsCopy": "Copiar",
"logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta",
"logsLoading": "Cargando registros..."
},
"uuid": {
"modalTitle": "Gestión de UUID",
"currentUserUUID": "UUID del usuario actual",
"allPlayerUUIDs": "Todos los UUIDs de jugadores",
"generateNew": "Generar nuevo UUID",
"loadingUUIDs": "Cargando UUIDs...",
"setCustomUUID": "Establecer UUID personalizado",
"customPlaceholder": "Ingresa un UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Establecer UUID",
"warning": "Advertencia: Establecer un UUID personalizado cambiará tu identidad de jugador actual",
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Generar nuevo UUID"
},
"profiles": {
"modalTitle": "Gestionar perfiles",
"newProfilePlaceholder": "Nombre del nuevo perfil",
"createProfile": "Crear perfil"
},
"discord": {
"notificationText": "¡Únete a nuestra comunidad de Discord!",
"joinButton": "Unirse a Discord"
},
"skins": {
"title": "Aspectos",
"comingSoon": "Personalización de aspectos próximamente..."
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"save": "Guardar",
"close": "Cerrar",
"delete": "Eliminar",
"edit": "Editar",
"loading": "Cargando...",
"apply": "Aplicar"
},
"notifications": {
"gameDataNotFound": "Error: No se encontraron datos del juego",
"gameUpdatedSuccess": "¡Juego actualizado con éxito! 🎉",
"updateFailed": "Actualización fallida: {error}",
"updateError": "Error de actualización: {error}",
"discordEnabled": "Discord Rich Presence habilitado",
"discordDisabled": "Discord Rich Presence deshabilitado",
"discordSaveFailed": "Error al guardar la configuración de Discord",
"playerNameRequired": "Por favor ingresa un nombre de jugador válido",
"playerNameSaved": "Nombre de jugador guardado con éxito",
"playerNameSaveFailed": "Error al guardar el nombre de jugador",
"uuidCopied": "¡UUID copiado al portapapeles!",
"uuidCopyFailed": "Error al copiar UUID",
"uuidRegenNotAvailable": "Regeneración de UUID no disponible",
"uuidRegenFailed": "Error al regenerar UUID",
"uuidGenerated": "¡Nuevo UUID generado con éxito!",
"uuidGeneratedShort": "¡Nuevo UUID generado!",
"uuidGenerateFailed": "Error al generar nuevo UUID",
"uuidRequired": "Por favor ingresa un UUID",
"uuidInvalidFormat": "Formato de UUID inválido",
"uuidSetFailed": "Error al establecer UUID personalizado",
"uuidSetSuccess": "¡UUID personalizado establecido con éxito!",
"uuidDeleteFailed": "Error al eliminar UUID",
"uuidDeleteSuccess": "¡UUID eliminado con éxito!",
"modsDownloading": "Descargando {name}...",
"modsTogglingMod": "Alternando mod...",
"modsDeletingMod": "Eliminando mod...",
"modsLoadingMods": "Cargando mods desde CurseForge...",
"modsInstalledSuccess": "¡{name} instalado con éxito! 🎉",
"modsDeletedSuccess": "{name} eliminado con éxito",
"modsDownloadFailed": "Error al descargar mod: {error}",
"modsToggleFailed": "Error al alternar mod: {error}",
"modsDeleteFailed": "Error al eliminar mod: {error}",
"modsModNotFound": "Información del mod no encontrada"
},
"confirm": {
"defaultTitle": "Confirmar acción",
"regenerateUuidTitle": "Generar nuevo UUID",
"regenerateUuidMessage": "¿Estás seguro de que quieres generar un nuevo UUID? Esto cambiará tu identidad de jugador.",
"regenerateUuidButton": "Generar",
"setCustomUuidTitle": "Establecer UUID personalizado",
"setCustomUuidMessage": "¿Estás seguro de que quieres establecer este UUID personalizado? Esto cambiará tu identidad de jugador.",
"setCustomUuidButton": "Establecer UUID",
"deleteUuidTitle": "Eliminar UUID",
"deleteUuidMessage": "¿Estás seguro de que quieres eliminar el UUID de \"{username}\"? Esta acción no se puede deshacer.",
"deleteUuidButton": "Eliminar",
"uninstallGameTitle": "Desinstalar juego",
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
"downloading": "Descargando...",
"installing": "Instalando...",
"extracting": "Extrayendo...",
"verifying": "Verificando...",
"switchingProfile": "Cambiando perfil...",
"profileSwitched": "¡Perfil cambiado!",
"startingGame": "Iniciando juego...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando juego...",
"gameUninstalled": "¡Juego desinstalado con éxito!",
"uninstallFailed": "Desinstalación fallida: {error}",
"startingUpdate": "Iniciando actualización obligatoria del juego...",
"installationComplete": "¡Instalación completada con éxito!",
"installationFailed": "Instalación fallida: {error}",
"installingGameFiles": "Instalando archivos del juego...",
"installComplete": "¡Instalación completa!"
}
}

234
GUI/locales/pt-BR.json Normal file
View File

@@ -0,0 +1,234 @@
{
"nav": {
"play": "Jogar",
"mods": "Mods",
"news": "Notícias",
"chat": "Chat de Jogadores",
"settings": "Configurações",
"skins": "Aparências"
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "LANÇADOR JOGO GRATUITO",
"playerName": "Nome do Jogador",
"playerNamePlaceholder": "Digite seu nome",
"customInstallation": "Instalação Personalizada",
"installationFolder": "Pasta de Instalação",
"pathPlaceholder": "Local padrão",
"browse": "Procurar",
"installButton": "INSTALAR HYTALE",
"installing": "INSTALANDO..."
},
"play": {
"ready": "PRONTO PARA JOGAR",
"subtitle": "Inicie Hytale e entre na aventura",
"playButton": "JOGAR HYTALE",
"latestNews": "ÚLTIMAS NOTÍCIAS",
"viewAll": "VER TUDO",
"checking": "VERIFICANDO...",
"play": "JOGAR"
},
"mods": {
"searchPlaceholder": "Pesquisar mods...",
"myMods": "MEUS MODS",
"previous": "ANTERIOR",
"next": "PRÓXIMO",
"page": "Página",
"of": "de",
"modalTitle": "MEUS MODS",
"noModsFound": "Nenhum mod encontrado",
"noModsFoundDesc": "Tente ajustar sua pesquisa",
"noModsInstalled": "Nenhum mod instalado",
"noModsInstalledDesc": "Adicione mods do CurseForge ou importe arquivos locais",
"view": "VER",
"install": "INSTALAR",
"installed": "INSTALADO",
"enable": "ATIVAR",
"disable": "DESATIVAR",
"active": "ATIVO",
"disabled": "DESATIVADO",
"delete": "Excluir mod",
"noDescription": "Nenhuma descrição disponível",
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
"confirmDeletion": "Confirmar exclusão"
},
"news": {
"title": "TODAS AS NOTÍCIAS",
"readMore": "Leia mais"
},
"chat": {
"title": "CHAT DE JOGADORES",
"pickColor": "Cor",
"inputPlaceholder": "Digite sua mensagem...",
"send": "Enviar",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Chat seguro - Links são censurados",
"joinChat": "Entrar no chat",
"chooseUsername": "Escolha um nome de usuário para entrar no chat de jogadores",
"username": "Nome de usuário",
"usernamePlaceholder": "Digite seu nome de usuário...",
"usernameHint": "3-20 caracteres, letras, números, - e _ apenas",
"joinButton": "Entrar no Chat",
"colorModal": {
"title": "Personalizar cor do nome de usuário",
"chooseSolid": "Escolha uma cor sólida:",
"customColor": "Cor personalizada:",
"preview": "Visualização:",
"previewUsername": "Nome de usuário",
"apply": "Aplicar cor"
}
},
"settings": {
"title": "CONFIGURAÇÕES",
"java": "Tempo de execução Java",
"useCustomJava": "Usar caminho personalizado do Java",
"javaDescription": "Substitua o tempo de execução Java incluído pela sua própria instalação",
"javaPath": "Caminho do executável Java",
"javaPathPlaceholder": "Selecione o caminho do Java...",
"javaBrowse": "Procurar",
"javaHint": "Selecione a pasta de instalação do Java (suporta Windows, Mac, Linux)",
"discord": "Integração do Discord",
"enableRPC": "Ativar Discord Rich Presence",
"discordDescription": "Mostre sua atividade do lançador no Discord",
"game": "Opções do jogo",
"playerName": "Nome do jogador",
"playerNamePlaceholder": "Digite seu nome de jogador",
"playerNameHint": "Este nome será usado no jogo (1-16 caracteres)",
"openGameLocation": "Abrir local do jogo",
"openGameLocationDesc": "Abra a pasta de instalação do jogo",
"account": "Gerenciamento de UUID do jogador",
"currentUUID": "UUID atual",
"uuidPlaceholder": "Carregando UUID...",
"copyUUID": "Copiar UUID",
"regenerateUUID": "Regenerar UUID",
"uuidHint": "Seu identificador único de jogador para este nome de usuário",
"manageUUIDs": "Gerenciar todos os UUIDs",
"manageUUIDsDesc": "Ver e gerenciar todos os UUIDs de jogadores",
"language": "Idioma",
"selectLanguage": "Selecionar idioma",
"repairGame": "Reparar jogo",
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
"gpuPreference": "Preferência de GPU",
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
"logs": "REGISTROS DO SISTEMA",
"logsCopy": "Copiar",
"logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta",
"logsLoading": "Carregando registros..."
},
"uuid": {
"modalTitle": "Gerenciamento de UUID",
"currentUserUUID": "UUID do usuário atual",
"allPlayerUUIDs": "Todos os UUIDs de jogadores",
"generateNew": "Gerar novo UUID",
"loadingUUIDs": "Carregando UUIDs...",
"setCustomUUID": "Definir UUID personalizado",
"customPlaceholder": "Digite um UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Definir UUID",
"warning": "Aviso: Definir um UUID personalizado alterará sua identidade de jogador atual",
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Gerar novo UUID"
},
"profiles": {
"modalTitle": "Gerenciar perfis",
"newProfilePlaceholder": "Nome do novo perfil",
"createProfile": "Criar perfil"
},
"discord": {
"notificationText": "Junte-se à nossa comunidade do Discord!",
"joinButton": "Entrar no Discord"
},
"skins": {
"title": "Aparências",
"comingSoon": "Personalização de aparências em breve..."
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"save": "Salvar",
"close": "Fechar",
"delete": "Excluir",
"edit": "Editar",
"loading": "Carregando...",
"apply": "Aplicar"
},
"notifications": {
"gameDataNotFound": "Erro: Dados do jogo não encontrados",
"gameUpdatedSuccess": "Jogo atualizado com sucesso! 🎉",
"updateFailed": "Falha na atualização: {error}",
"updateError": "Erro de atualização: {error}",
"discordEnabled": "Discord Rich Presence ativado",
"discordDisabled": "Discord Rich Presence desativado",
"discordSaveFailed": "Falha ao salvar configuração do Discord",
"playerNameRequired": "Por favor, digite um nome de jogador válido",
"playerNameSaved": "Nome do jogador salvo com sucesso",
"playerNameSaveFailed": "Falha ao salvar o nome do jogador",
"uuidCopied": "UUID copiado para a área de transferência!",
"uuidCopyFailed": "Falha ao copiar UUID",
"uuidRegenNotAvailable": "Regeneração de UUID não disponível",
"uuidRegenFailed": "Falha ao regenerar UUID",
"uuidGenerated": "Novo UUID gerado com sucesso!",
"uuidGeneratedShort": "Novo UUID gerado!",
"uuidGenerateFailed": "Falha ao gerar novo UUID",
"uuidRequired": "Por favor, digite um UUID",
"uuidInvalidFormat": "Formato de UUID inválido",
"uuidSetFailed": "Falha ao definir UUID personalizado",
"uuidSetSuccess": "UUID personalizado definido com sucesso!",
"uuidDeleteFailed": "Falha ao excluir UUID",
"uuidDeleteSuccess": "UUID excluído com sucesso!",
"modsDownloading": "Baixando {name}...",
"modsTogglingMod": "Alternando mod...",
"modsDeletingMod": "Excluindo mod...",
"modsLoadingMods": "Carregando mods do CurseForge...",
"modsInstalledSuccess": "{name} instalado com sucesso! 🎉",
"modsDeletedSuccess": "{name} excluído com sucesso",
"modsDownloadFailed": "Falha ao baixar mod: {error}",
"modsToggleFailed": "Falha ao alternar mod: {error}",
"modsDeleteFailed": "Falha ao excluir mod: {error}",
"modsModNotFound": "Informações do mod não encontradas"
},
"confirm": {
"defaultTitle": "Confirmar ação",
"regenerateUuidTitle": "Gerar novo UUID",
"regenerateUuidMessage": "Tem certeza de que deseja gerar um novo UUID? Isso alterará sua identidade de jogador.",
"regenerateUuidButton": "Gerar",
"setCustomUuidTitle": "Definir UUID personalizado",
"setCustomUuidMessage": "Tem certeza de que deseja definir este UUID personalizado? Isso alterará sua identidade de jogador.",
"setCustomUuidButton": "Definir UUID",
"deleteUuidTitle": "Excluir UUID",
"deleteUuidMessage": "Tem certeza de que deseja excluir o UUID de \"{username}\"? Esta ação não pode ser desfeita.",
"deleteUuidButton": "Excluir",
"uninstallGameTitle": "Desinstalar jogo",
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
"downloading": "Baixando...",
"installing": "Instalando...",
"extracting": "Extraindo...",
"verifying": "Verificando...",
"switchingProfile": "Alternando perfil...",
"profileSwitched": "Perfil alternado!",
"startingGame": "Iniciando jogo...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando jogo...",
"gameUninstalled": "Jogo desinstalado com sucesso!",
"uninstallFailed": "Falha na desinstalação: {error}",
"startingUpdate": "Iniciando atualização obrigatória do jogo...",
"installationComplete": "Instalação concluída com sucesso!",
"installationFailed": "Falha na instalação: {error}",
"installingGameFiles": "Instalando arquivos do jogo...",
"installComplete": "Instalação concluída!"
}
}

View File

@@ -4033,6 +4033,17 @@ body {
cursor: not-allowed; cursor: not-allowed;
} }
/* Language selector styling */
select.settings-input {
cursor: pointer;
}
select.settings-input option {
background: #1a1a1a;
color: white;
padding: 0.5rem;
}
.settings-button-group { .settings-button-group {
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@@ -78,6 +78,9 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
## 📋 Changelog ## 📋 Changelog
### 🆕 v2.0.ba *(Minor Update: Performance & Utilities)*
[TODO] add features list here
### 🆕 v2.0.2a *(Minor Update)* ### 🆕 v2.0.2a *(Minor Update)*
- 🧑‍🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**. - 🧑‍🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile. - 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.

View File

@@ -147,6 +147,15 @@ function loadDiscordRPC() {
return config.discordRPC !== undefined ? config.discordRPC : true; return config.discordRPC !== undefined ? config.discordRPC : true;
} }
function saveLanguage(language) {
saveConfig({ language: language || 'en' });
}
function loadLanguage() {
const config = loadConfig();
return config.language || 'en';
}
function saveModsToConfig(mods) { function saveModsToConfig(mods) {
try { try {
const config = loadConfig(); const config = loadConfig();
@@ -302,6 +311,8 @@ module.exports = {
loadInstallPath, loadInstallPath,
saveDiscordRPC, saveDiscordRPC,
loadDiscordRPC, loadDiscordRPC,
saveLanguage,
loadLanguage,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
isFirstLaunch, isFirstLaunch,

View File

@@ -15,6 +15,8 @@ const {
loadInstallPath, loadInstallPath,
saveDiscordRPC, saveDiscordRPC,
loadDiscordRPC, loadDiscordRPC,
saveLanguage,
loadLanguage,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
getUuidForUser, getUuidForUser,
@@ -118,6 +120,10 @@ module.exports = {
saveDiscordRPC, saveDiscordRPC,
loadDiscordRPC, loadDiscordRPC,
// Language functions
saveLanguage,
loadLanguage,
// GPU Preference functions // GPU Preference functions
saveGpuPreference, saveGpuPreference,
loadGpuPreference, loadGpuPreference,

11
main.js
View File

@@ -1,7 +1,7 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager'); const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger'); const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager'); const profileManager = require('./backend/managers/profileManager');
@@ -405,6 +405,15 @@ ipcMain.handle('load-discord-rpc', () => {
return loadDiscordRPC(); return loadDiscordRPC();
}); });
ipcMain.handle('save-language', (event, language) => {
saveLanguage(language);
return { success: true };
});
ipcMain.handle('load-language', () => {
return loadLanguage();
});
ipcMain.handle('select-install-path', async () => { ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],

View File

@@ -17,6 +17,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadInstallPath: () => ipcRenderer.invoke('load-install-path'), loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled), saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'), loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
loadLanguage: () => ipcRenderer.invoke('load-language'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),