{{font_row_label}}
diff --git a/src/bin/config.ini.template b/src/bin/config.ini.template
index 4e3025542c..e24c6414fb 100644
--- a/src/bin/config.ini.template
+++ b/src/bin/config.ini.template
@@ -16,6 +16,7 @@ ServerIP=localhost
ServerPort=44406
[UI]
Locale=en
+TranslateItemNames=1
Font=
RmlTheme=legacy
UIScalePercent=100
diff --git a/src/source/App/Platform/Windows/Winmain.cpp b/src/source/App/Platform/Windows/Winmain.cpp
index 615575dbfd..ecced7b133 100644
--- a/src/source/App/Platform/Windows/Winmain.cpp
+++ b/src/source/App/Platform/Windows/Winmain.cpp
@@ -26,6 +26,7 @@
#include
#include
#include "Core/Platform/WinIni.h" // private-profile (.ini) API
+#include "Data/DataHandler/ItemData/ItemDataHandler.h"
#include "Data/GameConfig/GameConfig.h"
#include "UI/Core/UIManager.h"
#include "Render/Textures/ZzzOpenglUtil.h"
@@ -2300,6 +2301,7 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int nC
std::string uiLocale(uiLocaleW.begin(), uiLocaleW.end());
I18N::SetLocale(uiLocale.c_str());
}
+ g_ItemDataHandler.SetTranslateItemNames(GameConfig::GetInstance().GetTranslateItemNames());
#ifdef _EDITOR
// Initialize MU Editor with the live SDL GPU window.
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.cpp b/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.cpp
index 0d1049f5cd..9142a659ae 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.cpp
+++ b/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.cpp
@@ -10,9 +10,6 @@
#include "Core/Utilities/StringUtils.h"
#include
-// External references
-extern ITEM_ATTRIBUTE* ItemAttribute;
-
// X-Macro helpers for CSV generation
#define CSV_HEADER_SIMPLE(name, type, arraySize, width, i18nName) "," #name
#define CSV_HEADER_ARRAY(nameWithIndex, baseName, index, type, width, i18nName) "," #nameWithIndex
@@ -29,7 +26,7 @@ extern ITEM_ATTRIBUTE* ItemAttribute;
#define CSV_PRINT_SIMPLE(name, type, arraySize, width, i18nName) PRINT_FIELD_##type(item, name);
#define CSV_PRINT_ARRAY(nameWithIndex, baseName, index, type, width, i18nName) fprintf(csvFp, ",%d", item.baseName[index]);
-bool ItemDataExportAsCSV::ExportToCsv(wchar_t* fileName)
+bool ItemDataExportAsCSV::ExportToCsv(wchar_t* fileName, const ITEM_ATTRIBUTE* attributes)
{
FILE* csvFp = _wfopen(fileName, L"w");
if (!csvFp)
@@ -46,10 +43,10 @@ bool ItemDataExportAsCSV::ExportToCsv(wchar_t* fileName)
for (int i = 0; i < MAX_ITEM; i++)
{
// Only export items with names
- if (ItemAttribute[i].Name[0] != 0)
+ if (attributes[i].Name[0] != 0)
{
// Convert wide char name to UTF-8
- std::string utf8Name = StringUtils::WideToNarrow(ItemAttribute[i].Name);
+ std::string utf8Name = StringUtils::WideToNarrow(attributes[i].Name);
// Escape quotes in name
std::string escapedName;
@@ -69,7 +66,7 @@ bool ItemDataExportAsCSV::ExportToCsv(wchar_t* fileName)
fprintf(csvFp, "%d,\"%s\"", i, escapedName.c_str());
// Print all fields using X-macros
- ITEM_ATTRIBUTE& item = ItemAttribute[i];
+ const ITEM_ATTRIBUTE& item = attributes[i];
ITEM_FIELDS_SIMPLE(CSV_PRINT_SIMPLE)
ITEM_FIELDS_ARRAYS(CSV_PRINT_ARRAY)
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.h b/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.h
index db433058a4..8d1d833945 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.h
+++ b/src/source/Data/DataHandler/ItemData/ItemDataExportAsCSV.h
@@ -2,11 +2,14 @@
#ifdef _EDITOR
+#include "Data/GameData/ItemData/ItemStructs.h"
+
// Item Data Export/Import Operations
class ItemDataExportAsCSV
{
public:
- static bool ExportToCsv(wchar_t* fileName);
+ // Writes the MAX_ITEM records of attributes that have a name.
+ static bool ExportToCsv(wchar_t* fileName, const ITEM_ATTRIBUTE* attributes);
// Future: static bool ImportFromCsv(wchar_t* fileName);
};
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.cpp b/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.cpp
index 170ffdb158..f376388327 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.cpp
+++ b/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.cpp
@@ -11,10 +11,7 @@
#include "Data/Translation/MultiLanguage.h"
#include
-// External references
-extern ITEM_ATTRIBUTE* ItemAttribute;
-
-bool ItemDataExportS6E3::SaveLegacy(wchar_t* fileName)
+bool ItemDataExportS6E3::SaveLegacy(wchar_t* fileName, const ITEM_ATTRIBUTE* attributes)
{
const int Size = sizeof(ITEM_ATTRIBUTE_FILE_LEGACY);
@@ -27,13 +24,13 @@ bool ItemDataExportS6E3::SaveLegacy(wchar_t* fileName)
auto Buffer = std::make_unique(Size * MAX_ITEM);
BYTE* pSeek = Buffer.get();
- // Convert ItemAttribute to ITEM_ATTRIBUTE_FILE_LEGACY format
+ // Convert the attributes to ITEM_ATTRIBUTE_FILE_LEGACY format
for (int i = 0; i < MAX_ITEM; i++)
{
ITEM_ATTRIBUTE_FILE_LEGACY dest;
memset(&dest, 0, Size);
- CopyItemAttributeToDestination(dest, ItemAttribute[i]);
+ CopyItemAttributeToDestination(dest, attributes[i]);
memcpy(pSeek, &dest, Size);
pSeek += Size;
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.h b/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.h
index e31b9ce4a1..5532064f43 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.h
+++ b/src/source/Data/DataHandler/ItemData/ItemDataExportS6E3.h
@@ -4,11 +4,14 @@
#include
+#include "Data/GameData/ItemData/ItemStructs.h"
+
// Item Data Legacy Format Saving Operations (S6E3 format)
class ItemDataExportS6E3
{
public:
- static bool SaveLegacy(wchar_t* fileName);
+ // Writes the MAX_ITEM records of attributes.
+ static bool SaveLegacy(wchar_t* fileName, const ITEM_ATTRIBUTE* attributes);
};
#endif // _EDITOR
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataHandler.cpp b/src/source/Data/DataHandler/ItemData/ItemDataHandler.cpp
index 6513cb5762..748db05a69 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataHandler.cpp
+++ b/src/source/Data/DataHandler/ItemData/ItemDataHandler.cpp
@@ -134,10 +134,11 @@ bool CItemDataHandler::Load(std::string& errorMessage)
#endif
const auto buildStart = std::chrono::steady_clock::now();
- g_ItemDatabase.SetDisplayLocale(I18N::GetCurrentLocale());
+ g_ItemDatabase.SetDisplayLocale(ItemNameLocale());
g_ItemDatabase.SetStatProfiles(std::move(statProfiles));
g_ItemDatabase.Build(result.items);
FillItemAttributes();
+ m_loaded = true;
RegisterLocaleObserver();
MU_LOG_INFO(mu::log::Get("data"), "Loaded {} items from {} in {:.1f} ms (item database build {:.2f} ms)",
@@ -208,9 +209,27 @@ void CItemDataHandler::RegisterLocaleObserver()
void CItemDataHandler::OnLocaleChanged(void* context) noexcept
{
- auto* handler = static_cast(context);
- g_ItemDatabase.SetDisplayLocale(I18N::GetCurrentLocale());
- handler->FillItemAttributes();
+ static_cast(context)->RefreshItemNames();
+}
+
+void CItemDataHandler::SetTranslateItemNames(bool translate)
+{
+ m_translateItemNames = translate;
+ if (m_loaded)
+ {
+ RefreshItemNames();
+ }
+}
+
+std::string_view CItemDataHandler::ItemNameLocale() const
+{
+ return Data::Items::GetItemNameLocale(I18N::GetCurrentLocale(), m_translateItemNames);
+}
+
+void CItemDataHandler::RefreshItemNames()
+{
+ g_ItemDatabase.SetDisplayLocale(ItemNameLocale());
+ FillItemAttributes();
}
#ifdef _EDITOR
@@ -725,11 +744,11 @@ bool CItemDataHandler::ExportAsBmd(std::string& changeLog)
bool CItemDataHandler::ExportAsS6E3(wchar_t* fileName)
{
- return ItemDataExportS6E3::SaveLegacy(fileName);
+ return ItemDataExportS6E3::SaveLegacy(fileName, BuildItemAttributes(I18N::GetCurrentLocale()).get());
}
bool CItemDataHandler::ExportToCsv(wchar_t* fileName)
{
- return ItemDataExportAsCSV::ExportToCsv(fileName);
+ return ItemDataExportAsCSV::ExportToCsv(fileName, BuildItemAttributes(I18N::GetCurrentLocale()).get());
}
#endif
diff --git a/src/source/Data/DataHandler/ItemData/ItemDataHandler.h b/src/source/Data/DataHandler/ItemData/ItemDataHandler.h
index c0267c554a..9d49374c10 100644
--- a/src/source/Data/DataHandler/ItemData/ItemDataHandler.h
+++ b/src/source/Data/DataHandler/ItemData/ItemDataHandler.h
@@ -21,12 +21,22 @@
// Owns loading the item data (Data/Items/*.json) into the item database and
// the item models (Data/Items/Models/*.json) into the item model database, and
// keeps ItemAttribute[], the table most game code still reads, filled from
-// the database. Item names follow the UI locale.
+// the database. Item names follow the UI locale, or stay English while
+// translated item names are off (SetTranslateItemNames).
class CItemDataHandler
{
public:
static CItemDataHandler& GetInstance();
+ // Whether item names follow the UI locale; off shows them in English. The
+ // game sets it from [UI] TranslateItemNames at start and when the option
+ // changes. Once items are loaded, the names change at once.
+ void SetTranslateItemNames(bool translate);
+ bool GetTranslateItemNames() const
+ {
+ return m_translateItemNames;
+ }
+
// Data/Local//Item_.bmd (legacy item files).
static std::wstring GetItemFilePath(const std::wstring& language);
@@ -42,7 +52,7 @@ class CItemDataHandler
#ifdef _EDITOR
// Puts the item into the item database at its (group, number) and refills
- // its row of ItemAttribute[]: the item in the UI locale, or an empty row
+ // its row of ItemAttribute[]: the item in the item name language, or an empty row
// when it no longer exists (no names). The weapon kind and the stat
// profile follow the data (FollowGroupWeaponKind, FollowStatProfile)
// unless the definition changes them itself: then they are the user's pick.
@@ -119,7 +129,7 @@ class CItemDataHandler
// Sets the item's name in `locale` (an empty text removes the translation;
// the English name cannot be emptied) in the item database, whole also when
// it is longer than ITEM_ATTRIBUTE's name; ItemAttribute shows the name in
- // the UI locale. False when the item does not exist, when the English name
+ // the item name language. False when the item does not exist, when the English name
// would be empty (also when the item has none yet) or when the text has
// LocalizedString::Separator, which the item checks refuse.
bool SetItemName(int itemType, std::string_view locale, std::string text);
@@ -171,6 +181,8 @@ class CItemDataHandler
// are and do not count as a failure.
bool ExportAsBmd(std::string& changeLog);
+ // The S6E3 item.bmd and the CSV table have the item names of the UI
+ // locale, also while translated item names are off.
bool ExportAsS6E3(wchar_t* fileName);
bool ExportToCsv(wchar_t* fileName);
#endif
@@ -185,6 +197,11 @@ class CItemDataHandler
~CItemDataHandler() = default;
void FillItemAttributes();
+ // The language of the item names: the UI locale, or English while
+ // translated item names are off.
+ std::string_view ItemNameLocale() const;
+ // Shows the item names in that language.
+ void RefreshItemNames();
void RegisterLocaleObserver();
static void OnLocaleChanged(void* context) noexcept;
@@ -251,6 +268,9 @@ class CItemDataHandler
#endif
bool m_localeObserverRegistered = false;
+ bool m_translateItemNames = true;
+ // Load succeeded once: there are item names to show.
+ bool m_loaded = false;
// Prevent copying
CItemDataHandler(const CItemDataHandler&) = delete;
diff --git a/src/source/Data/GameConfig/GameConfig.cpp b/src/source/Data/GameConfig/GameConfig.cpp
index 7ef8bf0f64..56cb09deb8 100644
--- a/src/source/Data/GameConfig/GameConfig.cpp
+++ b/src/source/Data/GameConfig/GameConfig.cpp
@@ -81,6 +81,7 @@ void GameConfig::Load()
m_chatCommandTemplates = ReadStringList(L"ChatCommands", L"Template");
m_uiLocale = ReadString(CfgSectionUI, CfgKeyUILocale, CfgDefaultUILocale);
+ m_translateItemNames = ReadBool(CfgSectionUI, CfgKeyTranslateItemNames, CfgDefaultTranslateItemNames);
m_fontSelection = ReadString(CfgSectionUI, CfgKeyFont, CfgDefaultFont);
m_rmlTheme = ReadString(CfgSectionUI, CfgKeyRmlTheme, CfgDefaultRmlTheme);
m_uiScalePercent = std::clamp(ReadInt(CfgSectionUI, CfgKeyUIScalePercent, CfgDefaultUIScalePercent),
@@ -153,6 +154,7 @@ void GameConfig::Save()
WriteStringList(L"ChatCommands", L"Template", m_chatCommandTemplates);
WriteString(CfgSectionUI, CfgKeyUILocale, m_uiLocale);
+ WriteBool(CfgSectionUI, CfgKeyTranslateItemNames, m_translateItemNames);
WriteString(CfgSectionUI, CfgKeyFont, m_fontSelection);
WriteString(CfgSectionUI, CfgKeyRmlTheme, m_rmlTheme);
WriteInt(CfgSectionUI, CfgKeyUIScalePercent, m_uiScalePercent);
@@ -331,6 +333,11 @@ void GameConfig::SetUILocale(const std::wstring& locale)
m_uiLocale = locale;
}
+void GameConfig::SetTranslateItemNames(bool translate)
+{
+ m_translateItemNames = translate;
+}
+
void GameConfig::SetFontSelection(const std::wstring& font)
{
m_fontSelection = font;
diff --git a/src/source/Data/GameConfig/GameConfig.h b/src/source/Data/GameConfig/GameConfig.h
index 8144949ecb..814634f1c8 100644
--- a/src/source/Data/GameConfig/GameConfig.h
+++ b/src/source/Data/GameConfig/GameConfig.h
@@ -72,6 +72,14 @@ class GameConfig
std::wstring GetUILocale() const { return m_uiLocale; }
void SetUILocale(const std::wstring& locale);
+ // Whether item names are shown in the UI language (GetUILocale); off keeps them English
+ // while the rest of the UI stays translated.
+ bool GetTranslateItemNames() const
+ {
+ return m_translateItemNames;
+ }
+ void SetTranslateItemNames(bool translate);
+
// UI font family name (GDI face name). Empty = platform default.
std::wstring GetFontSelection() const { return m_fontSelection; }
void SetFontSelection(const std::wstring& font);
@@ -208,6 +216,7 @@ class GameConfig
int m_serverPort;
std::wstring m_uiLocale;
+ bool m_translateItemNames;
std::wstring m_fontSelection;
std::wstring m_rmlTheme;
int m_uiScalePercent;
diff --git a/src/source/Data/GameConfig/GameConfigConstants.h b/src/source/Data/GameConfig/GameConfigConstants.h
index dbf0a6a56a..225ad1fd5e 100644
--- a/src/source/Data/GameConfig/GameConfigConstants.h
+++ b/src/source/Data/GameConfig/GameConfigConstants.h
@@ -37,6 +37,8 @@ namespace CfgKeys
// UI
inline constexpr wchar_t CfgKeyUILocale[] = L"Locale";
inline constexpr wchar_t CfgKeyFont[] = L"Font";
+ // Whether item names follow the UI language; off keeps them English.
+ inline constexpr wchar_t CfgKeyTranslateItemNames[] = L"TranslateItemNames";
// Active RmlUi theme name ("legacy"/"modern"), Data/Interface/RmlUi/themes//. Not yet
// a full runtime hot-swap -- read once at startup. See UI::RmlBridge::RmlTheme.
inline constexpr wchar_t CfgKeyRmlTheme[] = L"RmlTheme";
@@ -111,6 +113,7 @@ namespace CfgDefaults
// I18N locale code; "en" is the default the resx generator falls back to.
inline constexpr wchar_t CfgDefaultUILocale[] = L"en";
+ inline constexpr bool CfgDefaultTranslateItemNames = true;
// UI font family name. Empty = each platform's built-in default (Tahoma on
// Windows, fontconfig "sans-serif" on Linux), so the look is unchanged until
diff --git a/src/source/Data/GameData/ItemData/ItemDatabase.cpp b/src/source/Data/GameData/ItemData/ItemDatabase.cpp
index 913b898f70..5eb137410a 100644
--- a/src/source/Data/GameData/ItemData/ItemDatabase.cpp
+++ b/src/source/Data/GameData/ItemData/ItemDatabase.cpp
@@ -46,6 +46,11 @@ void ItemDatabase::Build(std::span definitions)
void ItemDatabase::SetDisplayLocale(std::string_view locale)
{
+ if (m_displayLocale == locale)
+ {
+ return;
+ }
+ ++m_nameVersion;
m_displayLocale = std::string(locale);
for (ItemDefinition& definition : m_definitions)
{
@@ -178,6 +183,7 @@ void ItemDatabase::UpdateDisplayName(ItemDefinition& definition) const
void ItemDatabase::Store(int itemType, ItemDefinition definition)
{
++m_version;
+ ++m_nameVersion;
definition.group = GetItemGroup(itemType);
definition.number = GetItemNumber(itemType);
UpdateDisplayName(definition);
diff --git a/src/source/Data/GameData/ItemData/ItemDatabase.h b/src/source/Data/GameData/ItemData/ItemDatabase.h
index 93c545748d..28be3c1581 100644
--- a/src/source/Data/GameData/ItemData/ItemDatabase.h
+++ b/src/source/Data/GameData/ItemData/ItemDatabase.h
@@ -28,8 +28,9 @@ class ItemDatabase
// definitions with invalid ids are skipped (validation reports them).
void Build(std::span definitions);
- // Fills ItemDefinition::name from the names for this UI locale, falling
- // back to English.
+ // Fills ItemDefinition::name from the names in the item name language:
+ // the UI language, or English while translated item names are off. A
+ // missing translation falls back to English.
void SetDisplayLocale(std::string_view locale);
const std::string& GetDisplayLocale() const
{
@@ -142,6 +143,15 @@ class ItemDatabase
return m_version;
}
+ // Counts the changes of the item names shown: a stored item or another
+ // language of the names (SetDisplayLocale). Views that keep item names
+ // take them again when it changes. Unlike GetVersion, a language change
+ // leaves the item tools' open edits alone.
+ int GetNameVersion() const
+ {
+ return m_nameVersion;
+ }
+
// Item editor changes. Set replaces the item at the definition's
// (group, number); an item without names counts as not existing.
void Set(const ItemDefinition& definition);
@@ -193,6 +203,7 @@ class ItemDatabase
std::string m_displayLocale{LocalizedString::NeutralLocale};
int m_existingItemCount = 0;
int m_version = 0;
+ int m_nameVersion = 0;
};
} // namespace Data::Items
diff --git a/src/source/Data/GameData/ItemData/ItemDefinition.h b/src/source/Data/GameData/ItemData/ItemDefinition.h
index eac093bd86..bf38614a5a 100644
--- a/src/source/Data/GameData/ItemData/ItemDefinition.h
+++ b/src/source/Data/GameData/ItemData/ItemDefinition.h
@@ -164,7 +164,8 @@ struct ItemDefinition
// All names of the item, English first. Logs always use the English
// name (names.GetNeutral()), never a translation.
LocalizedString names;
- // The name in the current UI locale, shown to the player. Filled by
+ // The name in the item name language (the UI language, or English while
+ // translated item names are off), shown to the player. Filled by
// ItemDatabase from names.
std::wstring name;
diff --git a/src/source/Data/GameData/ItemData/ItemNames.cpp b/src/source/Data/GameData/ItemData/ItemNames.cpp
index f577b83a88..180cbb6f35 100644
--- a/src/source/Data/GameData/ItemData/ItemNames.cpp
+++ b/src/source/Data/GameData/ItemData/ItemNames.cpp
@@ -22,6 +22,11 @@ bool HasNameIn(const ItemDefinition& definition, std::string_view locale)
return !GetOwnName(definition, locale).empty();
}
+std::string_view GetItemNameLocale(std::string_view uiLocale, bool translateItemNames)
+{
+ return translateItemNames ? uiLocale : LocalizedString::NeutralLocale;
+}
+
std::vector CountItemNames(std::span items, std::span locales)
{
std::vector counts;
diff --git a/src/source/Data/GameData/ItemData/ItemNames.h b/src/source/Data/GameData/ItemData/ItemNames.h
index f583701108..b25416740f 100644
--- a/src/source/Data/GameData/ItemData/ItemNames.h
+++ b/src/source/Data/GameData/ItemData/ItemNames.h
@@ -19,6 +19,10 @@ const std::string& GetOwnName(const ItemDefinition& definition, std::string_view
// Whether the item has a name of its own in the language (GetOwnName).
bool HasNameIn(const ItemDefinition& definition, std::string_view locale);
+// The language the game shows item names in: the UI language, or English when
+// the player turned translated item names off in the options window.
+std::string_view GetItemNameLocale(std::string_view uiLocale, bool translateItemNames);
+
struct ItemNameCount
{
std::string locale;
diff --git a/src/source/UI/Inventory/ItemHelpContentKey.cpp b/src/source/UI/Inventory/ItemHelpContentKey.cpp
index 6515e60d94..e680e90772 100644
--- a/src/source/UI/Inventory/ItemHelpContentKey.cpp
+++ b/src/source/UI/Inventory/ItemHelpContentKey.cpp
@@ -8,6 +8,10 @@
mu::ui::window::ItemHelpContentKey mu::ui::window::GetItemHelpContentKey(int itemType)
{
- return {itemType, g_ItemDatabase.GetVersion(), I18N::LocaleIndex(I18N::GetCurrentLocale()), Hero->Class,
+ return {itemType,
+ g_ItemDatabase.GetVersion(),
+ I18N::LocaleIndex(I18N::GetCurrentLocale()),
+ g_ItemDatabase.GetNameVersion(),
+ Hero->Class,
GetItemInfoHeroStats()};
}
diff --git a/src/source/UI/Inventory/ItemHelpContentKey.h b/src/source/UI/Inventory/ItemHelpContentKey.h
index ca7bffb43b..5c07819552 100644
--- a/src/source/UI/Inventory/ItemHelpContentKey.h
+++ b/src/source/UI/Inventory/ItemHelpContentKey.h
@@ -5,13 +5,15 @@
namespace mu::ui::window
{
// What the item help window's lines and table are built from: the item and its data (the item
-// editor and a reload raise the item database's version), the language, and the hero's class,
+// editor and a reload raise the item database's version), the language, the item names (their
+// version also changes when translated item names are turned on or off), and the hero's class,
// level and stats. The window builds them again only when this changes.
struct ItemHelpContentKey
{
int itemType = -1;
int itemDataVersion = 0;
int localeIndex = 0;
+ int itemNameVersion = 0;
CLASS_TYPE heroClass{};
ItemInfoHeroStats heroStats{};
diff --git a/src/source/UI/Options/OptionWindow.cpp b/src/source/UI/Options/OptionWindow.cpp
index 6e1eace52b..bd8be893d4 100644
--- a/src/source/UI/Options/OptionWindow.cpp
+++ b/src/source/UI/Options/OptionWindow.cpp
@@ -6,6 +6,7 @@
#include "Render/Textures/TexturePack.h"
#include "Render/Textures/ZzzTexture.h"
#include "Audio/DSPlaySound.h"
+#include "Data/DataHandler/ItemData/ItemDataHandler.h"
#include "Data/GameConfig/GameConfig.h"
#include "Data/GameConfig/GameConfigConstants.h"
#include "Render/Look/LookPresetSetting.h"
@@ -307,6 +308,7 @@ mu::ui::window::COptionWindow::COptionWindow()
m_iResolutionIndex = 0;
m_bWindowedMode = (g_bUseWindowMode == TRUE);
m_iLanguageIndex = FindCurrentLanguageIndex();
+ m_bTranslateItemNames = GameConfig::GetInstance().GetTranslateItemNames();
m_iFontIndex = FindCurrentFontIndex();
m_iActiveTab = 0;
@@ -398,6 +400,8 @@ void mu::ui::window::COptionWindow::BindRmlModel(Rml::DataModelConstructor& c, O
c.Bind("language_index", &model.languageIndex);
c.Bind("language_row_label", &model.languageRowLabel);
c.Bind("language_value_label", &model.languageValueLabel);
+ c.Bind("translate_item_names", &model.translateItemNames);
+ c.Bind("translate_item_names_label", &model.translateItemNamesLabel);
c.Bind("font_labels", &model.fontLabels);
c.Bind("font_index", &model.fontIndex);
c.Bind("font_row_label", &model.fontRowLabel);
@@ -517,6 +521,9 @@ void mu::ui::window::COptionWindow::BindRmlModel(Rml::DataModelConstructor& c, O
[this](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { RmlToggleRenderAllEffects(); });
c.BindEventCallback("option_toggle_windowed_mode",
[this](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { RmlToggleWindowedMode(); });
+ c.BindEventCallback("option_toggle_translate_item_names",
+ [this](Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&)
+ { RmlToggleTranslateItemNames(); });
c.BindEventCallback("option_gauge",
[this](Rml::DataModelHandle, Rml::Event& ev, const Rml::VariantList& args)
@@ -771,6 +778,7 @@ void mu::ui::window::COptionWindow::OpenningProcess()
// exposed through, so opening this window always reflects the current live truth.
InitResolutionCombo();
m_iLanguageIndex = FindCurrentLanguageIndex();
+ m_bTranslateItemNames = GameConfig::GetInstance().GetTranslateItemNames();
m_iFontIndex = FindCurrentFontIndex();
m_bWindowedMode = (g_bUseWindowMode == TRUE);
@@ -974,6 +982,14 @@ void mu::ui::window::COptionWindow::RmlLanguageChanged(int index)
ApplyLanguage();
}
+void mu::ui::window::COptionWindow::RmlToggleTranslateItemNames()
+{
+ m_bTranslateItemNames = !m_bTranslateItemNames;
+ GameConfig::GetInstance().SetTranslateItemNames(m_bTranslateItemNames);
+ GameConfig::GetInstance().Save();
+ g_ItemDataHandler.SetTranslateItemNames(m_bTranslateItemNames);
+}
+
void mu::ui::window::COptionWindow::RmlFontChanged(int index)
{
// See RmlResolutionChanged's own comment -- same settle-frame guard applies here.
@@ -1648,6 +1664,16 @@ void mu::ui::window::COptionWindow::ApplyWindowModeToggle()
GameConfig::GetInstance().Save();
}
+void mu::ui::window::COptionWindow::SyncLabel(Rml::String& field, const char* fieldName, const wchar_t* text)
+{
+ const Rml::String narrow = StringUtils::WideToNarrow(text);
+ if (field != narrow)
+ {
+ field = narrow;
+ m_RmlView.MarkDirty(fieldName);
+ }
+}
+
void mu::ui::window::COptionWindow::SyncRmlModel()
{
if (!m_RmlView.Document())
@@ -1668,15 +1694,6 @@ void mu::ui::window::COptionWindow::SyncRmlModel()
// Re-fetched every sync, not just once at BuildRmlUi() time -- native re-rendered every one of
// these from the live I18N::Game::* pointer every frame, so a language switch made from this
// very window relabels it immediately rather than only on next reopen.
- const auto syncLabel = [this](Rml::String& field, const char* fieldName, const wchar_t* text)
- {
- const Rml::String narrow = StringUtils::WideToNarrow(text);
- if (field != narrow)
- {
- field = narrow;
- m_RmlView.MarkDirty(fieldName);
- }
- };
// .option-dropdown's own closed-state box shows this instead of a {{}} array-index expression
// -- see model.resolutionValueLabel's own comment (OptionWindow.h) for why.
const auto syncDropdownValue = [this](Rml::String& field, const char* fieldName,
@@ -1690,50 +1707,51 @@ void mu::ui::window::COptionWindow::SyncRmlModel()
m_RmlView.MarkDirty(fieldName);
}
};
- syncLabel(model.title, "title", I18N::Game::Option385);
- syncLabel(model.closeLabel, "close_label", I18N::Game::Close);
- syncLabel(model.tabGameplayLabel, "tab_gameplay_label", I18N::Game::Gameplay);
- syncLabel(model.tabAudioLabel, "tab_audio_label", I18N::Game::Audio);
- syncLabel(model.tabDisplayLabel, "tab_display_label", I18N::Game::Display);
- syncLabel(model.tabGraphicsLabel, "tab_graphics_label", I18N::Game::Graphics);
- syncLabel(model.tabInterfaceLabel, "tab_interface_label", I18N::Game::Interface);
- syncLabel(model.sectionQualityLabel, "section_quality_label", I18N::Game::Quality);
- syncLabel(model.sectionEffectsLabel, "section_effects_label", I18N::Game::Effects);
- syncLabel(model.sectionShadowsLabel, "section_shadows_label", I18N::Game::Shadows);
- syncLabel(model.autoAttackLabel, "auto_attack_label", I18N::Game::AutomaticAttack);
- syncLabel(model.whisperSoundLabel, "whisper_sound_label", I18N::Game::BeepSoundForWhispering);
- syncLabel(model.slideHelpLabel, "slide_help_label", I18N::Game::SlideHelp);
- syncLabel(model.renderAllEffectsLabel, "render_all_effects_label", I18N::Game::RenderFullEffects);
- syncLabel(model.windowedModeLabel, "windowed_mode_label", I18N::Game::WindowedMode);
- syncLabel(model.soundVolumeLabel, "sound_volume_label", I18N::Game::SoundVolume);
- syncLabel(model.musicVolumeLabel, "music_volume_label", I18N::Game::MusicVolume);
- syncLabel(model.renderLevelLabel, "render_level_label", I18N::Game::EffectLimitation);
- syncLabel(model.resolutionRowLabel, "resolution_row_label", I18N::Game::Resolution);
- syncLabel(model.languageRowLabel, "language_row_label", I18N::Game::Language);
- syncLabel(model.fontRowLabel, "font_row_label", I18N::Game::Font);
- syncLabel(model.vsyncLabel, "vsync_label", I18N::Game::VSync);
- syncLabel(model.fpsCapRowLabel, "fps_cap_row_label", I18N::Game::FPSLimit);
- syncLabel(model.disableEffectsLabel, "disable_effects_label", I18N::Game::DisableEffects);
- syncLabel(model.disableParticlesLabel, "disable_particles_label", I18N::Game::DisableParticles);
- syncLabel(model.disableSkillEffectModelsLabel, "disable_skill_effect_models_label",
+ SyncLabel(model.title, "title", I18N::Game::Option385);
+ SyncLabel(model.closeLabel, "close_label", I18N::Game::Close);
+ SyncLabel(model.tabGameplayLabel, "tab_gameplay_label", I18N::Game::Gameplay);
+ SyncLabel(model.tabAudioLabel, "tab_audio_label", I18N::Game::Audio);
+ SyncLabel(model.tabDisplayLabel, "tab_display_label", I18N::Game::Display);
+ SyncLabel(model.tabGraphicsLabel, "tab_graphics_label", I18N::Game::Graphics);
+ SyncLabel(model.tabInterfaceLabel, "tab_interface_label", I18N::Game::Interface);
+ SyncLabel(model.sectionQualityLabel, "section_quality_label", I18N::Game::Quality);
+ SyncLabel(model.sectionEffectsLabel, "section_effects_label", I18N::Game::Effects);
+ SyncLabel(model.sectionShadowsLabel, "section_shadows_label", I18N::Game::Shadows);
+ SyncLabel(model.autoAttackLabel, "auto_attack_label", I18N::Game::AutomaticAttack);
+ SyncLabel(model.whisperSoundLabel, "whisper_sound_label", I18N::Game::BeepSoundForWhispering);
+ SyncLabel(model.slideHelpLabel, "slide_help_label", I18N::Game::SlideHelp);
+ SyncLabel(model.renderAllEffectsLabel, "render_all_effects_label", I18N::Game::RenderFullEffects);
+ SyncLabel(model.windowedModeLabel, "windowed_mode_label", I18N::Game::WindowedMode);
+ SyncLabel(model.soundVolumeLabel, "sound_volume_label", I18N::Game::SoundVolume);
+ SyncLabel(model.musicVolumeLabel, "music_volume_label", I18N::Game::MusicVolume);
+ SyncLabel(model.renderLevelLabel, "render_level_label", I18N::Game::EffectLimitation);
+ SyncLabel(model.resolutionRowLabel, "resolution_row_label", I18N::Game::Resolution);
+ SyncLabel(model.languageRowLabel, "language_row_label", I18N::Game::Language);
+ SyncLabel(model.translateItemNamesLabel, "translate_item_names_label", I18N::Game::TranslateItemNames);
+ SyncLabel(model.fontRowLabel, "font_row_label", I18N::Game::Font);
+ SyncLabel(model.vsyncLabel, "vsync_label", I18N::Game::VSync);
+ SyncLabel(model.fpsCapRowLabel, "fps_cap_row_label", I18N::Game::FPSLimit);
+ SyncLabel(model.disableEffectsLabel, "disable_effects_label", I18N::Game::DisableEffects);
+ SyncLabel(model.disableParticlesLabel, "disable_particles_label", I18N::Game::DisableParticles);
+ SyncLabel(model.disableSkillEffectModelsLabel, "disable_skill_effect_models_label",
I18N::Game::DisableSkillEffectModels);
- syncLabel(model.disableBoidsLabel, "disable_boids_label", I18N::Game::DisableAmbientWildlife);
- syncLabel(model.disableWingShadowLabel, "disable_wing_shadow_label", I18N::Game::DisableWingShadow);
- syncLabel(model.disableBodyShadowsLabel, "disable_body_shadows_label", I18N::Game::DisableShadows);
- syncLabel(model.enhancedGraphicsLabel, "enhanced_graphics_label", I18N::Game::EnhancedGraphics);
- syncLabel(model.lookLightingLabel, "look_lighting_label", I18N::Game::EnhancedLighting);
- syncLabel(model.lookDynamicLightsLabel, "look_dynamic_lights_label", I18N::Game::DynamicLights);
- syncLabel(model.lookLightMoodLabel, "look_light_mood_label", I18N::Game::LightMood);
- syncLabel(model.lookPostProcessingLabel, "look_post_processing_label", I18N::Game::BloomAndColourGrading);
- syncLabel(model.antialiasingRowLabel, "antialiasing_row_label", I18N::Game::AntiAliasing);
- syncLabel(model.antialiasingTooltip, "antialiasing_tooltip", I18N::Game::AntiAliasingTooltip);
- syncLabel(model.anisotropyRowLabel, "anisotropy_row_label", I18N::Game::AnisotropicFiltering);
- syncLabel(model.anisotropyTooltip, "anisotropy_tooltip", I18N::Game::AnisotropicFilteringTooltip);
- syncLabel(model.showFpsCounterLabel, "show_fps_counter_label", I18N::Game::ShowFPSCounter);
- syncLabel(model.showDebugInfoLabel, "show_debug_info_label", I18N::Game::ShowDebugInfo);
- syncLabel(model.themeRowLabel, "theme_row_label", I18N::Game::UITheme);
- syncLabel(model.uiScaleRowLabel, "ui_scale_row_label", I18N::Game::UIScale);
- syncLabel(model.uiScaleTooltip, "ui_scale_tooltip", I18N::Game::UIScaleTooltip);
+ SyncLabel(model.disableBoidsLabel, "disable_boids_label", I18N::Game::DisableAmbientWildlife);
+ SyncLabel(model.disableWingShadowLabel, "disable_wing_shadow_label", I18N::Game::DisableWingShadow);
+ SyncLabel(model.disableBodyShadowsLabel, "disable_body_shadows_label", I18N::Game::DisableShadows);
+ SyncLabel(model.enhancedGraphicsLabel, "enhanced_graphics_label", I18N::Game::EnhancedGraphics);
+ SyncLabel(model.lookLightingLabel, "look_lighting_label", I18N::Game::EnhancedLighting);
+ SyncLabel(model.lookDynamicLightsLabel, "look_dynamic_lights_label", I18N::Game::DynamicLights);
+ SyncLabel(model.lookLightMoodLabel, "look_light_mood_label", I18N::Game::LightMood);
+ SyncLabel(model.lookPostProcessingLabel, "look_post_processing_label", I18N::Game::BloomAndColourGrading);
+ SyncLabel(model.antialiasingRowLabel, "antialiasing_row_label", I18N::Game::AntiAliasing);
+ SyncLabel(model.antialiasingTooltip, "antialiasing_tooltip", I18N::Game::AntiAliasingTooltip);
+ SyncLabel(model.anisotropyRowLabel, "anisotropy_row_label", I18N::Game::AnisotropicFiltering);
+ SyncLabel(model.anisotropyTooltip, "anisotropy_tooltip", I18N::Game::AnisotropicFilteringTooltip);
+ SyncLabel(model.showFpsCounterLabel, "show_fps_counter_label", I18N::Game::ShowFPSCounter);
+ SyncLabel(model.showDebugInfoLabel, "show_debug_info_label", I18N::Game::ShowDebugInfo);
+ SyncLabel(model.themeRowLabel, "theme_row_label", I18N::Game::UITheme);
+ SyncLabel(model.uiScaleRowLabel, "ui_scale_row_label", I18N::Game::UIScale);
+ SyncLabel(model.uiScaleTooltip, "ui_scale_tooltip", I18N::Game::UIScaleTooltip);
// positioned/root_x/root_y stay at their model defaults (false/0/0, see OptionRmlModel's own
// comment): window_shell's `.center-both` CSS places the window, a drag moves it.
@@ -1769,6 +1787,11 @@ void mu::ui::window::COptionWindow::SyncRmlModel()
}
if (model.languageIndex != m_iLanguageIndex) { model.languageIndex = m_iLanguageIndex; m_RmlView.MarkDirty("language_index"); }
syncDropdownValue(model.languageValueLabel, "language_value_label", model.languageLabels, model.languageIndex);
+ if (model.translateItemNames != m_bTranslateItemNames)
+ {
+ model.translateItemNames = m_bTranslateItemNames;
+ m_RmlView.MarkDirty("translate_item_names");
+ }
// Rebuilt every sync (not just once) so the localized "Default" entry follows a live language
// switch, same as native's own GetFontLabels().
@@ -1892,10 +1915,10 @@ void mu::ui::window::COptionWindow::SyncRmlModel()
// The pack labels are built in RefreshTexturePackChoices(); only the localized "Original"
// entry can change here.
- syncLabel(model.texturePackRowLabel, "texture_pack_row_label", I18N::Game::TexturePack);
- syncLabel(model.texturePackTooltip, "texture_pack_tooltip", I18N::Game::TexturePackTooltip);
+ SyncLabel(model.texturePackRowLabel, "texture_pack_row_label", I18N::Game::TexturePack);
+ SyncLabel(model.texturePackTooltip, "texture_pack_tooltip", I18N::Game::TexturePackTooltip);
if (!model.texturePackLabels.empty())
- syncLabel(model.texturePackLabels.front(), "texture_pack_labels", I18N::Game::OriginalTextures);
+ SyncLabel(model.texturePackLabels.front(), "texture_pack_labels", I18N::Game::OriginalTextures);
if (model.texturePackIndex != m_iTexturePackIndex)
{
model.texturePackIndex = m_iTexturePackIndex;
diff --git a/src/source/UI/Options/OptionWindow.h b/src/source/UI/Options/OptionWindow.h
index 5975032d38..233198c450 100644
--- a/src/source/UI/Options/OptionWindow.h
+++ b/src/source/UI/Options/OptionWindow.h
@@ -164,6 +164,8 @@ namespace mu::ui::window
void RmlGaugeEvent(Rml::Event& event, int gauge);
void RmlResolutionChanged(int index);
void RmlLanguageChanged(int index);
+ // The checkbox under the language: item names in the UI language, or English.
+ void RmlToggleTranslateItemNames();
void RmlFontChanged(int index);
// Graphics tab -- DXP-23's per-system effect-cost toggles (MainScene.h), promoted from
// console-only ($effects ...) diagnostics to a persisted, in-game-options setting.
@@ -299,6 +301,8 @@ namespace mu::ui::window
int languageIndex = 0;
Rml::String languageRowLabel;
Rml::String languageValueLabel;
+ bool translateItemNames = true;
+ Rml::String translateItemNamesLabel;
std::vector fontLabels;
int fontIndex = 0;
Rml::String fontRowLabel;
@@ -401,6 +405,8 @@ namespace mu::ui::window
};
void BindRmlModel(Rml::DataModelConstructor& c, OptionRmlModel& model);
void SyncImpliedEffectRows(OptionRmlModel& model);
+ // The model's copy of a UI text, marked dirty when the text changed.
+ void SyncLabel(Rml::String& field, const char* fieldName, const wchar_t* text);
void SyncTuningModel(OptionRmlModel& model);
void OnRmlBuilt();
UI::RmlBridge::ThemedView m_RmlView{"option_window",
@@ -428,6 +434,7 @@ namespace mu::ui::window
int m_iResolutionIndex;
bool m_bWindowedMode;
int m_iLanguageIndex;
+ bool m_bTranslateItemNames;
int m_iFontIndex;
std::vector m_resolutions;
diff --git a/tests/data/test_item_database.cpp b/tests/data/test_item_database.cpp
index 1b64615aef..dcfd6b8713 100644
--- a/tests/data/test_item_database.cpp
+++ b/tests/data/test_item_database.cpp
@@ -236,6 +236,25 @@ TEST_CASE("Item names follow the display locale with English fallback [data][ite
CHECK(potion->name == L"Large Healing Potion");
}
+TEST_CASE("Another language of the item names raises the name version but not the item version [data][items]")
+{
+ ItemDatabase database;
+ database.Build(std::vector{MakeItem(SwordGroup, 0, "Kris||pt=Cris")});
+ const int version = database.GetVersion();
+ const int nameVersion = database.GetNameVersion();
+
+ database.SetDisplayLocale("en");
+ CHECK(database.GetNameVersion() == nameVersion);
+
+ database.SetDisplayLocale("pt");
+ CHECK(database.GetNameVersion() != nameVersion);
+ CHECK(database.GetVersion() == version);
+
+ const int ptNameVersion = database.GetNameVersion();
+ database.Set(*database.Find(SwordGroup, 0));
+ CHECK(database.GetNameVersion() != ptNameVersion);
+}
+
TEST_CASE("Item log names use English names and the item id [data][items]")
{
const std::vector items = {
diff --git a/tests/data/test_item_database_files.cpp b/tests/data/test_item_database_files.cpp
index 70a4a0d0ff..27fc534449 100644
--- a/tests/data/test_item_database_files.cpp
+++ b/tests/data/test_item_database_files.cpp
@@ -252,6 +252,39 @@ TEST_CASE("Loading item data fills ItemAttribute in the UI locale [data][items]"
CHECK(client.Name(KrisType) == L"Kris");
}
+// Turns translated item names back on when the test ends, as the other tests
+// expect them.
+class TranslatedItemNamesScope
+{
+public:
+ ~TranslatedItemNamesScope()
+ {
+ g_ItemDataHandler.SetTranslateItemNames(true);
+ }
+};
+
+TEST_CASE("Item names stay English while translated item names are off, also after a language change [data][items]")
+{
+ ClientDataScope client(ShippedClientDirectory);
+ TranslatedItemNamesScope translatedNames;
+ I18N::SetLocale("pt");
+ g_ItemDataHandler.SetTranslateItemNames(false);
+
+ std::string errorMessage;
+ REQUIRE(g_ItemDataHandler.Load(errorMessage));
+ CHECK(client.Name(BladeType) == L"Blade");
+
+ I18N::SetLocale("de");
+ CHECK(client.Name(BladeType) == L"Blade");
+
+ g_ItemDataHandler.SetTranslateItemNames(true);
+ CHECK(client.Name(BladeType) == L"Klinge");
+
+ g_ItemDataHandler.SetTranslateItemNames(false);
+ CHECK(client.Name(BladeType) == L"Blade");
+ CHECK(g_ItemDatabase.Find(BladeType)->name == L"Blade");
+}
+
TEST_CASE("Item data errors stop loading [data][items]")
{
TemporaryClientFolder folder;
diff --git a/tests/data/test_item_names.cpp b/tests/data/test_item_names.cpp
index 9b49c5c653..7583765947 100644
--- a/tests/data/test_item_names.cpp
+++ b/tests/data/test_item_names.cpp
@@ -36,6 +36,13 @@ TEST_CASE("An item has a name in a language when it has a translation, also one
CHECK(GetOwnName(kris, "de").empty());
}
+TEST_CASE("Item names are shown in the UI language, or in English while translated item names are off [data][items]")
+{
+ CHECK(GetItemNameLocale("de", true) == "de");
+ CHECK(GetItemNameLocale("de", false) == "en");
+ CHECK(GetItemNameLocale("en", false) == "en");
+}
+
TEST_CASE("Item names are counted by language over the items that exist [data][items]")
{
const std::vector items = {MakeNamedItem(0, "Kris||pt=Kris"),
diff --git a/tests/data/test_item_stats.cpp b/tests/data/test_item_stats.cpp
index 859c35c109..3c215e97a3 100644
--- a/tests/data/test_item_stats.cpp
+++ b/tests/data/test_item_stats.cpp
@@ -919,12 +919,21 @@ TEST_CASE("The item help window builds its content again only when what it shows
CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) != shown);
help.HeroCharacter().Class = CLASS_WIZARD;
+ // Another language changes the texts and the item names. Back in English
+ // the names were taken again; the content then stays until something changes.
I18N::SetLocale("pt");
CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) != shown);
I18N::SetLocale("en");
- CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) == shown);
+ const auto backInEnglish = GetItemHelpContentKey(ITEM_BONE_BLADE);
+ CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) == backInEnglish);
// An item editor change, which stores the item in the item database.
g_ItemDatabase.Set(*g_ItemDatabase.Find(ITEM_BONE_BLADE));
- CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) != shown);
+ CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) != backInEnglish);
+
+ // Translated item names turned on or off: only the item names change language.
+ const auto namesInEnglish = GetItemHelpContentKey(ITEM_BONE_BLADE);
+ g_ItemDatabase.SetDisplayLocale("pt");
+ CHECK(GetItemHelpContentKey(ITEM_BONE_BLADE) != namesInEnglish);
+ g_ItemDatabase.SetDisplayLocale("en");
}
diff --git a/tests/ui/test_display_settings_contract.cmake b/tests/ui/test_display_settings_contract.cmake
index c6e77f3560..76f2785ee1 100644
--- a/tests/ui/test_display_settings_contract.cmake
+++ b/tests/ui/test_display_settings_contract.cmake
@@ -145,6 +145,19 @@ foreach(required_text IN ITEMS
require_text(config_source "${required_text}" "GameConfig.cpp must persist VSync through")
endforeach()
+# Translated item names ([UI] TranslateItemNames, on by default): saved with the other [UI] keys
+# and given to the item data handler at start-up.
+require_text(config_constants "CfgDefaultTranslateItemNames = true;"
+ "GameConfigConstants.h must turn translated item names on by default")
+foreach(required_text IN ITEMS
+ "m_translateItemNames = ReadBool(CfgSectionUI, CfgKeyTranslateItemNames, CfgDefaultTranslateItemNames);"
+ "WriteBool(CfgSectionUI, CfgKeyTranslateItemNames, m_translateItemNames);")
+ require_text(config_source "${required_text}" "GameConfig.cpp must persist translated item names through")
+endforeach()
+require_text(winmain_source
+ "g_ItemDataHandler.SetTranslateItemNames(GameConfig::GetInstance().GetTranslateItemNames());"
+ "Winmain.cpp must apply translated item names at startup")
+
# A missing or invalid Look counts as CfgDefaultLook, which is Classic: Enhanced is opt-in.
require_text(config_constants "CfgDefaultLook[] = L\"Classic\""
"GameConfigConstants.h must default the look preset to Classic")