Translate unsupported sing-tun settings errors

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-10-07 13:42:39 +08:00
parent 02f8155acb
commit 29dd6d1ebd
4 changed files with 187 additions and 7 deletions
+32
View File
@@ -3757,5 +3757,37 @@ TRANSLATION = {
"RU": "Доступно, если для «Стек» выбрано go или «Автоматически».",
"ZH": "协议栈设为go或自动时可用。",
"isReviewed": "True"
},
"Unsupported sing-tun settings fields": {
"source": [
"Furious.Window.SingTUNSettingsDialog"
],
"RU": "Неподдерживаемые поля настроек sing-tun",
"ZH": "不支持的sing-tun设置字段",
"isReviewed": "True"
},
"Unsupported sing-tun stack": {
"source": [
"Furious.Window.SingTUNSettingsDialog"
],
"RU": "Неподдерживаемый сетевой стек sing-tun",
"ZH": "不支持的sing-tun协议栈",
"isReviewed": "True"
},
"Unsupported or application-owned native option": {
"source": [
"Furious.Window.SingTUNSettingsDialog"
],
"RU": "Неподдерживаемый или управляемый приложением нативный параметр",
"ZH": "不支持的原生选项或由应用管理的选项",
"isReviewed": "True"
},
"Unsupported sing-tun host options": {
"source": [
"Furious.Window.SingTUNSettingsDialog"
],
"RU": "Неподдерживаемые параметры хоста sing-tun",
"ZH": "不支持的sing-tun主机选项",
"isReviewed": "True"
}
}
+29 -4
View File
@@ -19,6 +19,8 @@
from __future__ import annotations
from enum import Enum
import copy
import functools
import ipaddress
@@ -81,6 +83,21 @@ _STACK_FIELDS = {
}
class SingTUNUnsupportedSettingsError(ValueError):
"""Identify unsupported settings independently of their diagnostic wording."""
class Reason(Enum):
SettingsFields = 'Unsupported sing-tun settings fields'
Stack = 'Unsupported sing-tun stack'
NativeOption = 'Unsupported or application-owned native option'
HostOptions = 'Unsupported sing-tun host options'
def __init__(self, reason: Reason):
self.reason = reason
super().__init__(reason.value)
class SingTUNHostSettingsCallers:
"""Bind sing-tun's named host settings callers to a captured host document."""
@@ -156,7 +173,9 @@ def prepareSingTUNSettings(document, *, platform=None):
raise ValueError('sing-tun settings must be an object')
if set(document) - set(SING_TUN_DEFAULTS):
raise ValueError('Unsupported sing-tun settings fields')
raise SingTUNUnsupportedSettingsError(
SingTUNUnsupportedSettingsError.Reason.SettingsFields
)
result = copy.deepcopy(SING_TUN_DEFAULTS)
@@ -170,7 +189,9 @@ def prepareSingTUNSettings(document, *, platform=None):
result[key] = copy.deepcopy(value)
if result['stack'] not in ('', 'go', 'gvisor', 'system', 'mixed'):
raise ValueError('Unsupported sing-tun stack')
raise SingTUNUnsupportedSettingsError(
SingTUNUnsupportedSettingsError.Reason.Stack
)
if result['log_level'] not in ('trace', 'debug', 'info', 'warn', 'error', 'silent'):
raise ValueError('Invalid sing-tun log level')
@@ -201,7 +222,9 @@ def prepareSingTUNSettings(document, *, platform=None):
tun, stack = result['tun_options'], result['stack_options']
if set(tun) - _TUN_FIELDS or set(stack) - _STACK_FIELDS:
raise ValueError('Unsupported or application-owned native option')
raise SingTUNUnsupportedSettingsError(
SingTUNUnsupportedSettingsError.Reason.NativeOption
)
for key, expected in _OWNED.items():
if type(tun[key]) is not type(expected) or tun[key] != expected:
@@ -303,7 +326,9 @@ def prepareSingHostSettings(document):
raise ValueError('sing-tun host options must be an object')
if set(document) - set(SING_TUN_HOST_DEFAULTS):
raise ValueError('Unsupported sing-tun host options')
raise SingTUNUnsupportedSettingsError(
SingTUNUnsupportedSettingsError.Reason.HostOptions
)
result = copy.deepcopy(SING_TUN_HOST_DEFAULTS)
result.update(copy.deepcopy(document))
+22 -2
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
from Furious.Models.SingTUN import (
SING_TUN_DEFAULTS,
SING_TUN_HOST_DEFAULTS,
SingTUNUnsupportedSettingsError,
prepareSingTUNSettings,
)
from Furious.Repository import Storage
@@ -55,6 +56,18 @@ import copy
import json
def _unsupportedSettingsErrorText(error: SingTUNUnsupportedSettingsError):
"""Translate application validation categories at the presentation boundary."""
reason = SingTUNUnsupportedSettingsError.Reason
return {
reason.SettingsFields: _('Unsupported sing-tun settings fields'),
reason.Stack: _('Unsupported sing-tun stack'),
reason.NativeOption: _('Unsupported or application-owned native option'),
reason.HostOptions: _('Unsupported sing-tun host options'),
}[error.reason]
class SingTUNSettingsDialog(AppQTransientDialog):
"""Never import the native engine or mutate storage while loading widgets."""
@@ -337,7 +350,7 @@ class SingTUNSettingsDialog(AppQTransientDialog):
instructions.setWordWrap(True)
advanced.addRow(instructions)
self.errorLabel = AppQLabel('')
self.errorLabel = AppQLabel('', translatable=False)
self.errorLabel.setWordWrap(True)
layout.addWidget(self.errorLabel)
@@ -505,7 +518,14 @@ class SingTUNSettingsDialog(AppQTransientDialog):
Storage.replaceSingTUNSettings(document)
except (ValueError, TypeError, OSError) as ex:
self.errorLabel.setText(str(ex))
self.errorLabel.translatable = isinstance(
ex, SingTUNUnsupportedSettingsError
)
self.errorLabel.setText(
_unsupportedSettingsErrorText(ex)
if self.errorLabel.translatable
else str(ex)
)
return
+104 -1
View File
@@ -26,7 +26,11 @@ from Furious.Core.MultiprocessingRuntime import (
)
from Furious.Interface import CoreRuntime, RuntimeState, RuntimeExitReason
from Furious.Frozenlib import AppSettings, Mixins
from Furious.Models.SingTUN import prepareSingTUNSettings, addSingTUNExclusions
from Furious.Models.SingTUN import (
prepareSingTUNSettings,
addSingTUNExclusions,
SingTUNUnsupportedSettingsError,
)
from Furious.Frozenlib.SocksProxy import socksURL
from Furious.Repository import Storage
from Furious.Repository.SingTUNSettings import UserSingTUNSettings
@@ -594,6 +598,105 @@ class SingTUNUIAndStorageTest(unittest.TestCase):
processQtEvents()
self.assertFalse(isValid(dialog))
def testUnsupportedSettingsMessagesAreLocalizedAndRetranslatable(self):
"""Reject unknown options with localized text without committing settings."""
cases = (
({'unknown': 1}, 'Unsupported sing-tun settings fields'),
(
{'tun_options': {'unknown': True}},
'Unsupported or application-owned native option',
),
(
{'stack_options': {'unknown': True}},
'Unsupported or application-owned native option',
),
({'host_options': {'sadfasfd': 1}}, 'Unsupported sing-tun host options'),
({}, 'Unsupported sing-tun stack'),
)
for advanced, source in cases:
with self.subTest(source=source, advanced=advanced), isolatedSettings():
AppSettings.set('Language', 'ZH')
dialog = self._dialog({})
dialog.advanced.setPlainText(json.dumps(advanced))
if source == 'Unsupported sing-tun stack':
dialog.fields['stack'].addItem('unknown', 'unknown')
dialog.fields['stack'].setCurrentIndex(
dialog.fields['stack'].count() - 1
)
dialog.open()
try:
with mock.patch.object(Storage, 'replaceSingTUNSettings') as commit:
dialog.accept()
commit.assert_not_called()
self.assertTrue(dialog.isVisible())
self.assertEqual(dialog.errorLabel.text(), _(source, 'ZH'))
for language in ('ZH', 'RU', 'EN', 'ZH'):
AppSettings.set('Language', language)
Mixins.QTranslatable.retranslateAll()
processQtEvents()
expected = _(source, language)
self.assertEqual(dialog.errorLabel.text(), expected)
if language != 'EN':
self.assertNotEqual(expected, source)
finally:
dialog.reject()
processQtEvents()
def testUnsupportedErrorCategoryDoesNotDependOnDiagnosticWording(self):
"""Keep UI language keyed to the category rather than an English match."""
with isolatedSettings():
AppSettings.set('Language', 'ZH')
dialog = self._dialog({})
error = SingTUNUnsupportedSettingsError(
SingTUNUnsupportedSettingsError.Reason.HostOptions
)
error.args = ('changed diagnostic wording',)
try:
with mock.patch.object(dialog, 'document', side_effect=error):
dialog.accept()
self.assertEqual(
dialog.errorLabel.text(),
_('Unsupported sing-tun host options', 'ZH'),
)
finally:
dialog.reject()
processQtEvents()
def testOtherErrorsKeepTheirDiagnosticsAcrossLanguageChanges(self):
"""Binding and persistence diagnostics do not become category whitelists."""
for error in (
ValueError('Unsupported sing-tun host options'),
TypeError('arbitrary diagnostic detail'),
OSError('unable to save settings'),
):
with self.subTest(error=error), isolatedSettings():
AppSettings.set('Language', 'ZH')
dialog = self._dialog({})
try:
with mock.patch.object(dialog, 'document', side_effect=error):
dialog.accept()
for language in ('ZH', 'RU', 'EN'):
AppSettings.set('Language', language)
Mixins.QTranslatable.retranslateAll()
processQtEvents()
self.assertEqual(dialog.errorLabel.text(), str(error))
finally:
dialog.reject()
processQtEvents()
def testSelectiveRowsKeepInputsBoundedAndPreserveKeyboardOrderAndValues(self):
pages = (
(