diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..02ae585834 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Auto detect text files and perform LF normalization +* text=auto + +**/Translations/*.txt text working-tree-encoding=UTF-16LE-BOM eol=crlf \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6a00922b47..caff4369bb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -45,6 +45,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + TRANSLATION_FILES: package/Interface/Translations/ + jobs: check-changes: name: Check for changes in PRs @@ -100,6 +103,56 @@ jobs: echo "should-build=true" >> $GITHUB_OUTPUT echo "hlsl-should-build=true" >> $GITHUB_OUTPUT + translate: + name: translate plugin + runs-on: ubuntu-latest + env: + ISO: .github/workflows/iso.json + TRANSLATION_CACHE: locales/ + WHITELIST_FILES: CommunityShaders_english.txt + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: pull files from Lokalise + env: + LOKALISE_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }} + PROJECT_ID: ${{ vars.PROJECT_ID }} + FILE_FORMAT: json + GITHUB_REF_NAME: ${{ github.ref_name }} + run: | + node .github/workflows/lokalise_download.mjs + + - name: convert json to utf-16-le-bom txt + run: | + for file in $TRANSLATION_CACHE/*.json; do + if [[ -f "$file" ]]; then + iso_code=$(basename "$file" .json) + language=$(jq -r --arg iso "$iso_code" '.iso_lang[$iso]' $ISO) + if [[ -z "$language" || "$language" == "null" ]]; then + echo "Warning: ISO code '$iso_code' not found. Skipping $file." >&2 + continue + fi + output_file="$TRANSLATION_FILES/CommunityShaders_${language}.txt" + if [[ "$(basename "$output_file")" != "$WHITELIST_FILES" ]]; then + printf "\xFF\xFE" > "$output_file" + jq -r 'to_entries | .[] | "\(.key)\t\(.value)"' "$file" \ + | sed 's/$/\r/' | iconv -f UTF-8 -t UTF-16LE >> "$output_file" + fi + fi + done + + - name: Upload translations + uses: actions/upload-artifact@v4 + with: + name: translations + path: ${{ env.TRANSLATION_FILES }} + cpp-build: needs: [check-changes] if: > @@ -128,6 +181,12 @@ jobs: with: arch: x64 + - name: Download translations + uses: actions/download-artifact@v4 + with: + name: translations + path: ${{ env.TRANSLATION_FILES }} + - name: Get MSVC version id: msvc_version shell: pwsh diff --git a/.github/workflows/iso.json b/.github/workflows/iso.json new file mode 100644 index 0000000000..e698808261 --- /dev/null +++ b/.github/workflows/iso.json @@ -0,0 +1,24 @@ +{ + "lang_iso": { + "english": "en", + "french": "fr", + "italian": "it", + "german": "de", + "spanish": "es", + "polish": "pl", + "chinese": "zh_CN", + "russian": "ru", + "japanese": "ja" + }, + "iso_lang": { + "en": "english", + "fr": "french", + "it": "italian", + "de": "german", + "es": "spanish", + "pl": "polish", + "zh_CN": "chinese", + "ru": "russian", + "ja": "japanese" + } +} diff --git a/.github/workflows/lokalise.yaml b/.github/workflows/lokalise.yaml new file mode 100644 index 0000000000..94ed394b9e --- /dev/null +++ b/.github/workflows/lokalise.yaml @@ -0,0 +1,45 @@ +name: push to Lokalise +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + env: + ISO: .github/workflows/iso.json + TRANSLATION_FILES: package/Interface/Translations/ + TRANSLATION_CACHE: locales/ + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: convert utf-16-le-bom txt to json + run: | + mkdir -p $TRANSLATION_CACHE + for file in "$TRANSLATION_FILES"/*.txt; do + if [[ -f "$file" ]]; then + language=$(basename "$file" | sed -E 's/CommunityShaders_(.*)\.txt/\1/') + iso_code=$(jq -r --arg lang "$language" '.lang_iso[$lang]' "$ISO") + if [[ -z "$iso_code" || "$iso_code" == "null" ]]; then + echo "Warning: Language '$language' not found. Skipping $file." >&2 + continue + fi + iconv -f UTF-16LE -t UTF-8 "$file" \ + | sed '1s/^\xEF\xBB\xBF//' \ + | grep -vE '^(;|\s*$)' \ + | jq -Rn '[ inputs | split("\t") | { (.[0]): .[1] } ] | add' \ + > "$TRANSLATION_CACHE/$iso_code.json" + fi + done + + - name: push files to Lokalise + uses: lokalise/lokalise-push-action@v3.0.0 + with: + api_token: ${{ secrets.LOKALISE_API_TOKEN }} + project_id: ${{ vars.PROJECT_ID }} + file_format: json + flat_naming: true + translations_path: | + ${{ env.TRANSLATION_CACHE }} + base_lang: en diff --git a/.github/workflows/lokalise_download.mjs b/.github/workflows/lokalise_download.mjs new file mode 100644 index 0000000000..ad16193692 --- /dev/null +++ b/.github/workflows/lokalise_download.mjs @@ -0,0 +1,117 @@ +import { execFile } from 'node:child_process'; +import { setTimeout } from 'node:timers/promises'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); + +const DEFAULT_MAX_RETRIES = 5; +const DEFAULT_SLEEP = 1; // in seconds +const MAX_SLEEP = 60; // exponential backoff limit +const MAX_TOTAL = 300; // total time limit in seconds +const DEFAULT_TIMEOUT = 120; // download command timeout (sec) + +const envOr = (name, def) => process.env[name] ?? def; +const parseIntEnv = (value, def) => isNaN(parseInt(value)) ? def : parseInt(value); +const parseBoolEnv = (value) => ['true','1','yes'].includes(String(value).toLowerCase()); + +const config = { + token: envOr('LOKALISE_TOKEN', ''), + projectId: envOr('PROJECT_ID', ''), + fileFormat: envOr('FILE_FORMAT', ''), + refName: envOr('GITHUB_REF_NAME',''), + skipIncludeTags: parseBoolEnv(envOr('SKIP_INCLUDE_TAGS', false)), + maxRetries: parseIntEnv(envOr('MAX_RETRIES', DEFAULT_MAX_RETRIES), DEFAULT_MAX_RETRIES), + sleepTime: parseIntEnv(envOr('SLEEP_TIME', DEFAULT_SLEEP), DEFAULT_SLEEP), + downloadTimeout: parseIntEnv(envOr('DOWNLOAD_TIMEOUT', DEFAULT_TIMEOUT), DEFAULT_TIMEOUT), + otherParams: envOr('CLI_ADD_PARAMS', '').trim(), +}; + +if (!config.token) { + console.error("Error: Missing LOKALISE_TOKEN env var."); + process.exit(1); +} +if (!config.projectId) { + console.error("Error: Missing PROJECT_ID env var."); + process.exit(1); +} +if (!config.fileFormat) { + console.error("Error: Missing FILE_FORMAT env var."); + process.exit(1); +} + +const isRateLimit = (txt) => txt.includes("API request error 429"); +const isNoKeysError = (txt) => txt.includes("API request error 406"); + +async function installLokaliseCLI(timeoutSec) { + console.log("Installing Lokalise CLI using the official installer script..."); + const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh"; + + await execFileAsync('sh', ['-c', `curl -sfL ${installerUrl} | sh`], { timeout: timeoutSec * 1000 }); + + console.log("Lokalise CLI installed successfully."); +} + +async function executeDownload(cmdPath, args, timeoutSec) { + const { stdout, stderr } = await execFileAsync(cmdPath, args, { timeout: timeoutSec * 1000 }); + const output = stdout + stderr; + if (isNoKeysError(output)) throw new Error(output); + if (isRateLimit(output)) throw new Error(output); + return output; +} + +async function downloadFiles(cfg) { + console.log("Starting download from Lokalise..."); + + const args = [ + `--token=${cfg.token}`, + `--project-id=${cfg.projectId}`, + 'file', 'download', + `--format=${cfg.fileFormat}`, + '--original-filenames=true', + `--directory-prefix=/`, + ]; + + if (!config.skipIncludeTags && config.refName) { + args.push(`--include-tags=${config.refName}`); + } + + let attempt = 1, sleepTime = cfg.sleepTime; + const startTime = Date.now(); + + while (attempt <= cfg.maxRetries) { + console.log(`Attempt ${attempt} of ${cfg.maxRetries}...`); + + try { + await executeDownload('./bin/lokalise2', args, cfg.downloadTimeout); + return; + } catch (err) { + const msg = err.message || String(err); + if (isNoKeysError(msg)) { + throw new Error("No keys found for export with current settings."); + } + if (isRateLimit(msg)) { + const elapsed = (Date.now() - startTime) / 1000; + if (elapsed >= MAX_TOTAL) { + throw new Error(`Max total time exceeded after ${attempt} attempts.`); + } + console.warn(`Rate-limited. Retrying in ${sleepTime}s...`); + await setTimeout(sleepTime * 1000); + sleepTime = Math.min(sleepTime * 2, MAX_SLEEP); + } else { + console.error("Unexpected error:", msg); + } + } + attempt++; + } + throw new Error(`Failed to download files after ${cfg.maxRetries} attempts.`); +} + +try { + await installLokaliseCLI(DEFAULT_TIMEOUT); + await downloadFiles(config); + console.log("Successfully downloaded files."); + process.exit(0); +} catch (err) { + console.error("Error:", err.message || err); + process.exit(1); +} \ No newline at end of file diff --git a/package/Interface/Translations/CommunityShaders_english.txt b/package/Interface/Translations/CommunityShaders_english.txt new file mode 100644 index 0000000000..f8003283ef --- /dev/null +++ b/package/Interface/Translations/CommunityShaders_english.txt @@ -0,0 +1,158 @@ +; Menu::DrawSettings + +$Community Shaders Version Community Shaders {} +$Save Settings Save Settings +$Load Settings Load Settings +$Clear Shader Cache Clear Shader Cache +$Clear Shader Cache Description The Shader Cache is the collection of compiled shaders which replace the vanilla shaders at runtime. Clearing the shader cache will mean that shaders are recompiled only when the game re-encounters them. This is only needed for hot-loading shaders for development purposes. +$Clear Disk Cache Clear Disk Cache +$Clear Disk Cache Description The Disk Cache is a collection of compiled shaders on disk, which are automatically created when shaders are added to the Shader Cache. If you do not have a Disk Cache, or it is outdated or invalid, you will see "Compiling Shaders" in the upper-left corner. After this has completed you will no longer see this message apart from when loading from the Disk Cache. Only delete the Disk Cache manually if you are encountering issues. +$Toggle Error Message Toggle Error Message +$Toggle Error Message Description Hide or show the shader failure message. Your installation is broken and will likely see errors in game. Please double check you have updated all features and that your load order is correct. See CommunityShaders.log for details and check the Nexus Mods page or Discord server. +$Feature Disabled Description Disabled at boot. Reenable, save settings, and restart. +$Feature Loading Description Feature pending restart. +$Enable at Boot Enable at Boot +$Disable at Boot Disable at Boot +$Enabled Enabled +$Disabled Disabled +$Enable Enable +$Disable Disable +$Feature State Description{}{} Current State: {}. {} the feature settings at boot. Restart will be required to reenable. This is the same as deleting the ini file. This should remove any performance impact for the feature. +$Restore Defaults Restore Defaults +$Restore Defaults Description Restores the feature's settings back to their default values. You will still need to Save Settings to make these changes permanent. +$General General +$Advanced Advanced +$Display Display +$Core Features Core Features +$Features Features +$Unloaded Features Unloaded Features +$Menu Description Please select an item on the left. + +; Menu::DrawGeneralSettings + +$Enable Shaders Enable Shaders +$Enable Shaders Description Disabling this effectively disables all features. +$Enable Disk Cache Enable Disk Cache +$Enable Disk Cache Description Disabling this stops shaders from being loaded from disk, as well as stops shaders from being saved to it. +$Enable Async Enable Async +$Enable Async Description Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast! +$Keybindings Keybindings +$Toggle Keybinding Description Press any key to set as toggle key... +$Toggle Keybinding Toggle Key: +$Effect Toggle Keybinding Description Press any key to set as a toggle key for all effects... +$Effect Toggle Keybinding Effect Toggle Key: +$Skip Compilation Keybinding Description Press any key to set as Skip Compilation Key... +$Skip Compilation Keybinding Skip Compilation Key: +$Font Font +$Font Path Font Path +$Font Path Description Enter the full path or relative path to the font file. (e.g., C:/Windows/Fonts/msyh.ttc) +$Font Size Font Size +$Invalid Font Description Invalid font path! Make sure the file exists. +$Refresh Font Refresh Font +$Theme Theme +$Sizes Sizes +$Global Scale Global Scale +$Main Main +$Window Padding Window Padding +$Frame Padding Frame Padding +$Item Spacing Item Spacing +$Item Inner Spacing Item Inner Spacing +$Indent Spacing Indent Spacing +$Scrollbar Size Scrollbar Size +$Grab Min Size Grab Min Size +$Borders Borders +$Window Border Size Window Border Size +$Child Border Size Child Border Size +$Popup Border Size Popup Border Size +$Frame Border Size Frame Border Size +$Tab Border Size Tab Border Size +$Tab Bar Border Size Tab Bar Border Size +$Rounding Rounding +$Window Rounding Window Rounding +$Child Rounding Child Rounding +$Frame Rounding Frame Rounding +$Popup Rounding Popup Rounding +$Scrollbar Rounding Scrollbar Rounding +$Grab Rounding Grab Rounding +$Tab Rounding Tab Rounding +$Tables Tables +$Cell Padding Cell Padding +$Table Angled Headers Angle Table Angled Headers Angle +$Widgets Widgets +$Color Button Position Color Button Position +$Color Button Position Option Left\0Right\0 +$Button Text Align Button Text Align +$Button Text Align Description Alignment applies when a button is larger than its text content. +$Selectable Text Align Selectable Text Align +$Selectable Text Align Description Alignment applies when a selectable is larger than its text content. +$Separator Text Border Size Separator Text Border Size +$Separator Text Align Separator Text Align +$Separator Text Padding Separator Text Padding +$Log Slider Deadzone Log Slider Deadzone +$Docking Docking +$Docking Splitter Size Docking Splitter Size +$Colors Colors +$Status Status +$Disabled Text Disabled Text +$Error Text Error Text +$Restart Needed Text Restart Needed Text +$Current Hotkey Text Current Hotkey Text +$Palette Palette +$Simple Palette Simple Palette +$Full Palette Full Palette +$Background Background +$Text Text +$Border Border +$Filter colors Filter colors + +; Menu::DrawAdvancedSettings + +$Dump Shaders Dump Shaders +$Dump Shaders Description Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this. +$Log Level Log Level +$Log Level Description Log level. Trace is most verbose. Default is info. +$Shader Defines Shader Defines +$Shader Defines Description Defines for Shader Compiler. Semicolon ";" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile. +$Compiler Threads Compiler Threads +$Compiler Threads Description Number of threads to use to compile shaders. The more threads the faster compilation will finish but may make the system unresponsive. +$Background Compiler Threads Background Compiler Threads +$Background Compiler Threads Description Number of threads to use to compile shaders while playing game. This is activated if the startup compilation is skipped. The more threads the faster compilation will finish but may make the system unresponsive. +$Test Interval Test Interval +$Test Interval Description Sets number of seconds before toggling between default USER and TEST config. 0 disables. Non-zero will enable testing mode. Enabling will save current settings as TEST config. This has no impact if no settings are changed. +$Enable File Watcher Enable File Watcher +$Enable File Watcher Description Automatically recompile shaders on file change. Intended for developing. +$Dump Ini Settings Dump Ini Settings +$Stop Blocking Shaders Stop Blocking {} Shaders +$Stop Blocking Shaders Description Stop blocking Community Shaders shader. Blocking is helpful when debugging shader errors in game to determine which shader has issues. Blocking is enabled if in developer mode and pressing PAGEUP and PAGEDOWN. Specific shader will be printed to logfile. +$Statistics Statistics +$Replace Original Shaders Replace Original Shaders +$Vertex Shader Description Replace Vertex Shaders. When false, will disable the custom Vertex Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. +$Pixel Shader Description Replace Pixel Shaders. When false, will disable the custom Pixel Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. +$Compute Shader Description Replace Compute Shaders. When false, will disable the custom Compute Shaders for the types above. For developers to test whether CS shaders match vanilla behavior. + +; Menu::DrawDisableAtBootSettings + +$Disable at Boot Disable at Boot +$Disable at Boot Description Select features to disable at boot. This is the same as deleting a feature.ini file. Restart will be required to reenable. +$Special Features Special Features + +; Menu::DrawDisplaySettings + +$Display Feature Description{} {{}} has been disabled at boot. Reenable in the Advanced -> Disable at Boot Menu. +$Display Disabled Description Display options disabled due to Skyrim Upscaler + +; Menu::DrawFooter + +$Game Version Game Version: {} {} +$D3D12 Interop D3D12 Interop: {} +$Active Active +$Inactive Inactive + +; Menu::DrawOverlay + +$Compiling Shaders Compiling Shaders: {} +$Background Compiling Shaders Background Compiling Shaders: {} +$Shader Compilation Info Shader Compilation Info +$Skip Compilation Press {} to proceed without completing shader compilation. +$Skip Compilation Description WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading. +$Skip Compilation Error Description ERROR: {} shaders failed to compile. Check installation and CommunityShaders.log \ No newline at end of file diff --git a/src/Menu.cpp b/src/Menu.cpp index abce9b6ad1..dd01cdb628 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -90,6 +90,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings, GlobalScale, + FontPath, + FontSize, UseSimplePalette, ShowActionIcons, TooltipHoverDelay, @@ -231,6 +233,40 @@ void Menu::Save(json& o_json) o_json = settings; } +void Menu::LoadFont(std::string& fontPath, float fontSize, bool refresh) +{ + static std::string lastFontPath = ""; + static float lastFontSize = 0.0f; + + if (fontPath == lastFontPath && fontSize == lastFontSize) + return; + + auto& io = ImGui::GetIO(); + io.Fonts->Clear(); + + ImFontConfig font_config; + font_config.GlyphExtraSpacing.x = -0.5; + + ImVector ranges; + ImFontGlyphRangesBuilder builder; + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull()); + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); + builder.BuildRanges(&ranges); + + io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontSize, &font_config, ranges.Data); + io.Fonts->Build(); + + if (refresh) { + ImGui_ImplDX11_InvalidateDeviceObjects(); + ImGui_ImplDX11_CreateDeviceObjects(); + } + + lastFontPath = fontPath; + lastFontSize = fontSize; +} + #define IM_VK_KEYPAD_ENTER (VK_RETURN + 256) void Menu::Init() @@ -238,6 +274,7 @@ void Menu::Init() // Setup Dear ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); + auto& imgui_io = ImGui::GetIO(); imgui_io.ConfigFlags = ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_DockingEnable; imgui_io.BackendFlags = ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_RendererHasVtxOffset; @@ -306,7 +343,8 @@ void Menu::DrawSettings() ImGui::SetNextWindowPos(Util::GetNativeViewportSizeScaled(0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(Util::GetNativeViewportSizeScaled(0.8f), ImGuiCond_FirstUseEver); - auto title = std::format("Community Shaders {}", Util::GetFormattedVersion(Plugin::VERSION)); + + auto title = "$Community Shaders Version"_i18n(Util::GetFormattedVersion(Plugin::VERSION)); // Determine window flags based on docking state ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar; @@ -662,8 +700,8 @@ void Menu::DrawSettings() float footer_height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y * 3 + 3.0f; // text + separator - ImGui::BeginChild("Menus Table", ImVec2(0, -footer_height)); - if (ImGui::BeginTable("Menus Table", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { + ImGui::BeginChild("##MenusTable", ImVec2(0, -footer_height)); + if (ImGui::BeginTable("##MenusTable", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("##ListOfMenus", 0, 2); ImGui::TableSetupColumn("##MenuConfig", 0, 8); @@ -974,14 +1012,9 @@ void Menu::DrawSettings() ImGui::PopStyleColor(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Current State: %s\n" - "%s the feature settings at boot. " - "Restart will be required to reenable. " - "This is the same as deleting the ini file. " - "This should remove any performance impact for the feature.", - isDisabled ? "Disabled" : "Enabled", - isDisabled ? "Enable" : "Disable"); + ImGui::Text("$Feature State Description{}{}"_i18n( + isDisabled ? "$Disabled" : "$Enabled", isDisabled ? "$Enable" : "$Disable") + .c_str()); } // Restore Defaults button (when feature is not disabled and is loaded) @@ -992,9 +1025,7 @@ void Menu::DrawSettings() } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Restores the feature's settings back to their default values. " - "You will still need to Save Settings to make these changes permanent."); + ImGui::Text("$Restore Defaults Description"_i18n_cs); } } } @@ -1009,9 +1040,9 @@ void Menu::DrawSettings() }); auto menuList = std::vector{ - BuiltInMenu{ "General", [&]() { DrawGeneralSettings(); } }, - BuiltInMenu{ "Advanced", [&]() { DrawAdvancedSettings(); } }, - BuiltInMenu{ "Display", [&]() { DrawDisplaySettings(); } } + BuiltInMenu{ "$General"_i18n_cs, [&]() { DrawGeneralSettings(); } }, + BuiltInMenu{ "$Advanced"_i18n_cs, [&]() { DrawAdvancedSettings(); } }, + BuiltInMenu{ "$Display"_i18n_cs, [&]() { DrawDisplaySettings(); } } }; // NOTE: The menu list is rebuilt every frame, so category expansion states // persist correctly. This is acceptable since the list is small and built // infrequently, but could be optimized if performance becomes an issue. @@ -1074,7 +1105,7 @@ void Menu::DrawSettings() return !feat->loaded && feat->IsInMenu() && (!FeatureIssues::IsObsoleteFeature(feat->GetShortName()) || globals::state->IsDeveloperMode()); }); if (std::ranges::distance(unloadedFeatures) != 0) { - menuList.push_back("Unloaded Features"s); + menuList.push_back(static_cast("$Unloaded Features"_i18n_cs)); std::ranges::copy(unloadedFeatures, std::back_inserter(menuList)); } // Add top section for feature issues (rejected features, obsolete info, etc.) @@ -1116,7 +1147,7 @@ void Menu::DrawSettings() if (selectedMenu < menuList.size()) { std::visit(DrawMenuVisitor{}, menuList[selectedMenu]); } else { - ImGui::TextDisabled("Please select an item on the left."); + ImGui::TextDisabled("$Menu Description"_i18n_cs); } ImGui::EndTable(); @@ -1144,23 +1175,23 @@ void Menu::DrawGeneralSettings() shaderCache->SetEnabled(useCustomShaders); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disabling this effectively disables all features."); + ImGui::Text("$Enable Shaders Description"_i18n_cs); } bool useDiskCache = shaderCache->IsDiskCache(); - if (ImGui::Checkbox("Enable Disk Cache", &useDiskCache)) { + if (ImGui::Checkbox("$Enable Disk Cache"_i18n_cs, &useDiskCache)) { shaderCache->SetDiskCache(useDiskCache); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disabling this stops shaders from being loaded from disk, as well as stops shaders from being saved to it."); + ImGui::Text("$Enable Disk Cache Description"_i18n_cs); } bool useAsync = shaderCache->IsAsync(); - if (ImGui::Checkbox("Enable Async", &useAsync)) { + if (ImGui::Checkbox("$Enable Async"_i18n_cs, &useAsync)) { shaderCache->SetAsync(useAsync); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!"); + ImGui::Text("$Enable Async Description"_i18n_cs); } ImGui::EndTabItem(); @@ -1362,13 +1393,13 @@ void Menu::DrawGeneralSettings() void Menu::DrawAdvancedSettings() { auto shaderCache = globals::shaderCache; - if (ImGui::CollapsingHeader("Advanced", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (ImGui::CollapsingHeader("$Advanced"_i18n_cs, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { bool useDump = shaderCache->IsDump(); - if (ImGui::Checkbox("Dump Shaders", &useDump)) { + if (ImGui::Checkbox("$Dump Shaders"_i18n_cs, &useDump)) { shaderCache->SetDump(useDump); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Dump shaders at startup. This should be used only when reversing shaders. Normal users don't need this."); + ImGui::Text("$Dump Shaders Description"_i18n_cs); } spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); const char* items[] = { @@ -1381,16 +1412,16 @@ void Menu::DrawAdvancedSettings() "off" }; static int item_current = static_cast(logLevel); - if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + if (ImGui::Combo("$Log Level"_i18n_cs, &item_current, items, IM_ARRAYSIZE(items))) { ImGui::SameLine(); globals::state->SetLogLevel(static_cast(item_current)); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Log level. Trace is most verbose. Default is info."); + ImGui::Text("$Log Level Description"_i18n_cs); } auto& shaderDefines = globals::state->shaderDefinesString; - if (ImGui::InputText("Shader Defines", &shaderDefines)) { + if (ImGui::InputText("$Shader Defines"_i18n_cs, &shaderDefines)) { globals::state->SetDefines(shaderDefines); } if (ImGui::IsItemDeactivatedAfterEdit() || (ImGui::IsItemActive() && @@ -1400,21 +1431,16 @@ void Menu::DrawAdvancedSettings() shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); + ImGui::Text("$Shader Defines Description"_i18n_cs); } ImGui::Spacing(); - ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt("$Compiler Threads"_i18n_cs, &shaderCache.compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads to use to compile shaders. " - "The more threads the faster compilation will finish but may make the system unresponsive. "); + ImGui::Text("$Compiler Threads Description"_i18n_cs); } - ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt("$Background Compiler Threads"_i18n_cs, &shaderCache.backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Number of threads to use to compile shaders while playing game. " - "This is activated if the startup compilation is skipped. " - "The more threads the faster compilation will finish but may make the system unresponsive. "); + ImGui::Text("$Background Compiler Threads Description"_i18n_cs); } // A/B Testing settings @@ -1426,25 +1452,19 @@ void Menu::DrawAdvancedSettings() shaderCache->SetFileWatcher(useFileWatcher); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Automatically recompile shaders on file change. " - "Intended for developing."); + ImGui::Text("$Enable File Watcher Description"_i18n_cs); } - if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { + if (ImGui::Button("$Dump Ini Settings"_i18n_cs, { -1, 0 })) { Util::DumpSettingsOptions(); } - if (!shaderCache->blockedKey.empty()) { - auto blockingButtonString = std::format("Stop Blocking {} Shaders", shaderCache->blockedIDs.size()); + if (!shaderCache.blockedKey.empty()) { + const std::string blockingButtonString = "$Stop Blocking Shaders"_i18n(shaderCache.blockedIDs.size()); if (ImGui::Button(blockingButtonString.c_str(), { -1, 0 })) { shaderCache->DisableShaderBlocking(); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Stop blocking Community Shaders shader. " - "Blocking is helpful when debugging shader errors in game to determine which shader has issues. " - "Blocking is enabled if in developer mode and pressing PAGEUP and PAGEDOWN. " - "Specific shader will be printed to logfile. "); + ImGui::Text("$Stop Blocking Shaders Description"_i18n_cs); } } if (ImGui::TreeNodeEx("Addresses")) { @@ -1456,14 +1476,14 @@ void Menu::DrawAdvancedSettings() ADDRESS_NODE(RendererShadowState) ImGui::TreePop(); } - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); + if (ImGui::TreeNodeEx("$Statistics"_i18n_cs, ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text(std::format("Shader Compiler : {}", shaderCache.GetShaderStatsString()).c_str()); ImGui::TreePop(); } ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); } - if (ImGui::CollapsingHeader("Replace Original Shaders", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (ImGui::CollapsingHeader("$Replace Original Shaders"_i18n_cs, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { auto state = globals::state; if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { globals::state->ForEachShaderTypeWithIndex([&](auto type, int classIndex) { @@ -1479,26 +1499,17 @@ void Menu::DrawAdvancedSettings() if (state->IsDeveloperMode()) { ImGui::Checkbox("Vertex", &state->enableVShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Vertex Shaders. " - "When false, will disable the custom Vertex Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("$Vertex Shader Description"_i18n_cs); } ImGui::Checkbox("Pixel", &state->enablePShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Pixel Shaders. " - "When false, will disable the custom Pixel Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("$Pixel Shader Description"_i18n_cs); } ImGui::Checkbox("Compute", &state->enableCShaders); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Replace Compute Shaders. " - "When false, will disable the custom Compute Shaders for the types above. " - "For developers to test whether CS shaders match vanilla behavior. "); + ImGui::Text("$Compute Shader Description"_i18n_cs); } } ImGui::EndTable(); @@ -1518,13 +1529,10 @@ void Menu::DrawDisableAtBootSettings() auto state = globals::state; auto& disabledFeatures = state->GetDisabledFeatures(); - if (ImGui::CollapsingHeader("Disable at Boot", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - ImGui::Text( - "Select features to disable at boot. " - "This is the same as deleting a feature.ini file. " - "Restart will be required to reenable."); + if (ImGui::CollapsingHeader("$Disable at Boot"_i18n_cs, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + ImGui::Text("$Disable at Boot Description"_i18n_cs); - if (ImGui::CollapsingHeader("Special Features")) { + if (ImGui::CollapsingHeader("$Special Features"_i18n_cs)) { // Prepare a sorted list of special feature names std::vector specialFeatureNames; for (const auto& [featureName, _] : state->specialFeatures) { @@ -1545,7 +1553,7 @@ void Menu::DrawDisableAtBootSettings() } } - if (ImGui::CollapsingHeader("Features")) { + if (ImGui::CollapsingHeader("$Features"_i18n_cs)) { // Prepare a sorted list of feature pointers auto featureList = Feature::GetFeatureList(); std::sort(featureList.begin(), featureList.end(), [](Feature* a, Feature* b) { @@ -1591,23 +1599,20 @@ void Menu::DrawDisplaySettings() ImGui::CollapsingHeader(featureName.c_str(), ImGuiTreeNodeFlags_NoTreePushOnOpen); ImGui::PopStyleColor(); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "%s has been disabled at boot. " - "Reenable in the Advanced -> Disable at Boot Menu.", - featureName.c_str()); + ImGui::Text("$Display Feature Description"_i18n(featureName.c_str()).c_str()); } } } } else { - ImGui::Text("Display options disabled due to Skyrim Upscaler"); + ImGui::Text("$Display Disabled Description"_i18n_cs); } } void Menu::DrawFooter() { - ImGui::BulletText(std::format("Game Version: {} {}", magic_enum::enum_name(REL::Module::GetRuntime()), Util::GetFormattedVersion(REL::Module::get().version()).c_str()).c_str()); + ImGui::BulletText("$Game Version"_i18n(magic_enum::enum_name(REL::Module::GetRuntime()), Util::GetFormattedVersion(REL::Module::get().version())).c_str()); ImGui::SameLine(); - ImGui::BulletText(std::format("D3D12 Interop: {}", globals::upscaling->d3d12Interop ? "Active" : "Inactive").c_str()); + ImGui::BulletText("$D3D12 Interop"_i18n(globals::upscaling->d3d12Interop ? "$Active"_i18n_cs : "$Inactive"_i18n_cs).c_str()); ImGui::SameLine(); ImGui::Text(std::format("GPU: {}", globals::state->adapterDescription.c_str()).c_str()); } @@ -1644,37 +1649,34 @@ void Menu::DrawOverlay() auto state = globals::state; auto& themeSettings = settings.Theme; - auto progressTitle = fmt::format("{}Compiling Shaders: {}", - shaderCache->backgroundCompilation ? "Background " : "", - shaderCache->GetShaderStatsString(!state->IsDeveloperMode()).c_str()); + auto shaderStates = shaderCache.GetShaderStatsString(!state->IsDeveloperMode()); + auto progressTitle = shaderCache.backgroundCompilation ? "$Background Compiling Shaders"_i18n(shaderStates) : "$Compiling Shaders"_i18n(shaderStates); auto percent = (float)compiledShaders / (float)totalShaders; auto progressOverlay = fmt::format("{}/{} ({:2.1f}%)", compiledShaders, totalShaders, 100 * percent); if (shaderCache->IsCompiling()) { ImGui::SetNextWindowPos(ImVec2(10, 10)); - if (!ImGui::Begin("ShaderCompilationInfo", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { + if (!ImGui::Begin("$Shader Compilation Info"_i18n_cs, nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { ImGui::End(); return; } ImGui::TextUnformatted(progressTitle.c_str()); ImGui::ProgressBar(percent, ImVec2(0.0f, 0.0f), progressOverlay.c_str()); if (!shaderCache->backgroundCompilation && shaderCache->menuLoaded) { - auto skipShadersText = fmt::format( - "Press {} to proceed without completing shader compilation. ", - KeyIdToString(settings.SkipCompilationKey)); + auto skipShadersText = "$Skip Compilation"_i18n(KeyIdToString(settings.SkipCompilationKey)); ImGui::TextUnformatted(skipShadersText.c_str()); - ImGui::TextUnformatted("WARNING: Uncompiled shaders will have visual errors or cause stuttering when loading."); + ImGui::TextUnformatted("$Skip Compilation Description"_i18n_cs); } ImGui::End(); } else if (failed) { if (!hide) { ImGui::SetNextWindowPos(ImVec2(10, 10)); - if (!ImGui::Begin("ShaderCompilationInfo", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { + if (!ImGui::Begin("$Shader Compilation Info"_i18n_cs, nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings)) { ImGui::End(); return; } - ImGui::TextColored(themeSettings.StatusPalette.Error, "ERROR: %d shaders failed to compile. Check installation and CommunityShaders.log", failed, totalShaders); + ImGui::TextColored(themeSettings.StatusPalette.Error, "$Skip Compilation Error Description"_i18n(totalShaders).c_str()); // Check for features that may cause shader compilation issues if (FeatureIssues::HasPotentialShaderModifyingFeatures()) { diff --git a/src/Menu.h b/src/Menu.h index 772fa3492b..a626b565c9 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -60,6 +60,9 @@ class Menu { float GlobalScale = REL::Module::IsVR() ? -0.5f : 0.f; // exponential + std::string FontPath = "Data\\Interface\\CommunityShaders\\Fonts\\Jost-Regular.ttf"; + float FontSize = 36; + bool UseSimplePalette = true; // simple palette or full customization bool ShowActionIcons = true; // whether to show action buttons as icons float TooltipHoverDelay = 0.5f; // tooltip hover delay in seconds @@ -189,6 +192,7 @@ class Menu uint32_t priorShaderKey = VK_PRIOR; // used for blocking shaders in debugging uint32_t nextShaderKey = VK_NEXT; // used for blocking shaders in debugging + bool fontReloadRequested = false; bool settingToggleKey = false; bool settingSkipCompilationKey = false; bool settingsEffectsToggle = false; @@ -198,6 +202,8 @@ class Menu void SetupImGuiStyle() const; const ImGuiKey VirtualKeyToImGuiKey(WPARAM vkKey); + void LoadFont(std::string& fontPath, float fontSize, bool refresh); + void DrawGeneralSettings(); void DrawAdvancedSettings(); void DrawDisplaySettings(); diff --git a/src/Util.h b/src/Util.h index 55ed6b583d..db70f47e66 100644 --- a/src/Util.h +++ b/src/Util.h @@ -6,5 +6,6 @@ #include "Utils/Game.h" #include "Utils/GameSetting.h" #include "Utils/Serialize.h" +#include "Utils/Translate.h" #include "Utils/UI.h" #include "Utils/WinApi.h" diff --git a/src/Utils/Translate.cpp b/src/Utils/Translate.cpp new file mode 100644 index 0000000000..c1e1b4a691 --- /dev/null +++ b/src/Utils/Translate.cpp @@ -0,0 +1,17 @@ +#include "Translate.h" + +#include "SKSE/Translation.h" + +namespace Util +{ + std::string Translate(const std::string& key) + { + std::string buffer; + + if (SKSE::Translation::Translate(key, buffer)) { + return buffer; + } + + return key; + } +} \ No newline at end of file diff --git a/src/Utils/Translate.h b/src/Utils/Translate.h new file mode 100644 index 0000000000..a0b667f74d --- /dev/null +++ b/src/Utils/Translate.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include + +namespace Util +{ + std::string Translate(const std::string& key); + + struct Translatable + { + std::string_view key; + + template + std::string operator()(Args&&... args) const + { + auto [keyArgs, otherArgs] = filterArgs(std::forward(args)...); + std::string preprocessedKey = preprocessKey(key, keyArgs); + std::string translatedKey = Translate(preprocessedKey); + return formatWithArgs(translatedKey, otherArgs); + } + + explicit operator std::string() const noexcept + { + return Translate(std::string(key)); + } + + private: + template + static std::pair, std::vector> filterArgs(Args&&... args) + { + std::vector keyArgs; + std::vector otherArgs; + + auto processArg = [&](auto&& arg) { + if constexpr (std::is_convertible_v) { + if (std::string_view argView(arg); argView.starts_with('$')) + keyArgs.emplace_back(argView); + else + otherArgs.emplace_back(argView); + } else { + otherArgs.emplace_back(fmt::format("{}", arg)); + } + }; + + (processArg(std::forward(args)), ...); + return { std::move(keyArgs), std::move(otherArgs) }; + } + + static std::string preprocessKey(const std::string_view key, const std::vector& keyArgs) + { + std::string result(key); + size_t pos = 0; + + for (const auto& arg : keyArgs) { + if ((pos = result.find("{}", pos)) != std::string::npos) { + result.replace(pos, 2, "{" + std::string(arg) + "}"); + pos += arg.size() + 2; + } else + break; + } + + return result; + } + + static std::string formatWithArgs(const std::string& translation, const std::vector& args) + { + std::vector formatArgs; + for (const auto& arg : args) { + formatArgs.push_back(fmt::detail::make_arg(arg)); + } + + return vformat(translation, fmt::basic_format_args(formatArgs.data(), static_cast(formatArgs.size()))); + } + }; +} + +inline Util::Translatable operator"" _i18n(const char* key, std::size_t) noexcept +{ + return Util::Translatable{ std::string_view(key) }; +} + +inline const char* operator"" _i18n_cs(const char* key, std::size_t) noexcept +{ + thread_local std::string translation; + translation = Util::Translate(std::string(key)); + return translation.c_str(); +} diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 3a8aed640a..28b72d7260 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -128,6 +128,9 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) shaderCache->WriteDiskCacheInfo(); } + if (!SKSE::Translation::ParseTranslation("CommunityShaders")) { + logger::warn("Failed to load translations for CommunityShaders"); + } if (!REL::Module::IsVR()) { RE::GetINISetting("bEnableImprovedSnow:Display")->data.b = false; RE::GetINISetting("bIBLFEnable:Display")->data.b = false;