Skip to content

Commit 491ef47

Browse files
committed
Add multi-language support to client settings
- Add Language property to GameDatabase with load/save persistence - Add GetStringsFileName() and GetAvailableLanguages() to Strings.cs - Load/save language-specific client_strings_xx.json files - Add language dropdown to Settings > Interface tab - Show restart prompt when language changes - Add LanguageRestartTitle and LanguageRestartPrompt strings
1 parent 90ac07a commit 491ef47

3 files changed

Lines changed: 166 additions & 2 deletions

File tree

Intersect.Client.Core/Interface/Shared/SettingsWindow.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public partial class SettingsWindow : Window
4444
private readonly LabeledCheckBox _showHealthAsPercentageCheckbox;
4545
private readonly LabeledCheckBox _showManaAsPercentageCheckbox;
4646
private readonly LabeledCheckBox _simplifiedEscapeMenu;
47+
private readonly LabeledComboBox _languageComboBox;
4748

4849
// Game Settings - Information
4950
private readonly TabButton _gameSettingsTabInformation;
@@ -238,6 +239,25 @@ public SettingsWindow(Base parent) : base(parent: parent, title: Strings.Setting
238239
Text = Strings.Settings.TypewriterText,
239240
};
240241

242+
// Game Settings - Interface: Language Selection.
243+
_languageComboBox = new LabeledComboBox(parent: _interfaceSettings, name: nameof(_languageComboBox))
244+
{
245+
Dock = Pos.Top,
246+
Font = _defaultFont,
247+
FontSize = 12,
248+
Label = Strings.Settings.Language,
249+
TextPadding = new Padding(8, 4, 0, 4),
250+
};
251+
252+
// Populate available languages from detected language files.
253+
var availableLanguages = Strings.GetAvailableLanguages();
254+
foreach (var langCode in availableLanguages)
255+
{
256+
var displayName = GetLanguageDisplayName(langCode);
257+
var addedItem = _languageComboBox.AddItem(label: displayName, userData: langCode);
258+
addedItem.TextAlign = Pos.Left;
259+
}
260+
241261
// Game > Information
242262

243263
_showPingCounterCheckbox = new LabeledCheckBox(parent: _informationSettings, name: nameof(_showPingCounterCheckbox))
@@ -991,6 +1011,9 @@ public void Show(Base? returnTo)
9911011
_autoSoftRetargetOnSelfCast.IsChecked = Globals.Database.AutoSoftRetargetOnSelfCast;
9921012
_typewriterCheckbox.IsChecked = Globals.Database.TypewriterBehavior == Enums.TypewriterBehavior.Word;
9931013

1014+
// Language Settings.
1015+
_languageComboBox.SelectByUserData(Globals.Database.Language);
1016+
9941017
// Video Settings.
9951018
_fullscreenCheckbox.IsChecked = Globals.Database.FullScreen;
9961019
_lightingEnabledCheckbox.IsChecked = Globals.Database.EnableLighting;
@@ -1172,6 +1195,15 @@ private void SettingsApplyBtn_Clicked(Base sender, MouseButtonState arguments)
11721195
Globals.Database.AutoSoftRetargetOnSelfCast = _autoSoftRetargetOnSelfCast.IsChecked;
11731196
Globals.Database.TypewriterBehavior = _typewriterCheckbox.IsChecked ? Enums.TypewriterBehavior.Word : Enums.TypewriterBehavior.Off;
11741197

1198+
// Language Settings — detect if language changed and prompt restart.
1199+
var previousLanguage = Globals.Database.Language;
1200+
var selectedLanguage = _languageComboBox.SelectedItem?.UserData as string ?? "en";
1201+
var languageChanged = !string.Equals(previousLanguage, selectedLanguage, StringComparison.OrdinalIgnoreCase);
1202+
if (languageChanged)
1203+
{
1204+
Globals.Database.Language = selectedLanguage;
1205+
}
1206+
11751207
// Video Settings.
11761208
Globals.Database.EnableScrollingWorldZoom = _enableScrollingWorldZoomCheckbox.IsChecked;
11771209
if (!_worldScale.IsHidden)
@@ -1249,6 +1281,22 @@ private void SettingsApplyBtn_Clicked(Base sender, MouseButtonState arguments)
12491281

