ref:307a608045e810b29c18cdf63b1624ed12e75c23

fix(web-ui): localize config categories (#5688)

SHA: 307a608045e810b29c18cdf63b1624ed12e75c23
Author: Dave Lane <42013603+ReenigneArcher@users.noreply.github.com>
Date: 2026-09-11 15:07
Parents: 97262c8
4 files changed +62 -16
Type
docs/configuration.md +3 −0
@@ -34,6 +34,9 @@
Although it is recommended to use the configuration UI, it is possible manually configure Sunshine by
editing the `conf` file in a text editor. Use the examples as reference.
The web UI groups these settings into the sidebar categories documented below. Encoder categories are shown only when
supported on the current platform.
## General
### locale
src_assets/common/assets/web/config.html +16 −16
@@ -50,7 +50,7 @@
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
@click="currentTab = tab.id">
<component :is="getTabIcon(tab.id)" :size="18" class="icon"></component>
{{tab.name}}
{{ $t(tab.nameKey) }}
</a>
</li>
</ul>
@@ -61,7 +61,7 @@
<a class="nav-link" :class="{'active': tab.id === currentTab}" href="#"
@click="currentTab = tab.id">
<component :is="getTabIcon(tab.id)" :size="18" class="icon"></component>
{{tab.name}}
{{ $t(tab.nameKey) }}
</a>
</li>
</ul>
@@ -208,7 +208,7 @@
tabs: [ // TODO: Move the options to each Component instead, encapsulate.
{
id: "general",
name: "General",
nameKey: "config.category_general",
options: {
"locale": "en",
"sunshine_name": "",
@@ -220,7 +220,7 @@
},
{
id: "input",
name: "Input",
nameKey: "config.category_input",
options: {
"controller": "enabled",
"gamepad_driver": "",
@@ -243,7 +243,7 @@
},
{
id: "av",
name: "Audio/Video",
nameKey: "config.category_audio_video",
options: {
"audio_sink": "",
"virtual_sink": "",
@@ -267,7 +267,7 @@
},
{
id: "network",
name: "Network",
nameKey: "config.category_network",
options: {
"upnp": "disabled",
"address_family": "ipv4",
@@ -284,7 +284,7 @@
},
{
id: "files",
name: "Config Files",
nameKey: "config.category_config_files",
options: {
"file_apps": "",
"credentials_file": "",
@@ -296,7 +296,7 @@
},
{
id: "advanced",
name: "Advanced",
nameKey: "config.category_advanced",
options: {
"fec_percentage": 20,
"qp": 28,
@@ -309,7 +309,7 @@
},
{
id: "nv",
name: "NVIDIA NVENC Encoder",
nameKey: "config.category_nvidia_nvenc_encoder",
options: {
"nvenc_preset": 1,
"nvenc_twopass": "quarter_res",
@@ -324,7 +324,7 @@
},
{
id: "qsv",
name: "Intel QuickSync Encoder",
nameKey: "config.category_intel_quicksync_encoder",
options: {
"qsv_preset": "medium",
"qsv_coder": "auto",
@@ -333,7 +333,7 @@
},
{
id: "amd",
name: "AMD AMF Encoder",
nameKey: "config.category_amd_amf_encoder",
options: {
"amd_usage": "ultralowlatency",
"amd_rc": "vbr_latency",
@@ -347,7 +347,7 @@
},
{
id: "vt",
name: "VideoToolbox Encoder",
nameKey: "config.category_videotoolbox_encoder",
options: {
"vt_coder": "auto",
"vt_software": "auto",
@@ -356,7 +356,7 @@
},
{
id: "vaapi",
name: "VA-API Encoder",
nameKey: "config.category_vaapi_encoder",
options: {
"vaapi_blbrc": "disabled",
"vaapi_quality": "auto",
@@ -366,7 +366,7 @@
},
{
id: "vulkan",
name: "Vulkan Encoder",
nameKey: "config.category_vulkan_encoder",
options: {
"vk_tune": 2,
"vk_rc_mode": 2,
@@ -374,7 +374,7 @@
},
{
id: "sw",
name: "Software Encoder",
nameKey: "config.category_software_encoder",
options: {
"sw_preset": "superfast",
"sw_tune": "zerolatency",
@@ -403,7 +403,7 @@
options.push({
key: key,
label: key.replaceAll('_', ' ').replaceAll(/\b\w/g, l => l.toUpperCase()),
tab: tab.name,
tab: this.$t(tab.nameKey),
tabId: tab.id
});
});
src_assets/common/assets/web/public/assets/locale/en.json +13 −0
@@ -169,6 +169,19 @@
"bind_address_desc": "Set the specific IP address Sunshine will bind to. If left blank, Sunshine will bind to all available addresses.",
"capture": "Force a Specific Capture Method",
"capture_desc": "On automatic mode Sunshine will use the first one that works. NvFBC requires patched nvidia drivers.",
"category_advanced": "Advanced",
"category_amd_amf_encoder": "AMD AMF Encoder",
"category_audio_video": "Audio/Video",
"category_config_files": "Config Files",
"category_general": "General",
"category_input": "Input",
"category_intel_quicksync_encoder": "Intel QuickSync Encoder",
"category_network": "Network",
"category_nvidia_nvenc_encoder": "NVIDIA NVENC Encoder",
"category_software_encoder": "Software Encoder",
"category_vaapi_encoder": "VA-API Encoder",
"category_videotoolbox_encoder": "VideoToolbox Encoder",
"category_vulkan_encoder": "Vulkan Encoder",
"cert": "Certificate",
"cert_desc": "The certificate used for the web UI and Moonlight client pairing. For best compatibility, this should have an RSA-2048 public key.",
"channels": "Maximum Connected Clients",
tests/integration/test_config_consistency.cpp +30 −0
@@ -471,6 +471,36 @@
}
}
TEST_F(ConfigConsistencyTest, AllConfigSidebarTabsUseEnglishLocaleKeys) {
const std::string content = file_handler::read_file("src_assets/common/assets/web/config.html");
const std::string tabsContent = extractTabsContent(content);
const auto jsonOptions = extractEnJsonConfigOptions();
const std::regex nameKeyPattern(R"DELIM(nameKey:\s*"config\.([^"]+)")DELIM");
std::vector<std::string> errors;
size_t tabPos = 0;
while (tabPos < tabsContent.length()) {
const size_t objStart = tabsContent.find('{', tabPos);
if (objStart == std::string::npos) {
break;
}
const size_t objEnd = findClosingBrace(tabsContent, objStart);
const std::string tabObject = tabsContent.substr(objStart, objEnd - objStart + 1);
const std::string tabId = extractTabId(tabObject);
if (std::smatch nameKeyMatch; !std::regex_search(tabObject, nameKeyMatch, nameKeyPattern)) {
errors.push_back(std::format("Tab '{}' does not use a localized name key", tabId));
} else if (!jsonOptions.contains(nameKeyMatch[1].str())) {
errors.push_back(std::format("Tab '{}' references missing en.json key 'config.{}'", tabId, nameKeyMatch[1].str()));
}
tabPos = objEnd + 1;
}
EXPECT_TRUE(errors.empty()) << buildCommaSeparatedString(errors);
}
TEST_F(ConfigConsistencyTest, ConfigTabsMatchDocumentationSections) {
auto htmlOptions = extractConfigHtmlOptions();
auto mdOptions = extractConfigMdOptions();