12501282
// Hide the currently opened window.
12511283
Hide();
1284+
1285+
// Show restart prompt if the language was changed.
1286+
if (languageChanged)
1287+
{
1288+
AlertWindow.Open(
1289+
Strings.Settings.LanguageRestartPrompt,
1290+
Strings.Settings.LanguageRestartTitle,
1291+
AlertType.Information,
1292+
inputType: InputType.YesNo,
1293+
handleSubmit: (_, _) =>
1294+
{
1295+
Intersect.Framework.Utilities.ProcessHelper.TryRelaunch();
1296+
Globals.IsRunning = false;
1297+
}
1298+
);
1299+
}
12521300
}
12531301

12541302
private void CancelPendingChangesButton_Clicked(Base sender, MouseButtonState arguments)
@@ -1264,4 +1312,43 @@ private void CancelPendingChangesButton_Clicked(Base sender, MouseButtonState ar
12641312
// Hide our current window.
12651313
Hide();
12661314
}
1315+
1316+
/// <summary>
1317+
/// Maps a language code to a human-readable display name.
1318+
/// </summary>
1319+
private static string GetLanguageDisplayName(string langCode)
1320+
{
1321+
return langCode.ToLowerInvariant() switch
1322+
{
1323+
"en" => "English",
1324+
"ar" => "العربية (Arabic)",
1325+
"fr" => "Français (French)",
1326+
"es" => "Español (Spanish)",
1327+
"de" => "Deutsch (German)",
1328+
"pt" => "Português (Portuguese)",
1329+
"ru" => "Русский (Russian)",
1330+
"zh" => "中文 (Chinese)",
1331+
"ja" => "日本語 (Japanese)",
1332+
"ko" => "한국어 (Korean)",
1333+
"tr" => "Türkçe (Turkish)",
1334+
"it" => "Italiano (Italian)",
1335+
"nl" => "Nederlands (Dutch)",
1336+
"pl" => "Polski (Polish)",
1337+
"sv" => "Svenska (Swedish)",
1338+
"da" => "Dansk (Danish)",
1339+
"fi" => "Suomi (Finnish)",
1340+
"no" => "Norsk (Norwegian)",
1341+
"id" => "Bahasa Indonesia (Indonesian)",
1342+
"th" => "ไทย (Thai)",
1343+
"vi" => "Tiếng Việt (Vietnamese)",
1344+
"hi" => "हिन्दी (Hindi)",
1345+
"uk" => "Українська (Ukrainian)",
1346+
"cs" => "Čeština (Czech)",
1347+
"ro" => "Română (Romanian)",
1348+
"hu" => "Magyar (Hungarian)",
1349+
"el" => "Ελληνικά (Greek)",
1350+
"he" => "עברית (Hebrew)",
1351+
_ => langCode.ToUpperInvariant(),
1352+
};
1353+
}
12671354
}

Intersect.Client.Core/Localization/Strings.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ namespace Intersect.Client.Localization;
1919
public static partial class Strings
2020
{
2121
private const string StringsFileName = "client_strings.json";
22+
private const string StringsFilePrefix = "client_strings";
23+
private const string StringsFileExtension = ".json";
2224
private static char[] mQuantityTrimChars = new char[] { '.', '0' };
2325

2426
private static string[] _unitsBits = [string.Empty, "Ki", "Mi", "Gi", "Ti"];
@@ -131,6 +133,69 @@ private static void PostLoad()
131133
Core.Program.OpenALLink = Errors.OpenAllLink.ToString();
132134
}
133135

136+
/// <summary>
137+
/// Gets the language-specific strings filename based on the current language preference.
138+
/// Falls back to the default file if the language-specific file does not exist.
139+
/// </summary>
140+
private static string GetStringsFileName()
141+
{
142+
var language = Intersect.Client.General.Globals.Database?.Language;
143+
if (string.IsNullOrWhiteSpace(language) || string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
144+
{
145+
// Default English uses the base filename for backward compatibility
146+
return StringsFileName;
147+
}
148+
149+
var languageFileName = $"{StringsFilePrefix}_{language}{StringsFileExtension}";
150+
var languageFilePath = Path.Combine(ClientConfiguration.ResourcesDirectory, languageFileName);
151+
152+
// Fall back to default if the language-specific file doesn't exist
153+
return File.Exists(languageFilePath) ? languageFileName : StringsFileName;
154+
}
155+
156+
/// <summary>
157+
/// Scans the resources directory for all available language files and returns a sorted list
158+
/// of language codes (e.g., "en", "ar", "fr").
159+
/// </summary>
160+
public static List<string> GetAvailableLanguages()
161+
{
162+
var languages = new List<string> { "en" }; // English is always available (default)
163+
164+
try
165+
{
166+
var resourcesDir = ClientConfiguration.ResourcesDirectory;
167+
if (!Directory.Exists(resourcesDir))
168+
{
169+
return languages;
170+
}
171+
172+
var pattern = $"{StringsFilePrefix}_*{StringsFileExtension}";
173+
var files = Directory.GetFiles(resourcesDir, pattern);
174+
175+
foreach (var file in files)
176+
{
177+
var fileName = Path.GetFileNameWithoutExtension(file);
178+
// Extract language code from "client_strings_xx"
179+
var prefix = StringsFilePrefix + "_";
180+
if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
181+
{
182+
var langCode = fileName.Substring(prefix.Length);
183+
if (!string.IsNullOrWhiteSpace(langCode) && !languages.Contains(langCode, StringComparer.OrdinalIgnoreCase))
184+
{
185+
languages.Add(langCode);
186+
}
187+
}
188+
}
189+
}
190+
catch (Exception exception)
191+
{
192+
ApplicationContext.Context.Value?.Logger.LogWarning(exception, "Error occurred while scanning for language files");
193+
}
194+
195+
languages.Sort(StringComparer.OrdinalIgnoreCase);
196+
return languages;
197+
}
198+
134199
private class OrdinalComparer : IComparer<string>
135200
{
136201
public int Compare(string? x, string? y) => string.CompareOrdinal(x, y);
@@ -148,8 +213,9 @@ public static void Load()
148213
try
149214
{
150215
var serialized = new Dictionary<string, Dictionary<string, object>>();
216+
var stringsFile = GetStringsFileName();
151217
serialized = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<string, object>>>(
152-
File.ReadAllText(Path.Combine(ClientConfiguration.ResourcesDirectory, StringsFileName))
218+
File.ReadAllText(Path.Combine(ClientConfiguration.ResourcesDirectory, stringsFile))
153219
);
154220

155221
var rootType = typeof(Strings);
@@ -408,8 +474,9 @@ private static void SaveSerialized(Dictionary<string, Dictionary<string, object>
408474
var languageDirectory = Path.Combine(ClientConfiguration.ResourcesDirectory);
409475
if (Directory.Exists(languageDirectory))
410476
{
477+
var stringsFile = GetStringsFileName();
411478
File.WriteAllText(
412-
Path.Combine(languageDirectory, StringsFileName),
479+
Path.Combine(languageDirectory, stringsFile),
413480
JsonConvert.SerializeObject(serialized, Formatting.Indented)
414481
);
415482
}
@@ -2147,6 +2214,12 @@ public partial struct Settings
21472214
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
21482215
public static LocalizedString Language = @"Language";
21492216

2217+
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
2218+
public static LocalizedString LanguageRestartTitle = @"Language Changed";
2219+
2220+
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
2221+
public static LocalizedString LanguageRestartPrompt = @"The language has been changed. Would you like to restart the game now for the new language to take effect?";
2222+
21502223
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
21512224
public static LocalizedString MusicVolume = @"Music Volume: {00}%";
21522225

Intersect.Client.Framework/Database/GameDatabase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ public abstract partial class GameDatabase
7777

7878
public float WorldZoom { get; set; } = 1.0f;
7979

80+
public string Language { get; set; } = "en";
81+
8082
public abstract void DeletePreference(string key);
8183

8284
public abstract bool HasPreference(string key);
@@ -150,6 +152,7 @@ public virtual void LoadPreferences()
150152
UIScale = LoadPreference(nameof(UIScale), 1.0f);
151153
EnableScrollingWorldZoom = LoadPreference(nameof(EnableScrollingWorldZoom), false);
152154
WorldZoom = LoadPreference(nameof(WorldZoom), 1.0f);
155+
Language = LoadPreference(nameof(Language), "en");
153156
}
154157

155158
/// <summary>
@@ -191,6 +194,7 @@ public virtual void SavePreferences()
191194
SavePreference(nameof(UIScale), UIScale);
192195
SavePreference(nameof(EnableScrollingWorldZoom), EnableScrollingWorldZoom);
193196
SavePreference(nameof(WorldZoom), WorldZoom);
197+
SavePreference(nameof(Language), Language);
194198
}
195199

196200
public abstract bool LoadConfig();

0 commit comments

Comments
 (0)